Files
openagent/ios_dashboard/OpenAgentDashboard/Models/Mission.swift
Thomas Marchand b519f02b62 Th0rgal/ios compat review (#37)
* Add hardcoded Google/Gemini OAuth credentials

Use the same client credentials as Gemini CLI for seamless OAuth flow.
This removes the need for GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET env vars.

* Add iOS Settings view and first-launch setup flow

- Add SetupSheet for configuring server URL on first launch
- Add SettingsView for managing server URL and appearance
- Add isConfigured flag to APIService to detect unconfigured state
- Show setup sheet automatically when no server URL is configured

* Add iOS global workspace state management

- Add WorkspaceState singleton for shared workspace selection
- Refactor ControlView to use global workspace state
- Refactor FilesView with workspace picker in toolbar
- Refactor HistoryView with workspace picker in toolbar
- Refactor TerminalView with workspace picker and improved UI
- Update Xcode project with new files

* Add reusable EnvVarsEditor component and fix page scrolling

- Extract EnvVarsEditor as reusable component with password masking
- Refactor workspaces page to use EnvVarsEditor component
- Refactor workspace-templates page to use EnvVarsEditor component
- Fix workspace-templates page to use h-screen with overflow-hidden
- Add min-h-0 to flex containers to enable proper internal scrolling
- Environment and Init Script tabs now scroll internally

* Improve workspace creation UX and build log auto-scroll

- Auto-scroll build log to bottom when new content arrives
- Fix chroot workspace creation to show correct building status immediately
- Prevent status flicker by triggering build before closing dialog

* Improve iOS control view empty state and input styling

- Show workspace name in empty state subtitle
- Distinguish between host and isolated workspaces
- Refine input field alignment and padding

* Add production security and self-hosting documentation

- Add Section 10: TLS + Reverse Proxy setup (Caddy and Nginx examples)
- Add Section 11: Authentication modes documentation (disabled, single tenant, multi-user)
- Add Section 12: Dashboard configuration (web and iOS)
- Add Section 13: OAuth provider setup information
- Add Production Deployment Checklist

* fix: wip

* wip

* Improve settings sync UX and fix failed mission display

Settings page:
- Add out-of-sync warning when Library and System settings differ
- Add post-save modal prompting to restart OpenCode
- Load both Library and System settings for comparison

Control client:
- Fix missionHistoryToItems to show "Failed" status for failed missions
- Last assistant message now inherits mission's failed status
- Show resume button for failed resumable missions

* Fix: restore original URL on connection failure in SetupSheet

Previously, SetupSheet.connectToServer() persisted the URL before validation.
If the health check failed, the invalid URL remained in UserDefaults, causing
the app to skip the setup flow on next launch and attempt to connect to an
unreachable server. Now the original URL is restored on failure, matching
the behavior in SettingsView.testConnection().

* Fix: restore queueLength on failed removal in ControlView

The removeFromQueue function now properly saves and restores both
queuedItems and queueLength on API error, matching the behavior of
clearQueue. Previously only queuedItems was refreshed via loadQueueItems()
while queueLength remained incorrectly decremented until the next SSE event.

* Add selective encryption for template environment variables

- Add lock/unlock icon to each env var row for encryption toggle
- When locking, automatically hide value and show eye icon
- Auto-enable encryption when key matches sensitive patterns
- Backend selectively encrypts only keys in encrypted_keys array
- Backwards compatible: detects encrypted values in legacy templates
- Refactor workspaces page to use SWR for data fetching

Frontend:
- env-vars-editor.tsx: Add encrypted field, lock toggle, getEncryptedKeys()
- api.ts: Add encrypted_keys to WorkspaceTemplate types
- workspaces/page.tsx: Use SWR, pass encrypted_keys on save
- workspace-templates/page.tsx: Load/save encrypted_keys

Backend:
- library/types.rs: Add encrypted_keys field to WorkspaceTemplate
- library/mod.rs: Selective encryption logic + legacy detection
- api/library.rs: Accept encrypted_keys in save request

* Fix: Settings Cancel restores URL and queue ops refresh on error

SettingsView:
- Store original URL at view init and restore it on Cancel
- Ensures Cancel properly discards unsaved changes including tested URLs

ControlView:
- Queue operations now refresh from server on error instead of restoring
  captured state, avoiding race conditions with concurrent operations

* Fix: preserve undefined for encrypted_keys to enable auto-detection

Passing `template.encrypted_keys || []` converted undefined to an empty
array, which broke the auto-detection logic in toEnvRows. The nullish
coalescing in `encryptedKeys?.includes(key) ?? secret` only falls back
to `secret` when encryptedKeys is undefined, not when it's an empty array.

* Add Queue button and fix SSE/desktop session handling

- Dashboard: Show Queue button when agent is busy to allow message queuing
- OpenCode: Fix SSE inactivity timeout to only reset on meaningful events,
  not heartbeats, preventing false timeout resets
- Desktop: Deduplicate sessions by display to prevent showing duplicate entries
- Docs: Add dashboard password to installation prerequisites

* Fix race conditions in default agent selection and workspace creation

- Fix default agent config being ignored: wait for config to finish loading
  before setting defaults to prevent race between agents and config SWR fetches
- Fix workspace list not refreshing after build failure: move mutateWorkspaces
  call to immediately after createWorkspace, add try/catch around getWorkspace

* Fix encryption lock icon and add skill content encryption

- Fix lock icon showing unlocked for sensitive keys when encrypted_keys is
  empty: now falls back to auto-detection based on key name patterns
- Add showEncryptionToggle prop to EnvVarsEditor to conditionally show
  encryption toggle (only for workspace templates)
- Add skill content encryption with <encrypted>...</encrypted> tags
- Update config pages with consistent styling and encryption support
2026-01-16 01:41:11 -08:00

248 lines
6.8 KiB
Swift

//
// Mission.swift
// OpenAgentDashboard
//
// Mission and task data models
//
import Foundation
enum MissionStatus: String, Codable, CaseIterable {
case active
case completed
case failed
case interrupted
case blocked
case notFeasible = "not_feasible"
var statusType: StatusType {
switch self {
case .active: return .active
case .completed: return .completed
case .failed: return .failed
case .interrupted: return .interrupted
case .blocked: return .blocked
case .notFeasible: return .failed
}
}
var displayLabel: String {
switch self {
case .active: return "Active"
case .completed: return "Completed"
case .failed: return "Failed"
case .interrupted: return "Interrupted"
case .blocked: return "Blocked"
case .notFeasible: return "Not Feasible"
}
}
var canResume: Bool {
self == .interrupted || self == .blocked
}
}
struct MissionHistoryEntry: Codable, Identifiable {
var id: String { "\(role)-\(content.prefix(20))" }
let role: String
let content: String
var isUser: Bool {
role == "user"
}
}
struct Mission: Codable, Identifiable, Hashable {
let id: String
var status: MissionStatus
let title: String?
let workspaceId: String?
let history: [MissionHistoryEntry]
let createdAt: String
let updatedAt: String
let interruptedAt: String?
let resumable: Bool
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Mission, rhs: Mission) -> Bool {
lhs.id == rhs.id
}
enum CodingKeys: String, CodingKey {
case id, status, title, history, resumable
case workspaceId = "workspace_id"
case createdAt = "created_at"
case updatedAt = "updated_at"
case interruptedAt = "interrupted_at"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
status = try container.decode(MissionStatus.self, forKey: .status)
title = try container.decodeIfPresent(String.self, forKey: .title)
workspaceId = try container.decodeIfPresent(String.self, forKey: .workspaceId)
history = try container.decode([MissionHistoryEntry].self, forKey: .history)
createdAt = try container.decode(String.self, forKey: .createdAt)
updatedAt = try container.decode(String.self, forKey: .updatedAt)
interruptedAt = try container.decodeIfPresent(String.self, forKey: .interruptedAt)
resumable = try container.decodeIfPresent(Bool.self, forKey: .resumable) ?? false
}
var displayTitle: String {
if let title = title, !title.isEmpty {
return title.count > 60 ? String(title.prefix(60)) + "..." : title
}
return "Untitled Mission"
}
var updatedDate: Date? {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter.date(from: updatedAt) ?? ISO8601DateFormatter().date(from: updatedAt)
}
var canResume: Bool {
resumable && status.canResume
}
}
enum TaskStatus: String, Codable, CaseIterable {
case pending
case running
case completed
case failed
case cancelled
var statusType: StatusType {
switch self {
case .pending: return .pending
case .running: return .running
case .completed: return .completed
case .failed: return .failed
case .cancelled: return .cancelled
}
}
}
struct TaskState: Codable, Identifiable {
let id: String
let status: TaskStatus
let task: String
let model: String
let iterations: Int
let result: String?
var displayModel: String {
if let lastPart = model.split(separator: "/").last {
return String(lastPart)
}
return model
}
}
// MARK: - Queue
struct QueuedMessage: Codable, Identifiable {
let id: String
let content: String
let agent: String?
/// Truncated content for display (max 100 chars)
var displayContent: String {
if content.count > 100 {
return String(content.prefix(100)) + "..."
}
return content
}
}
// MARK: - Parallel Execution
struct RunningMissionInfo: Codable, Identifiable {
let missionId: String
let state: String
let queueLen: Int
let historyLen: Int
let secondsSinceActivity: Int
let expectedDeliverables: Int
var id: String { missionId }
enum CodingKeys: String, CodingKey {
case missionId = "mission_id"
case state
case queueLen = "queue_len"
case historyLen = "history_len"
case secondsSinceActivity = "seconds_since_activity"
case expectedDeliverables = "expected_deliverables"
}
// Memberwise initializer for previews and testing
init(missionId: String, state: String, queueLen: Int, historyLen: Int, secondsSinceActivity: Int, expectedDeliverables: Int) {
self.missionId = missionId
self.state = state
self.queueLen = queueLen
self.historyLen = historyLen
self.secondsSinceActivity = secondsSinceActivity
self.expectedDeliverables = expectedDeliverables
}
var isRunning: Bool {
state == "running" || state == "waiting_for_tool"
}
var isStalled: Bool {
isRunning && secondsSinceActivity > 60
}
/// Short identifier for the mission (first 8 chars of ID)
var shortId: String {
String(missionId.prefix(8)).uppercased()
}
}
struct ParallelConfig: Codable {
let maxParallelMissions: Int
let runningCount: Int
enum CodingKeys: String, CodingKey {
case maxParallelMissions = "max_parallel_missions"
case runningCount = "running_count"
}
}
// MARK: - Runs
struct Run: Codable, Identifiable {
let id: String
let createdAt: String
let status: String
let inputText: String
let finalOutput: String?
let totalCostCents: Int
let summaryText: String?
enum CodingKeys: String, CodingKey {
case id, status
case createdAt = "created_at"
case inputText = "input_text"
case finalOutput = "final_output"
case totalCostCents = "total_cost_cents"
case summaryText = "summary_text"
}
var costDollars: Double {
Double(totalCostCents) / 100.0
}
var createdDate: Date? {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter.date(from: createdAt) ?? ISO8601DateFormatter().date(from: createdAt)
}
}