2026-03-29 00:58:22 -04:00
|
|
|
|
// FloatingHUDView.swift — Content for the always-on-top focus HUD panel
|
2026-04-01 16:10:30 -05:00
|
|
|
|
// All notifications (friction, nudges, resume) render here — not in system notifications.
|
2026-03-29 00:58:22 -04:00
|
|
|
|
|
2026-04-01 16:10:30 -05:00
|
|
|
|
import AppKit
|
2026-03-29 00:58:22 -04:00
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
|
|
struct FloatingHUDView: View {
|
|
|
|
|
|
@Environment(SessionManager.self) private var session
|
|
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
|
|
|
|
header
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
content
|
|
|
|
|
|
}
|
|
|
|
|
|
.background(.ultraThinMaterial)
|
|
|
|
|
|
.clipShape(.rect(cornerRadius: 12))
|
|
|
|
|
|
.shadow(color: .black.opacity(0.25), radius: 12, x: 0, y: 4)
|
|
|
|
|
|
.frame(width: 320)
|
|
|
|
|
|
.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)
|
2026-03-29 06:29:18 -04:00
|
|
|
|
.animation(.spring(duration: 0.3), value: session.monitoringError)
|
2026-04-01 16:10:30 -05:00
|
|
|
|
.animation(.spring(duration: 0.3), value: session.nudgeMessage)
|
|
|
|
|
|
.animation(.spring(duration: 0.3), value: session.showingResumeCard)
|
2026-03-29 00:58:22 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Header
|
|
|
|
|
|
|
|
|
|
|
|
private var header: some View {
|
|
|
|
|
|
HStack(spacing: 8) {
|
2026-04-01 16:10:30 -05:00
|
|
|
|
Image(systemName: session.isSessionActive ? "eye.fill" : "eye")
|
|
|
|
|
|
.foregroundStyle(session.isSessionActive ? .blue : .secondary)
|
2026-03-29 00:58:22 -04:00
|
|
|
|
.font(.caption)
|
|
|
|
|
|
|
2026-04-01 16:10:30 -05:00
|
|
|
|
Text(session.activeTask?.title ?? (session.isSessionActive ? "Focus Session" : "Argus Monitoring"))
|
2026-03-29 00:58:22 -04:00
|
|
|
|
.font(.caption.bold())
|
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
|
|
if session.isExecuting {
|
|
|
|
|
|
Image(systemName: "bolt.fill")
|
|
|
|
|
|
.font(.caption2)
|
|
|
|
|
|
.foregroundStyle(.orange)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Circle()
|
|
|
|
|
|
.fill(session.isCapturing ? Color.green : Color.secondary.opacity(0.4))
|
|
|
|
|
|
.frame(width: 7, height: 7)
|
|
|
|
|
|
.overlay(
|
|
|
|
|
|
Circle()
|
|
|
|
|
|
.fill(session.isCapturing ? Color.green.opacity(0.3) : .clear)
|
|
|
|
|
|
.frame(width: 14, height: 14)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(.horizontal, 14)
|
|
|
|
|
|
.padding(.vertical, 10)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 16:10:30 -05:00
|
|
|
|
// MARK: - Content (priority order)
|
2026-03-29 00:58:22 -04:00
|
|
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
|
|
private var content: some View {
|
2026-04-01 16:10:30 -05:00
|
|
|
|
// 1. Error / warning banner
|
2026-03-29 06:29:18 -04:00
|
|
|
|
if let error = session.monitoringError {
|
|
|
|
|
|
MonitoringErrorBanner(message: error)
|
|
|
|
|
|
.transition(.move(edge: .top).combined(with: .opacity))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 16:10:30 -05:00
|
|
|
|
// 2. Executor output sticky card (highest priority — persists until dismissed)
|
2026-03-29 00:58:22 -04:00
|
|
|
|
if let output = session.executorOutput {
|
|
|
|
|
|
ExecutorOutputCard(title: output.title, content: output.content) {
|
|
|
|
|
|
session.executorOutput = nil
|
|
|
|
|
|
}
|
|
|
|
|
|
.transition(.move(edge: .top).combined(with: .opacity))
|
|
|
|
|
|
}
|
2026-04-01 16:10:30 -05:00
|
|
|
|
// 3. Executing spinner
|
2026-03-29 00:58:22 -04:00
|
|
|
|
else if session.isExecuting {
|
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
|
ProgressView()
|
|
|
|
|
|
.controlSize(.small)
|
|
|
|
|
|
Text("Executing action…")
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.orange)
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(14)
|
|
|
|
|
|
.transition(.opacity)
|
|
|
|
|
|
}
|
2026-04-01 16:10:30 -05:00
|
|
|
|
// 4. Resume card (shown in HUD, not as system overlay)
|
|
|
|
|
|
else if session.showingResumeCard, let card = session.resumeCard {
|
|
|
|
|
|
ResumeCardView(card: card)
|
|
|
|
|
|
.transition(.move(edge: .top).combined(with: .opacity))
|
|
|
|
|
|
}
|
|
|
|
|
|
// 5. Proactive friction / session action card
|
2026-03-29 00:58:22 -04:00
|
|
|
|
else if let card = session.proactiveCard {
|
|
|
|
|
|
HUDCardView(card: card)
|
|
|
|
|
|
.transition(.move(edge: .top).combined(with: .opacity))
|
|
|
|
|
|
}
|
2026-04-01 16:10:30 -05:00
|
|
|
|
// 6. Nudge card (amber, shown in HUD instead of system notification)
|
|
|
|
|
|
else if let nudge = session.nudgeMessage {
|
|
|
|
|
|
NudgeCardView(message: nudge)
|
|
|
|
|
|
.transition(.move(edge: .top).combined(with: .opacity))
|
|
|
|
|
|
}
|
|
|
|
|
|
// 7. Idle state — latest VLM summary
|
2026-03-29 06:29:18 -04:00
|
|
|
|
else if session.monitoringError == nil {
|
2026-04-01 16:10:30 -05:00
|
|
|
|
IdleSummaryView()
|
|
|
|
|
|
.transition(.opacity)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Nudge Card (amber — replaces UNUserNotificationCenter)
|
|
|
|
|
|
|
|
|
|
|
|
private struct NudgeCardView: View {
|
|
|
|
|
|
let message: String
|
|
|
|
|
|
@Environment(SessionManager.self) private var session
|
|
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
|
HStack(alignment: .top, spacing: 8) {
|
|
|
|
|
|
Image(systemName: "lightbulb.fill")
|
|
|
|
|
|
.foregroundStyle(.orange)
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
|
|
|
|
|
|
Text(message)
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.primary)
|
|
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
|
.lineLimit(4)
|
|
|
|
|
|
|
|
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
|
|
|
|
|
|
|
|
Button { session.dismissNudge() } label: {
|
|
|
|
|
|
Image(systemName: "xmark")
|
|
|
|
|
|
.font(.caption2.bold())
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
}
|
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(12)
|
|
|
|
|
|
.background(Color.orange.opacity(0.08))
|
|
|
|
|
|
.overlay(
|
|
|
|
|
|
Rectangle()
|
|
|
|
|
|
.frame(width: 3)
|
|
|
|
|
|
.foregroundStyle(Color.orange),
|
|
|
|
|
|
alignment: .leading
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Resume Card (warm welcome-back in HUD)
|
|
|
|
|
|
|
|
|
|
|
|
private struct ResumeCardView: View {
|
|
|
|
|
|
let card: ResumeCard
|
|
|
|
|
|
@Environment(SessionManager.self) private var session
|
|
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
|
Image(systemName: "arrow.counterclockwise.circle.fill")
|
|
|
|
|
|
.foregroundStyle(.blue)
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
Text(card.welcomeBack)
|
|
|
|
|
|
.font(.caption.bold())
|
|
|
|
|
|
.foregroundStyle(.blue)
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
Button { session.showingResumeCard = false } label: {
|
|
|
|
|
|
Image(systemName: "xmark")
|
|
|
|
|
|
.font(.caption2.bold())
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
}
|
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Text(card.youWereDoing)
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.primary)
|
|
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
|
|
|
|
|
|
|
Text(card.nextStep)
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
|
|
|
|
|
|
|
Text(card.motivation)
|
|
|
|
|
|
.font(.caption.italic())
|
|
|
|
|
|
.foregroundStyle(.blue.opacity(0.8))
|
|
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
|
|
|
|
|
|
|
Button("Got it — let's go") {
|
|
|
|
|
|
session.showingResumeCard = false
|
|
|
|
|
|
}
|
|
|
|
|
|
.font(.caption.bold())
|
|
|
|
|
|
.foregroundStyle(.white)
|
|
|
|
|
|
.padding(.horizontal, 12)
|
|
|
|
|
|
.padding(.vertical, 5)
|
|
|
|
|
|
.background(Color.blue)
|
|
|
|
|
|
.clipShape(.rect(cornerRadius: 6))
|
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(14)
|
|
|
|
|
|
.background(Color.blue.opacity(0.07))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Idle Summary View
|
|
|
|
|
|
|
|
|
|
|
|
private struct IdleSummaryView: View {
|
|
|
|
|
|
@Environment(SessionManager.self) private var session
|
|
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
|
// Step progress — only when session is active with steps
|
|
|
|
|
|
if session.isSessionActive && session.totalSteps > 0 {
|
|
|
|
|
|
HStack(spacing: 6) {
|
|
|
|
|
|
Image(systemName: "checklist")
|
|
|
|
|
|
.font(.caption2)
|
|
|
|
|
|
.foregroundStyle(.blue)
|
|
|
|
|
|
Text("Step \(min(session.completedCount + 1, session.totalSteps))/\(session.totalSteps): \(session.currentStep?.title ?? "In progress")")
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.blue)
|
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Inferred task
|
|
|
|
|
|
if let task = session.latestInferredTask, !task.isEmpty {
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
|
Text("DOING NOW")
|
|
|
|
|
|
.font(.system(size: 9, weight: .semibold))
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
.tracking(0.5)
|
2026-03-29 06:29:18 -04:00
|
|
|
|
Text(task)
|
|
|
|
|
|
.font(.caption.bold())
|
|
|
|
|
|
.foregroundStyle(.primary)
|
|
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
|
.lineLimit(2)
|
|
|
|
|
|
}
|
2026-04-01 16:10:30 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// App badge + VLM summary
|
|
|
|
|
|
HStack(alignment: .top, spacing: 6) {
|
|
|
|
|
|
if let app = session.latestAppName, !app.isEmpty {
|
|
|
|
|
|
Text(app)
|
|
|
|
|
|
.font(.system(size: 9, weight: .medium))
|
|
|
|
|
|
.foregroundStyle(.purple)
|
|
|
|
|
|
.padding(.horizontal, 5)
|
|
|
|
|
|
.padding(.vertical, 2)
|
|
|
|
|
|
.background(Color.purple.opacity(0.1))
|
|
|
|
|
|
.clipShape(.capsule)
|
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
|
}
|
2026-03-29 06:29:18 -04:00
|
|
|
|
Text(session.latestVlmSummary ?? "Monitoring your screen…")
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
|
.lineLimit(3)
|
|
|
|
|
|
}
|
2026-04-01 16:10:30 -05:00
|
|
|
|
|
|
|
|
|
|
// Distraction count badge
|
|
|
|
|
|
if session.isSessionActive && session.distractionCount > 0 {
|
|
|
|
|
|
HStack(spacing: 4) {
|
|
|
|
|
|
Image(systemName: "exclamationmark.triangle")
|
|
|
|
|
|
.font(.system(size: 9))
|
|
|
|
|
|
.foregroundStyle(.orange)
|
|
|
|
|
|
Text("\(session.distractionCount) distraction\(session.distractionCount == 1 ? "" : "s") this session")
|
|
|
|
|
|
.font(.system(size: 9))
|
|
|
|
|
|
.foregroundStyle(.orange)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-29 00:58:22 -04:00
|
|
|
|
}
|
2026-04-01 16:10:30 -05:00
|
|
|
|
.padding(14)
|
2026-03-29 00:58:22 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 16:10:30 -05:00
|
|
|
|
// MARK: - HUD Card (friction + proposed actions / session actions)
|
2026-03-29 00:58:22 -04:00
|
|
|
|
|
|
|
|
|
|
private struct HUDCardView: View {
|
|
|
|
|
|
let card: ProactiveCard
|
|
|
|
|
|
@Environment(SessionManager.self) private var session
|
|
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
|
|
|
|
// Title row
|
|
|
|
|
|
HStack(alignment: .top, spacing: 8) {
|
|
|
|
|
|
Image(systemName: card.icon)
|
|
|
|
|
|
.foregroundStyle(.purple)
|
|
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
|
|
|
|
Text(card.title)
|
|
|
|
|
|
.font(.caption.bold())
|
|
|
|
|
|
.foregroundStyle(.purple)
|
|
|
|
|
|
Text(bodyText)
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
|
.lineLimit(3)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
|
|
Button { session.dismissProactiveCard() } label: {
|
|
|
|
|
|
Image(systemName: "xmark")
|
|
|
|
|
|
.font(.caption2.bold())
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
}
|
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 06:29:18 -04:00
|
|
|
|
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: {
|
2026-04-01 16:10:30 -05:00
|
|
|
|
Text(action.label)
|
|
|
|
|
|
.font(.caption.bold())
|
|
|
|
|
|
.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())
|
2026-03-29 06:29:18 -04:00
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
|
.padding(.horizontal, 10)
|
|
|
|
|
|
.padding(.vertical, 6)
|
|
|
|
|
|
.background(Color.purple.opacity(0.12))
|
|
|
|
|
|
.clipShape(.rect(cornerRadius: 8))
|
|
|
|
|
|
}
|
2026-04-01 16:10:30 -05:00
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
|
.foregroundStyle(.purple)
|
|
|
|
|
|
|
2026-03-29 06:29:18 -04:00
|
|
|
|
notNowButton
|
|
|
|
|
|
}
|
2026-03-29 00:58:22 -04:00
|
|
|
|
|
2026-04-01 16:10:30 -05:00
|
|
|
|
case .appSwitchLoop:
|
2026-03-29 06:29:18 -04:00
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
|
|
|
|
Button {
|
|
|
|
|
|
session.approveProactiveCard(actionIndex: 0)
|
|
|
|
|
|
} label: {
|
2026-04-01 16:10:30 -05:00
|
|
|
|
Text("Help me with this")
|
2026-03-29 06:29:18 -04:00
|
|
|
|
.font(.caption.bold())
|
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
|
.padding(.horizontal, 10)
|
|
|
|
|
|
.padding(.vertical, 6)
|
|
|
|
|
|
.background(Color.purple.opacity(0.12))
|
|
|
|
|
|
.clipShape(.rect(cornerRadius: 8))
|
2026-03-29 00:58:22 -04:00
|
|
|
|
}
|
2026-03-29 06:29:18 -04:00
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
|
.foregroundStyle(.purple)
|
2026-04-01 16:10:30 -05:00
|
|
|
|
|
2026-03-29 06:29:18 -04:00
|
|
|
|
notNowButton
|
2026-03-29 00:58:22 -04:00
|
|
|
|
}
|
2026-03-29 06:29:18 -04:00
|
|
|
|
|
|
|
|
|
|
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"
|
2026-04-01 16:10:30 -05:00
|
|
|
|
case "start_new": return "Create task + start focus session"
|
2026-03-29 06:29:18 -04:00
|
|
|
|
default: return "OK"
|
2026-03-29 00:58:22 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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?"
|
2026-04-01 16:10:30 -05:00
|
|
|
|
case .sessionAction(_, _, let checkpoint, let reason, _, _):
|
2026-03-29 06:29:18 -04:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-03-29 00:58:22 -04:00
|
|
|
|
}
|
2026-03-29 06:29:18 -04:00
|
|
|
|
.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
|
|
|
|
|
|
)
|
2026-03-29 00:58:22 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Executor Output Sticky Card
|
|
|
|
|
|
|
|
|
|
|
|
private struct ExecutorOutputCard: View {
|
|
|
|
|
|
let title: String
|
|
|
|
|
|
let content: String
|
|
|
|
|
|
let onDismiss: () -> Void
|
|
|
|
|
|
|
2026-04-01 16:10:30 -05:00
|
|
|
|
@State private var copied = false
|
|
|
|
|
|
|
|
|
|
|
|
private var maxScrollHeight: CGFloat {
|
|
|
|
|
|
let screenHeight = NSScreen.main?.visibleFrame.height ?? 800
|
|
|
|
|
|
return max(120, screenHeight - 157)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 00:58:22 -04:00
|
|
|
|
var body: some View {
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
|
HStack(spacing: 6) {
|
|
|
|
|
|
Image(systemName: "checkmark.circle.fill")
|
|
|
|
|
|
.foregroundStyle(.green)
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
Text(title)
|
|
|
|
|
|
.font(.caption.bold())
|
|
|
|
|
|
.foregroundStyle(.green)
|
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
Button { onDismiss() } label: {
|
|
|
|
|
|
Image(systemName: "xmark")
|
|
|
|
|
|
.font(.caption2.bold())
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
}
|
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ScrollView {
|
|
|
|
|
|
Text(content)
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.primary)
|
2026-04-01 16:10:30 -05:00
|
|
|
|
.textSelection(.enabled)
|
2026-03-29 00:58:22 -04:00
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
|
}
|
2026-04-01 16:10:30 -05:00
|
|
|
|
.frame(maxHeight: maxScrollHeight)
|
2026-03-29 00:58:22 -04:00
|
|
|
|
|
2026-04-01 16:10:30 -05:00
|
|
|
|
HStack {
|
|
|
|
|
|
Button("Dismiss") { onDismiss() }
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
Button {
|
|
|
|
|
|
NSPasteboard.general.clearContents()
|
|
|
|
|
|
NSPasteboard.general.setString(content, forType: .string)
|
|
|
|
|
|
copied = true
|
|
|
|
|
|
Task {
|
|
|
|
|
|
try? await Task.sleep(for: .seconds(2))
|
|
|
|
|
|
copied = false
|
|
|
|
|
|
}
|
|
|
|
|
|
} label: {
|
|
|
|
|
|
Label(copied ? "Copied!" : "Copy", systemImage: copied ? "checkmark" : "doc.on.doc")
|
|
|
|
|
|
.font(.caption.bold())
|
|
|
|
|
|
.foregroundStyle(copied ? AnyShapeStyle(.secondary) : AnyShapeStyle(Color.green))
|
|
|
|
|
|
}
|
2026-03-29 00:58:22 -04:00
|
|
|
|
.buttonStyle(.plain)
|
2026-04-01 16:10:30 -05:00
|
|
|
|
.animation(.easeInOut(duration: 0.15), value: copied)
|
|
|
|
|
|
}
|
2026-03-29 00:58:22 -04:00
|
|
|
|
}
|
|
|
|
|
|
.padding(14)
|
|
|
|
|
|
.background(Color.green.opacity(0.07))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|