new macOS version
This commit is contained in:
@@ -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]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user