@lattices/cli 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -13
  3. package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
  4. package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/{app → apps/mac}/Package.swift +2 -1
  6. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  7. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  8. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  9. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  10. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
  11. package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
  12. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
  13. package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
  14. package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
  15. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
  16. package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
  17. package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
  18. package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
  19. package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
  20. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
  21. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
  22. package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
  23. package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
  24. package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
  25. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
  26. package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
  27. package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
  28. package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
  29. package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
  30. package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +1 -0
  31. package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
  32. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
  33. package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
  34. package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
  35. package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
  36. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
  37. package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
  38. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
  39. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
  40. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
  41. package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
  42. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
  43. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
  44. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
  45. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
  46. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
  47. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +46 -27
  48. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
  49. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
  50. package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
  51. package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
  52. package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
  53. package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
  54. package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
  55. package/apps/mac/Sources/Core/System/Capability.swift +79 -0
  56. package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
  57. package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
  58. package/bin/handsoff-infer.ts +14 -5
  59. package/bin/handsoff-worker.ts +11 -7
  60. package/bin/infer.ts +406 -0
  61. package/bin/lattices-app.ts +57 -7
  62. package/bin/lattices-dev +40 -1
  63. package/bin/lattices.ts +1 -1
  64. package/docs/agent-execution-plan.md +9 -9
  65. package/docs/api.md +119 -0
  66. package/docs/app.md +1 -0
  67. package/docs/companion-deck.md +1 -1
  68. package/docs/gesture-customization-proposal.md +520 -0
  69. package/docs/mouse-gestures.md +79 -0
  70. package/docs/overview.md +2 -2
  71. package/docs/presentation-execution-review.md +9 -9
  72. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  73. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  74. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  75. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  76. package/docs/reference/dewey.config.ts +74 -0
  77. package/docs/reference/install-agent.md +79 -0
  78. package/docs/repo-structure.md +100 -0
  79. package/docs/voice-error-model.md +7 -7
  80. package/docs/voice.md +18 -0
  81. package/package.json +23 -13
  82. package/swift/Package.swift +20 -0
  83. package/swift/Sources/DeckKit/DeckAction.swift +51 -0
  84. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
  85. package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
  86. package/swift/Sources/DeckKit/DeckHost.swift +7 -0
  87. package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
  88. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
  89. package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
  90. package/swift/Sources/DeckKit/DeckValue.swift +93 -0
  91. package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
  92. package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
  93. package/app/Sources/AppShell/AppDelegate.swift +0 -408
  94. package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
  95. package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
  96. package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
  97. package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
  98. /package/{app → apps/mac}/Info.plist +0 -0
  99. /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  100. /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
  101. /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
  102. /package/{app → apps/mac}/Lattices.entitlements +0 -0
  103. /package/{app → apps/mac}/Resources/tap.wav +0 -0
  104. /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
  105. /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
  106. /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
  107. /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
  108. /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
  109. /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
  110. /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
  111. /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
  112. /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
  113. /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
  114. /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
  115. /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
  116. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
  117. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
  118. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
  119. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
  120. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
  121. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
  122. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
  123. /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
  124. /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
  125. /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
  126. /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
  127. /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
  128. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
  129. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
  130. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
  131. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
  132. /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
  133. /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
  134. /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
  135. /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
  136. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
  137. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
  138. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
  139. /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
  140. /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
  141. /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
  142. /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
  143. /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
  144. /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
  145. /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
  146. /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
  147. /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
  148. /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
  149. /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
  150. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
  151. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
  152. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
  153. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
  154. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
  155. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
  156. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
  157. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
  158. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
  159. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
  160. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
  161. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
  162. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
  163. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
  164. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
  165. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
  166. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
  167. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
  168. /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
  169. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
  170. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
  171. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
  172. /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
  173. /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
  174. /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
  175. /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
  176. /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
  177. /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
  178. /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
  179. /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
  180. /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
  181. /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
  182. /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
  183. /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
  184. /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
  185. /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
  186. /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
  187. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
  188. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
  189. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
  190. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
  191. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
  192. /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
  193. /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
  194. /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
  195. /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
  196. /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
  197. /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
  198. /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
  199. /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
  200. /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
  201. /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
@@ -40,7 +40,7 @@ struct PiProvider: Identifiable, Equatable {
40
40
  authMode: .oauth,
41
41
  tokenLabel: "OAuth",
42
42
  tokenPlaceholder: "",
43
- helpText: "Uses Pi's device-code login. Personal access tokens are not accepted on this path."
43
+ helpText: "Uses device-code login. Personal access tokens are not accepted on this path."
44
44
  ),
