include argus workflow

This commit is contained in:
joyzhuo
2026-03-29 06:29:18 -04:00
parent 275a53ab40
commit 56673078f5
23 changed files with 3098 additions and 307 deletions

View File

@@ -18,6 +18,7 @@ struct FloatingHUDView: View {
.animation(.spring(duration: 0.3), value: session.proactiveCard?.id)
.animation(.spring(duration: 0.3), value: session.isExecuting)
.animation(.spring(duration: 0.3), value: session.executorOutput?.title)
.animation(.spring(duration: 0.3), value: session.monitoringError)
}
// MARK: - Header
@@ -58,6 +59,12 @@ struct FloatingHUDView: View {
@ViewBuilder
private var content: some View {
// Error / warning banner shown above all other content when monitoring has a problem
if let error = session.monitoringError {
MonitoringErrorBanner(message: error)
.transition(.move(edge: .top).combined(with: .opacity))
}
// Executor output sticky card (highest priority persists until dismissed)
if let output = session.executorOutput {
ExecutorOutputCard(title: output.title, content: output.content) {
@@ -83,13 +90,23 @@ struct FloatingHUDView: View {
.transition(.move(edge: .top).combined(with: .opacity))
}
// Latest VLM summary (idle state)
else {
Text(session.latestVlmSummary ?? "Monitoring your screen…")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
.padding(14)
.transition(.opacity)
else if session.monitoringError == nil {
VStack(alignment: .leading, spacing: 4) {
if let task = session.latestInferredTask, !task.isEmpty {
Text(task)
.font(.caption.bold())
.foregroundStyle(.primary)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
}
Text(session.latestVlmSummary ?? "Monitoring your screen…")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(3)
}
.padding(14)
.transition(.opacity)
}
}
}
@@ -128,58 +145,147 @@ private struct HUDCardView: View {
.buttonStyle(.plain)
}
// Action buttons only for VLM friction with proposed actions
if case .vlmFriction(_, _, let actions) = card.source, !actions.isEmpty {
VStack(alignment: .leading, spacing: 6) {
ForEach(Array(actions.prefix(2).enumerated()), id: \.offset) { index, action in
Button {
session.approveProactiveCard(actionIndex: index)
} label: {
VStack(alignment: .leading, spacing: 2) {
Text(action.label)
.font(.caption.bold())
.lineLimit(2)
.multilineTextAlignment(.leading)
if let details = action.details, !details.isEmpty {
Text(details)
.font(.caption2)
.foregroundStyle(.purple.opacity(0.7))
.lineLimit(2)
.multilineTextAlignment(.leading)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.purple.opacity(0.12))
.clipShape(.rect(cornerRadius: 8))
}
.buttonStyle(.plain)
.foregroundStyle(.purple)
}
Button("Not now — I'm good") { session.dismissProactiveCard() }
.font(.caption)
.foregroundStyle(.secondary)
.buttonStyle(.plain)
.padding(.top, 2)
}
}
// Action buttons
actionButtons
}
.padding(14)
.background(Color.purple.opacity(0.07))
}
@ViewBuilder
private var actionButtons: some View {
switch card.source {
case .vlmFriction(_, _, let actions) where !actions.isEmpty:
VStack(alignment: .leading, spacing: 6) {
ForEach(Array(actions.prefix(2).enumerated()), id: \.offset) { index, action in
Button {
session.approveProactiveCard(actionIndex: index)
} label: {
VStack(alignment: .leading, spacing: 2) {
Text(action.label)
.font(.caption.bold())
.lineLimit(2)
.multilineTextAlignment(.leading)
if let details = action.details, !details.isEmpty {
Text(details)
.font(.caption2)
.foregroundStyle(.purple.opacity(0.7))
.lineLimit(2)
.multilineTextAlignment(.leading)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.purple.opacity(0.12))
.clipShape(.rect(cornerRadius: 8))
}
.buttonStyle(.plain)
.foregroundStyle(.purple)
}
notNowButton
}
case .sessionAction(let type, _, _, _, _):
VStack(alignment: .leading, spacing: 6) {
Button {
session.approveProactiveCard(actionIndex: 0)
} label: {
Text(sessionActionButtonLabel(type))
.font(.caption.bold())
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.purple.opacity(0.12))
.clipShape(.rect(cornerRadius: 8))
}
.buttonStyle(.plain)
.foregroundStyle(.purple)
notNowButton
}
default:
EmptyView()
}
}
private var notNowButton: some View {
Button("Not now — I'm good") { session.dismissProactiveCard() }
.font(.caption)
.foregroundStyle(.secondary)
.buttonStyle(.plain)
.padding(.top, 2)
}
private func sessionActionButtonLabel(_ type: String) -> String {
switch type {
case "resume": return "Resume session"
case "switch": return "Switch to this task"
case "complete": return "Mark complete"
case "start_new": return "Start focus session"
default: return "OK"
}
}
private var bodyText: String {
switch card.source {
case .vlmFriction(_, let description, _):
return description ?? "I noticed something that might be slowing you down."
case .appSwitchLoop(let apps, let count):
return "You've switched between \(apps.joined(separator: "")) \(count)× — are you stuck?"
case .sessionAction(_, _, let checkpoint, let reason, _):
if !checkpoint.isEmpty { return "Left off: \(checkpoint)" }
return reason.isEmpty ? "Argus noticed a session change." : reason
}
}
}
// MARK: - Monitoring Error Banner
private struct MonitoringErrorBanner: View {
let message: String
@Environment(SessionManager.self) private var session
private var isRestarting: Bool { message.contains("restarting") }
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Image(systemName: isRestarting ? "arrow.clockwise" : "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(isRestarting ? .orange : .red)
.symbolEffect(.pulse, isActive: isRestarting)
Text(message)
.font(.caption)
.foregroundStyle(isRestarting ? .orange : .red)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 0)
}
if !isRestarting {
Button("Retry") { session.retryMonitoring() }
.font(.caption.bold())
.foregroundStyle(.white)
.padding(.horizontal, 12)
.padding(.vertical, 5)
.background(Color.red.opacity(0.8))
.clipShape(.rect(cornerRadius: 6))
.buttonStyle(.plain)
}
}
.padding(12)
.background(isRestarting ? Color.orange.opacity(0.08) : Color.red.opacity(0.08))
.overlay(
Rectangle()
.frame(width: 3)
.foregroundStyle(isRestarting ? Color.orange : Color.red),
alignment: .leading
)
}
}
// MARK: - Executor Output Sticky Card
private struct ExecutorOutputCard: View {