new macOS version

This commit is contained in:
2026-04-01 16:10:30 -05:00
parent 56673078f5
commit 483e3c1d00
13 changed files with 2302 additions and 319 deletions

View File

@@ -265,6 +265,26 @@ final class APIClient {
// MARK: - Sessions
/// Returns all active + interrupted sessions (for VLM session context).
func getOpenSessions() async throws -> [OpenSession] {
do {
let data = try await req("/sessions/open")
return try decode([OpenSession].self, from: data)
} catch NetworkError.httpError(404, _) {
return []
}
}
/// Create a task detected by the VLM from screen analysis.
func createVLMTask(title: String) async throws -> AppTask {
let body = try JSONSerialization.data(withJSONObject: [
"title": title,
"source": "vlm_detected"
])
let data = try await req("/tasks", method: "POST", body: body)
return try decode(AppTask.self, from: data)
}
/// Returns the currently active session, or nil if none (404).
func getActiveSession() async throws -> FocusSession? {
do {
@@ -318,6 +338,28 @@ final class APIClient {
_ = try await req("/sessions/\(sessionId)/checkpoint", method: "POST", body: body)
}
// MARK: - Nudge (cross-device)
/// Send a focus-session nudge via the backend push pipeline to all signed-in devices.
func sendNudge(
sessionId: String,
title: String,
body: String,
nudgeNumber: Int,
lastStep: String?,
nextStep: String?
) async throws {
var dict: [String: Any] = [
"title": title,
"body": body,
"nudge_number": nudgeNumber,
]
if let ls = lastStep { dict["last_step"] = ls }
if let ns = nextStep { dict["next_step"] = ns }
let bodyData = try JSONSerialization.data(withJSONObject: dict)
_ = try await req("/sessions/\(sessionId)/nudge", method: "POST", body: bodyData)
}
// MARK: - App Activity
func appActivity(
@@ -352,14 +394,16 @@ final class APIClient {
if let stepId = result.currentStepId { payload["current_step_id"] = stepId }
if let note = result.checkpointNoteUpdate { payload["checkpoint_note_update"] = note }
if let app = result.appName { payload["app_name"] = app }
if let nudge = result.gentleNudge { payload["gentle_nudge"] = nudge }
if let notif = result.notification {
payload["notification"] = ["type": notif.type, "message": notif.message as Any]
}
if let friction = result.friction {
payload["friction"] = [
"type": friction.type,
"confidence": friction.confidence,
"description": friction.description as Any,
"proposed_actions": friction.proposedActions.map {
["label": $0.label, "action_type": $0.actionType, "details": $0.details as Any]
["label": $0.label, "details": $0.details as Any]
},
]
}

View File

@@ -83,7 +83,7 @@ struct FocusSessionView: View {
}
// Latest nudge
if let nudge = session.lastNudge {
if let nudge = session.nudgeMessage {
NudgeCard(message: nudge)
}
@@ -401,7 +401,7 @@ private struct ProactiveCardView: View {
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)× in a row — are you stuck?"
case .sessionAction(_, _, let checkpoint, let reason, _):
case .sessionAction(_, _, let checkpoint, let reason, _, _):
return checkpoint.isEmpty ? reason : "Left off: \(checkpoint)"
}
}

View File

@@ -2,10 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.security.app-sandbox</key>
<false/>
</dict>

View File

@@ -18,6 +18,11 @@ struct MenuBarView: View {
Divider()
// Settings
settingsSection
Divider()
// Bottom
HStack {
Text(auth.currentUser?.displayName ?? auth.currentUser?.email ?? "LockInBro")
@@ -120,6 +125,31 @@ struct MenuBarView: View {
}
.padding(.vertical, 4)
}
private var settingsSection: some View {
HStack(spacing: 8) {
Image(systemName: "bell.badge")
.foregroundStyle(.secondary)
.frame(width: 16)
Text("Nudge after")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Picker("", selection: Binding(
get: { Int(session.distractionThresholdSeconds) },
set: { session.distractionThresholdSeconds = TimeInterval($0) }
)) {
Text("1 min").tag(60)
Text("2 min").tag(120)
Text("3 min").tag(180)
Text("5 min").tag(300)
}
.pickerStyle(.menu)
.frame(width: 80)
.font(.caption)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
}
}
// MARK: - Menu Bar Button

View File

@@ -208,14 +208,8 @@ struct StepPlanResponse: Codable {
/// A single action the proactive agent can take on the user's behalf.
struct ProposedAction: Codable {
let label: String // e.g. "Extract all 14 events"
let actionType: String // e.g. "auto_extract", "brain_dump"
let details: String?
enum CodingKeys: String, CodingKey {
case label, details
case actionType = "action_type"
}
let label: String // e.g. "Extract all 14 events into transcript.md"
let details: String? // Executor instruction spec (not shown as UI text)
}
/// Friction pattern detected by the upgraded Argus VLM prompt.
@@ -244,14 +238,25 @@ struct SessionAction: Codable {
/// resume | switch | complete | start_new | none
let type: String
let sessionId: String?
/// When start_new matches an existing task, the VLM sets this to the task's ID.
let taskId: String?
let reason: String?
enum CodingKeys: String, CodingKey {
case type, reason
case sessionId = "session_id"
case taskId = "task_id"
}
}
/// VLM-decided notification intent replaces the old gentle_nudge field.
struct VLMNotification: Codable {
/// "none" | "nudge" | "friction"
let type: String
/// Populated when type == "nudge"; nil otherwise.
let message: String?
}
struct DistractionAnalysisResponse: Codable {
let onTask: Bool
let currentStepId: String?
@@ -260,12 +265,13 @@ struct DistractionAnalysisResponse: Codable {
let stepsCompleted: [String]
// Upgraded Argus prompt fields (nil when backend uses legacy prompt)
let friction: FrictionInfo?
let sessionAction: SessionAction? // new argus: session lifecycle suggestions
let sessionAction: SessionAction?
/// VLM explicitly decides what to show: none | nudge | friction
let notification: VLMNotification?
let intent: String? // skimming | engaged | unclear | null
let distractionType: String?
let appName: String?
let confidence: Double
let gentleNudge: String?
let vlmSummary: String?
enum CodingKeys: String, CodingKey {
@@ -274,12 +280,11 @@ struct DistractionAnalysisResponse: Codable {
case inferredTask = "inferred_task"
case checkpointNoteUpdate = "checkpoint_note_update"
case stepsCompleted = "steps_completed"
case friction, intent
case friction, notification, intent
case sessionAction = "session_action"
case distractionType = "distraction_type"
case appName = "app_name"
case confidence
case gentleNudge = "gentle_nudge"
case vlmSummary = "vlm_summary"
}
}
@@ -347,11 +352,14 @@ struct ProactiveCard: Identifiable {
/// Heuristic app-switch loop detected by NSWorkspace observer (fallback when VLM hasn't returned friction yet).
case appSwitchLoop(apps: [String], switchCount: Int)
/// VLM suggests a session lifecycle action (new argus: resume, switch, complete, start_new).
case sessionAction(type: String, taskTitle: String, checkpoint: String, reason: String, sessionId: String?)
/// taskId: if start_new matches an existing unstarted task, this is set so we link instead of creating.
case sessionAction(type: String, taskTitle: String, checkpoint: String, reason: String, sessionId: String?, taskId: String?)
}
let id = UUID()
let source: Source
/// For start_new: an existing task from the database that matches the inferred work.
var matchedTask: AppTask?
/// Human-readable title for the card header.
var title: String {
@@ -366,7 +374,7 @@ struct ProactiveCard: Identifiable {
}
case .appSwitchLoop:
return "Repetitive Pattern Detected"
case .sessionAction(let type, let taskTitle, _, _, _):
case .sessionAction(let type, let taskTitle, _, _, _, _):
switch type {
case "resume": return "Resume: \(taskTitle)"
case "switch": return "Switch to: \(taskTitle)"
@@ -390,7 +398,7 @@ struct ProactiveCard: Identifiable {
}
case .appSwitchLoop:
return "arrow.triangle.2.circlepath"
case .sessionAction(let type, _, _, _, _):
case .sessionAction(let type, _, _, _, _, _):
switch type {
case "resume": return "arrow.counterclockwise.circle"
case "switch": return "arrow.left.arrow.right"
@@ -402,6 +410,45 @@ struct ProactiveCard: Identifiable {
}
}
// MARK: - Open Sessions (GET /sessions/open for VLM session context)
struct OpenSessionTask: Codable {
let title: String
let goal: String?
}
struct OpenSessionCheckpoint: Codable {
let activeApp: String?
let activeFile: String?
let currentStepId: String?
let lastActionSummary: String?
enum CodingKeys: String, CodingKey {
case activeApp = "active_app"
case activeFile = "active_file"
case currentStepId = "current_step_id"
case lastActionSummary = "last_action_summary"
}
}
struct OpenSession: Identifiable, Codable {
let id: String
let taskId: String?
let task: OpenSessionTask?
let status: String // active | interrupted
let platform: String
let startedAt: String
let endedAt: String?
let checkpoint: OpenSessionCheckpoint?
enum CodingKeys: String, CodingKey {
case id, task, status, platform, checkpoint
case taskId = "task_id"
case startedAt = "started_at"
case endedAt = "ended_at"
}
}
// MARK: - API Error
struct APIErrorResponse: Codable {

File diff suppressed because it is too large Load Diff