45
45
  PiProvider(
46
46
  id: "openai-codex",
@@ -48,7 +48,7 @@ struct PiProvider: Identifiable, Equatable {
48
48
  authMode: .oauth,
49
49
  tokenLabel: "OAuth",
50
50
  tokenPlaceholder: "",
51
- helpText: "Uses Pi's browser login for ChatGPT Plus/Pro Codex access."
51
+ helpText: "Uses browser login for ChatGPT Plus/Pro Codex access."
52
52
  ),
53
53
  PiProvider(
54
54
  id: "openai",
@@ -56,7 +56,7 @@ struct PiProvider: Identifiable, Equatable {
56
56
  authMode: .apiKey,
57
57
  tokenLabel: "API key",
58
58
  tokenPlaceholder: "sk-...",
59
- helpText: "Stores an OpenAI API key in Pi's auth.json for this app and Pi CLI to reuse."
59
+ helpText: "Stores an OpenAI API key for this app and the provider runtime to reuse."
60
60
  ),
61
61
  PiProvider(
62
62
  id: "anthropic",
@@ -64,7 +64,7 @@ struct PiProvider: Identifiable, Equatable {
64
64
  authMode: .apiKey,
65
65
  tokenLabel: "API key",
66
66
  tokenPlaceholder: "sk-ant-...",
67
- helpText: "Stores an Anthropic API key for Pi. OAuth-capable Anthropic flows can be added later."
67
+ helpText: "Stores an Anthropic API key for provider-backed chat."
68
68
  ),
69
69
  PiProvider(
70
70
  id: "google",
@@ -72,7 +72,7 @@ struct PiProvider: Identifiable, Equatable {
72
72
  authMode: .apiKey,
73
73
  tokenLabel: "API key",
74
74
  tokenPlaceholder: "AIza...",
75
- helpText: "Stores a Gemini API key for Pi's Google provider."
75
+ helpText: "Stores a Gemini API key for provider-backed chat."
76
76
  ),
77
77
  PiProvider(
78
78
  id: "openrouter",
@@ -80,7 +80,7 @@ struct PiProvider: Identifiable, Equatable {
80
80
  authMode: .apiKey,
81
81
  tokenLabel: "API key",
82
82
  tokenPlaceholder: "sk-or-...",
83
- helpText: "Stores an OpenRouter API key for Pi."
83
+ helpText: "Stores an OpenRouter API key for provider-backed chat."
84
84
  ),
85
85
  PiProvider(
86
86
  id: "groq",
@@ -88,7 +88,7 @@ struct PiProvider: Identifiable, Equatable {
88
88
  authMode: .apiKey,
89
89
  tokenLabel: "API key",
90
90
  tokenPlaceholder: "gsk_...",
91
- helpText: "Stores a Groq API key for Pi."
91
+ helpText: "Stores a Groq API key for provider-backed chat."
92
92
  ),
93
93
  PiProvider(
94
94
  id: "xai",
@@ -96,7 +96,7 @@ struct PiProvider: Identifiable, Equatable {
96
96
  authMode: .apiKey,
97
97
  tokenLabel: "API key",
98
98
  tokenPlaceholder: "xai-...",
99
- helpText: "Stores an xAI API key for Pi."
99
+ helpText: "Stores an xAI API key for provider-backed chat."
100
100
  ),
101
101
  PiProvider(
102
102
  id: "mistral",
@@ -104,7 +104,7 @@ struct PiProvider: Identifiable, Equatable {
104
104
  authMode: .apiKey,
105
105
  tokenLabel: "API key",
106
106
  tokenPlaceholder: "",
107
- helpText: "Stores a Mistral API key for Pi."
107
+ helpText: "Stores a Mistral API key for provider-backed chat."
108
108
  ),
109
109
  PiProvider(
110
110
  id: "minimax",
@@ -112,7 +112,7 @@ struct PiProvider: Identifiable, Equatable {
112
112
  authMode: .apiKey,
113
113
  tokenLabel: "API key",
114
114
  tokenPlaceholder: "",
115
- helpText: "Stores a MiniMax API key for Pi."
115
+ helpText: "Stores a MiniMax API key for provider-backed chat."
116
116
  ),
117
117
  ]
118
118
 
@@ -128,7 +128,7 @@ final class PiChatSession: ObservableObject {
128
128
  @Published private(set) var messages: [PiChatMessage] = [
129
129
  PiChatMessage(
130
130
  role: .system,
131
- text: "Pi dock ready. This is a lightweight in-app conversation surface, not a full terminal.",
131
+ text: "Assistant ready. This is a lightweight in-app conversation surface, not a full terminal.",
132
132
  timestamp: Date()
133
133
  )
134
134
  ]
@@ -151,6 +151,7 @@ final class PiChatSession: ObservableObject {
151
151
  }
152
152
  UserDefaults.standard.set(authProviderID, forKey: Self.selectedProviderDefaultsKey)
153
153
  authToken = ""
154
+ isEditingStoredCredential = false
154
155
  authPromptInput = ""
155
156
  pendingAuthPrompt = nil
156
157
  authNoticeText = nil
@@ -163,6 +164,7 @@ final class PiChatSession: ObservableObject {
163
164
  }
164
165
  }
165
166
  @Published var authToken: String = ""
167
+ @Published var isEditingStoredCredential: Bool = false
166
168
  @Published var authPromptInput: String = ""
167
169
  @Published private(set) var isAuthenticating: Bool = false
168
170
  @Published private(set) var authenticatingProviderID: String?
@@ -177,6 +179,8 @@ final class PiChatSession: ObservableObject {
177
179
 
178
180
  private let queue = DispatchQueue(label: "pi-chat-session", qos: .userInitiated)
179
181
  private let sessionFileURL: URL
182
+ private let voiceAdvisorSessionFileURL: URL
183
+ private let voiceResolverSessionFileURL: URL
180
184
  private let authFileURL: URL
181
185
  private var authProcess: Process?
182
186
  private var authProcessIdentifier: Int32?
@@ -198,6 +202,8 @@ final class PiChatSession: ObservableObject {
198
202
  let dir = base.appendingPathComponent("Lattices/pi-chat", isDirectory: true)
199
203
  try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
200
204
  sessionFileURL = dir.appendingPathComponent("session.jsonl")
205
+ voiceAdvisorSessionFileURL = dir.appendingPathComponent("voice-advisor.jsonl")
206
+ voiceResolverSessionFileURL = dir.appendingPathComponent("voice-resolver.jsonl")
201
207
  authFileURL = Self.piAgentDirURL().appendingPathComponent("auth.json")
202
208
 
203
209
  if let savedProvider = UserDefaults.standard.string(forKey: Self.selectedProviderDefaultsKey),
@@ -218,6 +224,10 @@ final class PiChatSession: ObservableObject {
218
224
  piBinaryPath != nil
219
225
  }
220
226
 
227
+ var isProviderInferenceReady: Bool {
228
+ hasPiBinary && !needsProviderSetup
229
+ }
230
+
221
231
  var piInstallCommand: String {
222
232
  Self.installCommand
223
233
  }
@@ -288,7 +298,7 @@ final class PiChatSession: ObservableObject {
288
298
  return prompt.message
289
299
  }
290
300
  if latestAuthURL == nil {
291
- return "Stay here for a second while Pi prepares the browser step."
301
+ return "Stay here for a second while the sign-in page is prepared."
292
302
  }
293
303
  if authVerificationCode != nil {
294
304
  return authVerificationCodeCopied
@@ -313,7 +323,7 @@ final class PiChatSession: ObservableObject {
313
323
 
314
324
  var setupStatusSummary: String {
315
325
  if !hasPiBinary {
316
- return "Install Pi to enable the assistant"
326
+ return "Install the provider runtime to enable provider chat"
317
327
  }
318
328
  if isAuthenticating {
319
329
  return authStepShortText
@@ -360,7 +370,6 @@ final class PiChatSession: ObservableObject {
360
370
  isAuthPanelVisible = true
361
371
  statusText = "connecting..."
362
372
  } else if needsProviderSetup {
363
- isAuthPanelVisible = true
364
373
  statusText = "setup ai"
365
374
  } else if hasPiBinary && (statusText == "setup ai" || statusText == "missing pi") {
366
375
  statusText = "idle"
@@ -389,12 +398,12 @@ final class PiChatSession: ObservableObject {
389
398
  func copyPiInstallCommand() {
390
399
  NSPasteboard.general.clearContents()
391
400
  NSPasteboard.general.setString(piInstallCommand, forType: .string)
392
- appendSystemMessage("Copied the Pi install command to the clipboard.")
401
+ appendSystemMessage("Copied the provider runtime install command to the clipboard.")
393
402
  }
394
403
 
395
404
  func installPiInTerminal() {
396
405
  Preferences.shared.terminal.launch(command: piInstallCommand, in: NSHomeDirectory())
397
- appendSystemMessage("Opened \(Preferences.shared.terminal.rawValue) and started the Pi install.")
406
+ appendSystemMessage("Opened \(Preferences.shared.terminal.rawValue) and started the provider runtime install.")
398
407
  }
399
408
 
400
409
  func sendDraft() {
@@ -409,6 +418,14 @@ final class PiChatSession: ObservableObject {
409
418
  guard !trimmed.isEmpty else { return }
410
419
  guard !isSending else { return }
411
420
 
421
+ messages.append(PiChatMessage(role: .user, text: trimmed, timestamp: Date()))
422
+
423
+ if let localResponse = handleLocalSettingsCommand(trimmed) {
424
+ messages.append(PiChatMessage(role: .assistant, text: localResponse, timestamp: Date()))
425
+ statusText = hasPiBinary ? (needsProviderSetup ? "setup ai" : "idle") : "missing pi"
426
+ return
427
+ }
428
+
412
429
  refreshBinaryAvailability()
413
430
 
414
431
  guard let piPath = piBinaryPath else {
@@ -419,14 +436,15 @@ final class PiChatSession: ObservableObject {
419
436
 
420
437
  guard !needsProviderSetup else {
421
438
  prepareForDisplay()
439
+ isAuthPanelVisible = true
440
+ dockHeight = max(dockHeight, 300)
422
441
  return
423
442
  }
424
443
 
425
- messages.append(PiChatMessage(role: .user, text: trimmed, timestamp: Date()))
426
-
427
444
  let provider = currentProvider
428
445
  isSending = true
429
446
  statusText = "thinking..."
447
+ let prompt = providerPrompt(for: trimmed)
430
448
 
431
449
  queue.async { [weak self] in
432
450
  guard let self else { return }
@@ -437,7 +455,7 @@ final class PiChatSession: ObservableObject {
437
455
  "--provider", provider.id,
438
456
  "-p",
439
457
  "--session", self.sessionFileURL.path,
440
- trimmed,
458
+ prompt,
441
459
  ]
442
460
 
443
461
  var env = ProcessInfo.processInfo.environment
@@ -469,7 +487,7 @@ final class PiChatSession: ObservableObject {
469
487
  DispatchQueue.main.async {
470
488
  self.isSending = false
471
489
  self.statusText = "launch failed"
472
- self.appendSystemMessage("Failed to launch Pi: \(error.localizedDescription)")
490
+ self.appendSystemMessage("Failed to launch the provider runtime: \(error.localizedDescription)")
473
491
  }
474
492
  return
475
493
  }
@@ -487,7 +505,7 @@ final class PiChatSession: ObservableObject {
487
505
  return
488
506
  }
489
507
 
490
- let message = !stderr.isEmpty ? stderr : (stdout.isEmpty ? "Pi returned no output." : stdout)
508
+ let message = !stderr.isEmpty ? stderr : (stdout.isEmpty ? "The provider runtime returned no output." : stdout)
491
509
  if let friendly = self.friendlyAuthFailureMessage(for: message) {
492
510
  self.statusText = "setup ai"
493
511
  self.authErrorText = friendly
@@ -504,6 +522,140 @@ final class PiChatSession: ObservableObject {
504
522
  }
505
523
  }
506
524
 
525
+ func askVoiceAdvisor(transcript: String, matched: String, callback: @escaping (AgentResponse?) -> Void) {
526
+ runProviderInference(
527
+ prompt: voiceAdvisorPrompt(transcript: transcript, matched: matched),
528
+ sessionURL: voiceAdvisorSessionFileURL,
529
+ label: "voice advisor"
530
+ ) { output in
531
+ guard let output, !output.isEmpty else {
532
+ callback(nil)
533
+ return
534
+ }
535
+ callback(AgentResponse.parse(text: output))
536
+ }
537
+ }
538
+
539
+ func answerVoiceQuestion(_ transcript: String, callback: @escaping (AgentResponse?) -> Void) {
540
+ runProviderInference(
541
+ prompt: voiceQuestionPrompt(transcript: transcript),
542
+ sessionURL: voiceAdvisorSessionFileURL,
543
+ label: "voice question"
544
+ ) { output in
545
+ guard let output, !output.isEmpty else {
546
+ callback(nil)
547
+ return
548
+ }
549
+ callback(AgentResponse(commentary: output, suggestion: nil, raw: output))
550
+ }
551
+ }
552
+
553
+ func resolveVoiceIntent(transcript: String, callback: @escaping (ResolvedIntent?) -> Void) {
554
+ runProviderInference(
555
+ prompt: voiceResolverPrompt(transcript: transcript),
556
+ sessionURL: voiceResolverSessionFileURL,
557
+ label: "voice resolver"
558
+ ) { output in
559
+ guard let output,
560
+ let jsonStr = Self.extractJSON(from: output),
561
+ let data = jsonStr.data(using: .utf8),
562
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
563
+ let intent = json["intent"] as? String,
564
+ intent != "unknown" else {
565
+ callback(nil)
566
+ return
567
+ }
568
+
569
+ var slots: [String: JSON] = [:]
570
+ if let rawSlots = json["slots"] as? [String: Any] {
571
+ for (key, value) in rawSlots {
572
+ if let value = value as? String {
573
+ slots[key] = .string(value)
574
+ } else if let value = value as? Int {
575
+ slots[key] = .int(value)
576
+ } else if let value = value as? Bool {
577
+ slots[key] = .bool(value)
578
+ }
579
+ }
580
+ }
581
+ callback(ResolvedIntent(intent: intent, slots: slots))
582
+ }
583
+ }
584
+
585
+ private func runProviderInference(
586
+ prompt: String,
587
+ sessionURL: URL,
588
+ label: String,
589
+ callback: @escaping (String?) -> Void
590
+ ) {
591
+ refreshBinaryAvailability()
592
+
593
+ guard let piPath = piBinaryPath else {
594
+ DiagnosticLog.shared.info("Assistant inference[\(label)]: provider runtime not installed")
595
+ callback(nil)
596
+ return
597
+ }
598
+ guard !needsProviderSetup else {
599
+ DiagnosticLog.shared.info("Assistant inference[\(label)]: selected provider needs credentials")
600
+ callback(nil)
601
+ return
602
+ }
603
+
604
+ let provider = currentProvider
605
+ let hasStoredCredential = storedCredentialKinds[provider.id] != nil
606
+
607
+ queue.async {
608
+ let timer = DiagnosticLog.shared.startTimed("Assistant inference[\(label)] via \(provider.name)")
609
+
610
+ let proc = Process()
611
+ proc.executableURL = URL(fileURLWithPath: piPath)
612
+ proc.arguments = [
613
+ "--provider", provider.id,
614
+ "-p",
615
+ "--session", sessionURL.path,
616
+ prompt,
617
+ ]
618
+
619
+ var env = ProcessInfo.processInfo.environment
620
+ env.removeValue(forKey: "CLAUDECODE")
621
+ Self.sanitizeEnvironment(&env, for: provider.id, hasStoredCredential: hasStoredCredential)
622
+ proc.environment = env
623
+
624
+ let outPipe = Pipe()
625
+ let errPipe = Pipe()
626
+ proc.standardOutput = outPipe
627
+ proc.standardError = errPipe
628
+
629
+ do {
630
+ try proc.run()
631
+ proc.waitUntilExit()
632
+ } catch {
633
+ DiagnosticLog.shared.warn("Assistant inference[\(label)]: launch failed — \(error)")
634
+ DiagnosticLog.shared.finish(timer)
635
+ DispatchQueue.main.async { callback(nil) }
636
+ return
637
+ }
638
+
639
+ DiagnosticLog.shared.finish(timer)
640
+ let stdout = String(data: outPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
641
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
642
+ let stderr = String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
643
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
644
+
645
+ if !stderr.isEmpty {
646
+ DiagnosticLog.shared.info("Assistant inference[\(label)] stderr: \(stderr.prefix(200))")
647
+ }
648
+
649
+ guard proc.terminationStatus == 0, !stdout.isEmpty else {
650
+ DiagnosticLog.shared.info("Assistant inference[\(label)]: empty/error response")
651
+ DispatchQueue.main.async { callback(nil) }
652
+ return
653
+ }
654
+
655
+ DispatchQueue.main.async { callback(stdout) }
656
+ }
657
+ }
658
+
507
659
  func saveSelectedToken() {
508
660
  let token = authToken.trimmingCharacters(in: .whitespacesAndNewlines)
509
661
  guard !token.isEmpty else {
@@ -519,10 +671,11 @@ final class PiChatSession: ObservableObject {
519
671
  ]
520
672
  }
521
673
  authToken = ""
674
+ isEditingStoredCredential = false
522
675
  authNoticeText = "Saved \(currentProvider.tokenLabel.lowercased()) for \(currentProvider.name)."
523
676
  authErrorText = nil
524
677
  reloadAuthState()
525
- appendSystemMessage("Saved \(currentProvider.name) credentials to Pi auth storage.")
678
+ appendSystemMessage("Saved \(currentProvider.name) credentials.")
526
679
  isAuthPanelVisible = false
527
680
  prepareForDisplay()
528
681
  } catch {
@@ -537,8 +690,9 @@ final class PiChatSession: ObservableObject {
537
690
  }
538
691
  authNoticeText = "Removed saved credentials for \(currentProvider.name)."
539
692
  authErrorText = nil
693
+ isEditingStoredCredential = true
540
694
  reloadAuthState()
541
- appendSystemMessage("Removed saved \(currentProvider.name) credentials from Pi auth storage.")
695
+ appendSystemMessage("Removed saved \(currentProvider.name) credentials.")
542
696
  prepareForDisplay()
543
697
  } catch {
544
698
  authErrorText = "Failed to remove credentials: \(error.localizedDescription)"
@@ -554,6 +708,19 @@ final class PiChatSession: ObservableObject {
554
708
  startOAuthLogin(for: currentProvider)
555
709
  }
556
710
 
711
+ func beginReplacingSelectedCredential() {
712
+ authToken = ""
713
+ authErrorText = nil
714
+ authNoticeText = nil
715
+ isEditingStoredCredential = true
716
+ }
717
+
718
+ func cancelReplacingSelectedCredential() {
719
+ authToken = ""
720
+ authErrorText = nil
721
+ isEditingStoredCredential = false
722
+ }
723
+
557
724
  func submitAuthPrompt() {
558
725
  guard let prompt = pendingAuthPrompt else { return }
559
726
  let value = authPromptInput.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -563,7 +730,7 @@ final class PiChatSession: ObservableObject {
563
730
 
564
731
  private func submitAuthPromptValue(_ value: String) {
565
732
  guard let handle = authInputHandle else {
566
- authErrorText = "Pi auth input pipe is no longer available."
733
+ authErrorText = "The auth input pipe is no longer available."
567
734
  return
568
735
  }
569
736
 
@@ -639,17 +806,17 @@ final class PiChatSession: ObservableObject {
639
806
  refreshBinaryAvailability()
640
807
 
641
808
  guard hasPiBinary else {
642
- authErrorText = "Install Pi before starting auth."
809
+ authErrorText = "Install the provider runtime before starting auth."
643
810
  return
644
811
  }
645
812
 
646
813
  guard let nodePath = nodeBinaryPath else {
647
- authErrorText = "Node.js is required for Pi OAuth login."
814
+ authErrorText = "Node.js is required for OAuth login."
648
815
  return
649
816
  }
650
817
 
651
818
  guard let oauthModuleURL = resolveOAuthModuleURL() else {
652
- authErrorText = "Couldn't locate Pi's OAuth module next to the installed `pi` CLI."
819
+ authErrorText = "Couldn't locate the OAuth module next to the installed provider runtime."
653
820
  return
654
821
  }
655
822
 
@@ -783,11 +950,11 @@ final class PiChatSession: ObservableObject {
783
950
  NSWorkspace.shared.open(url)
784
951
  }
785
952
  if authVerificationCode != nil {
786
- appendSystemMessage("Pi auth is ready. The sign-in code is copied, and you can reopen the browser page here if needed.")
953
+ appendSystemMessage("Auth is ready. The sign-in code is copied, and you can reopen the browser page here if needed.")
787
954
  } else if let instructions, !instructions.isEmpty {
788
- appendSystemMessage("Pi auth: \(instructions) If nothing opened, use OPEN AGAIN.")
955
+ appendSystemMessage("Auth: \(instructions) If nothing opened, use OPEN AGAIN.")
789
956
  } else {
790
- appendSystemMessage("Pi auth is ready in your browser. If nothing opened, use OPEN AGAIN.")
957
+ appendSystemMessage("Auth is ready in your browser. If nothing opened, use OPEN AGAIN.")
791
958
  }
792
959
 
793
960
  case "progress":
@@ -795,7 +962,7 @@ final class PiChatSession: ObservableObject {
795
962
 
796
963
  case "success":
797
964
  guard var credentials = json["credentials"] as? [String: Any] else {
798
- authErrorText = "Pi auth completed but returned no credentials."
965
+ authErrorText = "Auth completed but returned no credentials."
799
966
  return
800
967
  }
801
968
  let providerID = authenticatingProviderID ?? authProviderID
@@ -808,7 +975,7 @@ final class PiChatSession: ObservableObject {
808
975
  reloadAuthState()
809
976
  authNoticeText = "Saved OAuth credentials for \(provider.name)."
810
977
  authErrorText = nil
811
- appendSystemMessage("Saved \(provider.name) OAuth credentials to Pi auth storage.")
978
+ appendSystemMessage("Saved \(provider.name) OAuth credentials.")
812
979
  isAuthPanelVisible = false
813
980
  prepareForDisplay()
814
981
  } catch {
@@ -816,9 +983,9 @@ final class PiChatSession: ObservableObject {
816
983
  }
817
984
 
818
985
  case "error":
819
- let message = json["message"] as? String ?? "Unknown Pi auth error."
986
+ let message = json["message"] as? String ?? "Unknown auth error."
820
987
  authErrorText = message
821
- appendSystemMessage("Pi auth failed: \(message)")
988
+ appendSystemMessage("Auth failed: \(message)")
822
989
 
823
990
  default:
824
991
  authNoticeText = line
@@ -886,9 +1053,9 @@ final class PiChatSession: ObservableObject {
886
1053
  private func structuredWelcomeMessage() -> String {
887
1054
  if !hasPiBinary {
888
1055
  return """
889
- Welcome to Pi Workspace.
1056
+ Welcome to the Workspace Assistant.
890
1057
 
891
- Pi powers the in-app assistant. Install it first, then come back here and refresh.
1058
+ I can manage app settings here. Install the provider runtime to unlock provider-backed chat for longer planning and coding prompts.
892
1059
 
893
1060
  Install command:
894
1061
  \(piInstallCommand)
@@ -897,7 +1064,7 @@ final class PiChatSession: ObservableObject {
897
1064
 
898
1065
  if isAuthenticating {
899
1066
  return """
900
- Welcome to Pi Workspace.
1067
+ Welcome to the Workspace Assistant.
901
1068
 
902
1069
  \(authStepTitle)
903
1070
 
@@ -907,21 +1074,487 @@ final class PiChatSession: ObservableObject {
907
1074
 
908
1075
  if needsProviderSetup {
909
1076
  return """
910
- Welcome to Pi Workspace.
1077
+ Welcome to the Workspace Assistant.
911
1078
 
912
1079
  Next step: connect \(currentProvider.name).
913
1080
 
914
- The setup panel above is open. Once you finish that one step, the chat box unlocks automatically.
1081
+ Open Settings with the gear icon, choose a provider, and save its API key to unlock provider-backed chat.
915
1082
  """
916
1083
  }
917
1084
 
918
1085
  return """
919
- Welcome to Pi Workspace.
1086
+ Welcome to the Workspace Assistant.
1087
+
1088
+ You're connected with \(currentProvider.name). I can manage Lattices settings locally, or use the provider for code help, planning, debugging, and second opinions.
1089
+ """
1090
+ }
1091
+
1092
+ private func handleLocalSettingsCommand(_ text: String) -> String? {
1093
+ let lower = text.lowercased()
1094
+ let prefs = Preferences.shared
1095
+
1096
+ if lower.contains("open settings") || lower.contains("show settings") {
1097
+ SettingsWindowController.shared.show()
1098
+ return "Opened Settings."
1099
+ }
1100
+
1101
+ if lower.contains("scan root") || lower.contains("project root") || lower.contains("project scan") {
1102
+ if let root = extractPathValue(from: text) {
1103
+ prefs.scanRoot = root
1104
+ ProjectScanner.shared.updateRoot(root)
1105
+ ProjectScanner.shared.scan()
1106
+ return "Set project scan root to \(root) and started a rescan."
1107
+ }
1108
+ return nil
1109
+ }
1110
+
1111
+ if lower.contains("terminal") {
1112
+ if let terminal = parseTerminal(from: lower) {
1113
+ guard terminal.isInstalled else {
1114
+ return "\(terminal.rawValue) is not installed, so I left the terminal set to \(prefs.terminal.rawValue)."
1115
+ }
1116
+ prefs.terminal = terminal
1117
+ return "Set terminal to \(terminal.rawValue)."
1118
+ }
1119
+ return nil
1120
+ }
1121
+
1122
+ if lower.contains("detach mode") || lower.contains("interaction mode") || lower.contains("learning mode") || lower.contains("auto mode") {
1123
+ if lower.contains("auto") {
1124
+ prefs.mode = .auto
1125
+ return "Set detach mode to Auto."
1126
+ }
1127
+ if lower.contains("learning") {
1128
+ prefs.mode = .learning
1129
+ return "Set detach mode to Learning."
1130
+ }
1131
+ return nil
1132
+ }
1133
+
1134
+ if lower.contains("drag") && lower.contains("snap") {
1135
+ if let enabled = parseBooleanIntent(from: lower) {
1136
+ prefs.dragSnapEnabled = enabled
1137
+ return "\(enabled ? "Enabled" : "Disabled") drag-to-snap."
1138
+ }
1139
+ return nil
1140
+ }
1141
+
1142
+ if lower.contains("mouse") && (lower.contains("gesture") || lower.contains("shortcut")) {
1143
+ if let enabled = parseBooleanIntent(from: lower) {
1144
+ prefs.mouseGesturesEnabled = enabled
1145
+ return "\(enabled ? "Enabled" : "Disabled") mouse gestures."
1146
+ }
1147
+ return nil
1148
+ }
1149
+
1150
+ if lower.contains("companion") && lower.contains("bridge") {
1151
+ if let enabled = parseBooleanIntent(from: lower) {
1152
+ prefs.companionBridgeEnabled = enabled
1153
+ return "\(enabled ? "Enabled" : "Disabled") the companion bridge."
1154
+ }
1155
+ return nil
1156
+ }
1157
+
1158
+ if lower.contains("companion") && lower.contains("trackpad") {
1159
+ if let enabled = parseBooleanIntent(from: lower) {
1160
+ prefs.companionTrackpadEnabled = enabled
1161
+ return "\(enabled ? "Enabled" : "Disabled") companion trackpad."
1162
+ }
1163
+ return nil
1164
+ }
1165
+
1166
+ if lower.contains("ocr") || lower.contains("screen text") || lower.contains("text recognition") {
1167
+ if lower.contains("accuracy") {
1168
+ if lower.contains("fast") {
1169
+ prefs.ocrAccuracy = "fast"
1170
+ return "Set OCR accuracy to Fast."
1171
+ }
1172
+ if lower.contains("accurate") {
1173
+ prefs.ocrAccuracy = "accurate"
1174
+ return "Set OCR accuracy to Accurate."
1175
+ }
1176
+ return nil
1177
+ }
1178
+
1179
+ if let enabled = parseBooleanIntent(from: lower) {
1180
+ OcrModel.shared.setEnabled(enabled)
1181
+ return "\(enabled ? "Enabled" : "Disabled") screen text recognition."
1182
+ }
1183
+ return nil
1184
+ }
1185
+
1186
+ if lower.contains("advisor") || lower.contains("voice advisor") {
1187
+ return nil
1188
+ }
1189
+
1190
+ if lower.contains("open assistant settings") || lower.contains("show assistant settings") {
1191
+ SettingsWindowController.shared.showAssistant()
1192
+ return "Opened Assistant settings."
1193
+ }
1194
+
1195
+ return nil
1196
+ }
1197
+
1198
+ private func providerPrompt(for userText: String) -> String {
1199
+ """
1200
+ You are the Workspace Assistant, the in-app assistant for Lattices.
1201
+
1202
+ Use the structured context as ground truth. Answer naturally and concretely. For informational questions, explain what is currently configured and what the available choices mean. For setting changes, describe what should change; the host app is responsible for applying supported changes.
1203
+
1204
+ Structured context:
1205
+ \(assistantKnowledgeBrief())
1206
+
1207
+ User request:
1208
+ \(userText)
1209
+ """
1210
+ }
1211
+
1212
+ private func voiceAdvisorPrompt(transcript: String, matched: String) -> String {
1213
+ """
1214
+ You are the same Workspace Assistant used by Lattices chat, responding through the voice command surface.
1215
+
1216
+ Use the shared structured context below as ground truth. The voice surface needs terse commentary and optional next actions, not a chatty answer.
1217
+
1218
+ Structured context:
1219
+ \(assistantKnowledgeBrief())
1220
+
1221
+ Voice transcript:
1222
+ "\(transcript)"
1223
+
1224
+ Local match already handled:
1225
+ \(matched)
1226
+
1227
+ Respond with ONLY a JSON object:
1228
+ {"commentary": "short observation or null", "suggestion": {"label": "button text", "intent": "intent_name", "slots": {"key": "value"}} or null}
1229
+
1230
+ Rules:
1231
+ - commentary: 1 sentence max. null if the matched command fully covers the request.
1232
+ - suggestion: a follow-up action. null if none needed.
1233
+ - Never suggest what was already executed.
1234
+ - Suggestions MUST include all required slots.
1235
+ - Be terse and useful.
1236
+ """
1237
+ }
1238
+
1239
+ private func voiceQuestionPrompt(transcript: String) -> String {
1240
+ """
1241
+ You are the same Workspace Assistant used by Lattices chat, responding through the voice surface.
1242
+
1243
+ This is an informational question, not necessarily a command. Use the shared structured context below, answer naturally, and include concrete current settings when relevant. Keep it short enough for voice, but do not give a clipped yes/no answer.
1244
+
1245
+ Structured context:
1246
+ \(assistantKnowledgeBrief())
1247
+
1248
+ User said:
1249
+ "\(transcript)"
1250
+ """
1251
+ }
1252
+
1253
+ private func voiceResolverPrompt(transcript: String) -> String {
1254
+ let windowList = DesktopModel.shared.windows.values
1255
+ .prefix(20)
1256
+ .map { "\($0.app): \($0.title)" }
1257
+ .joined(separator: "\n")
1258
+
1259
+ var intentList = ""
1260
+ if case .array(let intents) = PhraseMatcher.shared.catalog() {
1261
+ intentList = intents.compactMap { intent -> String? in
1262
+ guard let name = intent["intent"]?.stringValue else { return nil }
1263
+ var slotNames: [String] = []
1264
+ if case .array(let slots) = intent["slots"] {
1265
+ slotNames = slots.compactMap { $0["name"]?.stringValue }
1266
+ }
1267
+ return slotNames.isEmpty ? name : "\(name)(\(slotNames.joined(separator: ",")))"
1268
+ }.joined(separator: ", ")
1269
+ }
1270
+
1271
+ return """
1272
+ You are the same Workspace Assistant used by Lattices chat, resolving a spoken command into one executable Lattices intent.
1273
+
1274
+ Structured context:
1275
+ \(assistantKnowledgeBrief())
1276
+
1277
+ Voice transcript, possibly with transcription errors:
1278
+ "\(transcript)"
1279
+
1280
+ Available intents:
1281
+ \(intentList)
920
1282
 
921
- You're connected with \(currentProvider.name). Ask for code help, planning, debugging, or a second opinion.
1283
+ Current windows:
1284
+ \(windowList)
1285
+
1286
+ Return ONLY a JSON object like:
1287
+ {"intent":"search","slots":{"query":"dewey"},"reasoning":"user wants to find dewey windows"}
1288
+
1289
+ Rules:
1290
+ - Use intent "unknown" if the request cannot be mapped confidently.
1291
+ - Include all required slots.
1292
+ - For search, extract the key term.
1293
+ - Use app/window names from the current windows list when targeting windows.
922
1294
  """
923
1295
  }
924
1296
 
1297
+ private func assistantKnowledgeBrief() -> String {
1298
+ assistantContextJSON()
1299
+ }
1300
+
1301
+ private func assistantContextJSON() -> String {
1302
+ let payload = assistantContextPayload()
1303
+ guard JSONSerialization.isValidJSONObject(payload),
1304
+ let data = try? JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys]),
1305
+ let text = String(data: data, encoding: .utf8) else {
1306
+ return #"{"error":"context unavailable"}"#
1307
+ }
1308
+ return text
1309
+ }
1310
+
1311
+ private func assistantContextPayload() -> [String: Any] {
1312
+ let prefs = Preferences.shared
1313
+ MouseShortcutStore.shared.reloadIfNeeded()
1314
+
1315
+ return [
1316
+ "assistant": [
1317
+ "name": "Workspace Assistant",
1318
+ "selectedProvider": [
1319
+ "id": authProviderID,
1320
+ "name": currentProvider.name,
1321
+ "credential": selectedCredentialSummary,
1322
+ ],
1323
+ "providerRuntime": [
1324
+ "binary": (piBinaryPath as Any?) ?? NSNull(),
1325
+ "node": (nodeBinaryPath as Any?) ?? NSNull(),
1326
+ "authFile": authFileURL.path,
1327
+ "chatSession": sessionFileURL.path,
1328
+ ],
1329
+ ],
1330
+ "currentSettings": [
1331
+ "terminal": prefs.terminal.rawValue,
1332
+ "detachMode": prefs.mode.rawValue,
1333
+ "scanRoot": prefs.scanRoot.isEmpty ? NSNull() : prefs.scanRoot,
1334
+ "dragToSnap": prefs.dragSnapEnabled,
1335
+ "companionBridge": prefs.companionBridgeEnabled,
1336
+ "companionTrackpad": prefs.companionTrackpadEnabled,
1337
+ "ocr": [
1338
+ "enabled": prefs.ocrEnabled,
1339
+ "accuracy": prefs.ocrAccuracy,
1340
+ "quickIntervalSeconds": prefs.ocrQuickInterval,
1341
+ "deepIntervalSeconds": prefs.ocrDeepInterval,
1342
+ "quickWindowLimit": prefs.ocrQuickLimit,
1343
+ "deepWindowLimit": prefs.ocrDeepLimit,
1344
+ "deepScanBudget": prefs.ocrDeepBudget,
1345
+ ],
1346
+ "mouseShortcuts": mouseShortcutContextPayload(),
1347
+ ],
1348
+ "settingsCatalog": [
1349
+ [
1350
+ "id": "terminal",
1351
+ "type": "enum",
1352
+ "choices": Terminal.allCases.map(\.rawValue),
1353
+ "installedChoices": Terminal.installed.map(\.rawValue),
1354
+ "description": "Terminal app used when Lattices launches workspaces.",
1355
+ ],
1356
+ [
1357
+ "id": "detachMode",
1358
+ "type": "enum",
1359
+ "choices": ["learning", "auto"],
1360
+ "description": "Learning mode shows tmux hints; auto mode stays quieter.",
1361
+ ],
1362
+ [
1363
+ "id": "scanRoot",
1364
+ "type": "path",
1365
+ "description": "Root directory scanned for projects containing .lattices.json.",
1366
+ ],
1367
+ [
1368
+ "id": "dragToSnap",
1369
+ "type": "boolean",
1370
+ "description": "Enables drag-to-snap window zones.",
1371
+ ],
1372
+ [
1373
+ "id": "mouseShortcuts",
1374
+ "type": "boolean-plus-json-rules",
1375
+ "description": "Middle-click and drag gesture shortcuts controlled by mouseGestures.enabled plus ~/.lattices/mouse-shortcuts.json.",
1376
+ ],
1377
+ [
1378
+ "id": "ocr",
1379
+ "type": "object",
1380
+ "description": "Screen text recognition settings, including enablement, cadence, and accuracy.",
1381
+ ],
1382
+ [
1383
+ "id": "assistantProvider",
1384
+ "type": "enum-plus-api-key",
1385
+ "choices": ["openai", "groq", "openrouter", "minimax"],
1386
+ "description": "Provider-backed inference for chat and voice.",
1387
+ ],
1388
+ ],
1389
+ "settingsFiles": [
1390
+ "workspace": "\(NSHomeDirectory())/.lattices/workspace.json",
1391
+ "mouseShortcuts": MouseShortcutStore.shared.configURL.path,
1392
+ "snapZones": "\(NSHomeDirectory())/.lattices/snap-zones.json",
1393
+ "ocrDatabase": "\(NSHomeDirectory())/.lattices/ocr.db",
1394
+ "diagnostics": "\(NSHomeDirectory())/.lattices/lattices.log",
1395
+ ],
1396
+ "cliCommands": [
1397
+ "lattices",
1398
+ "lattices init",
1399
+ "lattices sync",
1400
+ "lattices restart [pane]",
1401
+ "lattices tile <position>",
1402
+ "lattices group [id]",
1403
+ "lattices layer [name|index]",
1404
+ "lattices windows --json",
1405
+ "lattices search <query>",
1406
+ "lattices app restart",
1407
+ ],
1408
+ "runtimeSnapshot": [
1409
+ "installedTerminals": Terminal.installed.map(\.rawValue),
1410
+ "discoveredProjectCount": ProjectScanner.shared.projects.count,
1411
+ ],
1412
+ ]
1413
+ }
1414
+
1415
+ private func mouseShortcutContextPayload() -> [String: Any] {
1416
+ let prefs = Preferences.shared
1417
+ let store = MouseShortcutStore.shared
1418
+ store.reloadIfNeeded()
1419
+
1420
+ return [
1421
+ "enabled": prefs.mouseGesturesEnabled,
1422
+ "configFile": store.configURL.path,
1423
+ "tuning": [
1424
+ "dragThresholdPx": Double(store.tuning.dragThreshold),
1425
+ "holdTolerancePx": Double(store.tuning.holdTolerance),
1426
+ "axisBias": Double(store.tuning.axisBias),
1427
+ ],
1428
+ "activeMappings": store.enabledRules.map { rule in
1429
+ [
1430
+ "id": rule.id,
1431
+ "trigger": rule.trigger.displayLabel,
1432
+ "action": rule.action.label,
1433
+ "summary": rule.summary,
1434
+ ]
1435
+ },
1436
+ ]
1437
+ }
1438
+
1439
+ private func settingsSummary() -> String {
1440
+ let prefs = Preferences.shared
1441
+ return """
1442
+ Current settings:
1443
+ Terminal: \(prefs.terminal.rawValue)
1444
+ Detach mode: \(prefs.mode.rawValue)
1445
+ Scan root: \(prefs.scanRoot.isEmpty ? "not set" : prefs.scanRoot)
1446
+ Drag-to-snap: \(prefs.dragSnapEnabled ? "on" : "off")
1447
+ Mouse gestures: \(prefs.mouseGesturesEnabled ? "on" : "off")
1448
+ Companion bridge: \(prefs.companionBridgeEnabled ? "on" : "off")
1449
+ Companion trackpad: \(prefs.companionTrackpadEnabled ? "on" : "off")
1450
+ OCR: \(prefs.ocrEnabled ? "on" : "off"), \(prefs.ocrAccuracy)
1451
+ Voice assistant: same provider as chat, \(currentProvider.name)
1452
+
1453
+ \(mouseShortcutSummary())
1454
+ """
1455
+ }
1456
+
1457
+ private func mouseShortcutSummary() -> String {
1458
+ let prefs = Preferences.shared
1459
+ let store = MouseShortcutStore.shared
1460
+ store.reloadIfNeeded()
1461
+ let mappings = store.summaryLines
1462
+ let mappingText = mappings.isEmpty
1463
+ ? "- No active mouse shortcut mappings."
1464
+ : mappings.map { "- \($0)" }.joined(separator: "\n")
1465
+
1466
+ return """
1467
+ Mouse shortcuts:
1468
+ - Middle-click shortcuts are \(prefs.mouseGesturesEnabled ? "enabled" : "disabled").
1469
+ - Config file: \(store.configURL.path)
1470
+ - Drag threshold: \(Int(store.tuning.dragThreshold)) px; hold tolerance: \(Int(store.tuning.holdTolerance)) px; axis bias: \(String(format: "%.1f", Double(store.tuning.axisBias))).
1471
+ Active mappings:
1472
+ \(mappingText)
1473
+ """
1474
+ }
1475
+
1476
+ private static func extractJSON(from text: String) -> String? {
1477
+ let cleaned = text
1478
+ .replacingOccurrences(of: "```json", with: "")
1479
+ .replacingOccurrences(of: "```", with: "")
1480
+ .trimmingCharacters(in: .whitespacesAndNewlines)
1481
+ guard let start = cleaned.firstIndex(of: "{"),
1482
+ let end = cleaned.lastIndex(of: "}") else { return nil }
1483
+ return String(cleaned[start...end])
1484
+ }
1485
+
1486
+ private func settingsHelpText() -> String {
1487
+ """
1488
+ I can manage Lattices settings from chat. Try:
1489
+ - set terminal to Ghostty
1490
+ - set scan root to ~/dev
1491
+ - turn OCR off
1492
+ - set OCR accuracy to fast
1493
+ - enable drag snap
1494
+ - disable mouse gestures
1495
+ - set detach mode to auto
1496
+ - open assistant settings
1497
+ - open settings
1498
+ """
1499
+ }
1500
+
1501
+ private func parseTerminal(from lower: String) -> Terminal? {
1502
+ let aliases: [(Terminal, [String])] = [
1503
+ (.iterm2, ["iterm2", "iterm"]),
1504
+ (.warp, ["warp"]),
1505
+ (.ghostty, ["ghostty"]),
1506
+ (.kitty, ["kitty"]),
1507
+ (.alacritty, ["alacritty"]),
1508
+ (.terminal, ["terminal.app", "apple terminal", "terminal"]),
1509
+ ]
1510
+
1511
+ return aliases.first { _, names in
1512
+ names.contains { lower.contains($0) }
1513
+ }?.0
1514
+ }
1515
+
1516
+ private func parseBooleanIntent(from lower: String) -> Bool? {
1517
+ let offTokens = ["turn off", "disable", "disabled", "off", "false", "stop"]
1518
+ if offTokens.contains(where: lower.contains) {
1519
+ return false
1520
+ }
1521
+
1522
+ let onTokens = ["turn on", "enable", "enabled", "on", "true", "start"]
1523
+ if onTokens.contains(where: lower.contains) {
1524
+ return true
1525
+ }
1526
+
1527
+ return nil
1528
+ }
1529
+
1530
+ private func extractPathValue(from text: String) -> String? {
1531
+ let markers = [" to ", " at ", " root "]
1532
+ let lower = text.lowercased()
1533
+
1534
+ for marker in markers {
1535
+ guard let range = lower.range(of: marker) else { continue }
1536
+ let raw = text[range.upperBound...]
1537
+ .trimmingCharacters(in: .whitespacesAndNewlines)
1538
+ .trimmingCharacters(in: CharacterSet(charactersIn: "\"'`"))
1539
+ guard !raw.isEmpty else { continue }
1540
+ return (raw as NSString).expandingTildeInPath
1541
+ }
1542
+
1543
+ return nil
1544
+ }
1545
+
1546
+ private func extractFirstNumber(from text: String) -> Double? {
1547
+ let pattern = #"(?<![A-Za-z0-9_])\$?([0-9]+(?:\.[0-9]+)?)"#
1548
+ guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
1549
+ let nsRange = NSRange(text.startIndex..<text.endIndex, in: text)
1550
+ guard let match = regex.firstMatch(in: text, range: nsRange),
1551
+ match.numberOfRanges > 1,
1552
+ let range = Range(match.range(at: 1), in: text) else {
1553
+ return nil
1554
+ }
1555
+ return Double(text[range])
1556
+ }
1557
+
925
1558
  private func friendlyAuthFailureMessage(for message: String) -> String? {
926
1559
  let lowercased = message.lowercased()
927
1560
  let authHints = [
@@ -937,10 +1570,10 @@ final class PiChatSession: ObservableObject {
937
1570
  guard authHints.contains(where: lowercased.contains) else { return nil }
938
1571
 
939
1572
  if currentProvider.authMode == .oauth {
940
- return "This provider is not connected yet. Use the setup panel to sign in with \(currentProvider.name), then come back and send your first prompt."
1573
+ return "This provider is not connected yet. Open Settings with the gear icon, connect \(currentProvider.name), then come back and send your first prompt."
941
1574
  }
942
1575
 
943
- return "This provider still needs an API key. Paste your \(currentProvider.tokenLabel.lowercased()) into the setup panel above, save it, and then try again."
1576
+ return "This provider still needs an API key. Open Settings with the gear icon, save your \(currentProvider.tokenLabel.lowercased()), and then try again."
944
1577
  }
945
1578
 
946
1579
  private func shouldAutoSubmitPrompt(_ prompt: PiAuthPrompt) -> Bool {