@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.
- package/LICENSE +21 -0
- package/README.md +13 -13
- package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
- package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/{app → apps/mac}/Package.swift +2 -1
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
- package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
- package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
- package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
- package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
- package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
- package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
- package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
- package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
- package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
- package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
- package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
- package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
- package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
- package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
- package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +1 -0
- package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
- package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
- package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
- package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
- package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +46 -27
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
- package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
- package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
- package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
- package/apps/mac/Sources/Core/System/Capability.swift +79 -0
- package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
- package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
- package/bin/handsoff-infer.ts +14 -5
- package/bin/handsoff-worker.ts +11 -7
- package/bin/infer.ts +406 -0
- package/bin/lattices-app.ts +57 -7
- package/bin/lattices-dev +40 -1
- package/bin/lattices.ts +1 -1
- package/docs/agent-execution-plan.md +9 -9
- package/docs/api.md +119 -0
- package/docs/app.md +1 -0
- package/docs/companion-deck.md +1 -1
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/mouse-gestures.md +79 -0
- package/docs/overview.md +2 -2
- package/docs/presentation-execution-review.md +9 -9
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/repo-structure.md +100 -0
- package/docs/voice-error-model.md +7 -7
- package/docs/voice.md +18 -0
- package/package.json +23 -13
- package/swift/Package.swift +20 -0
- package/swift/Sources/DeckKit/DeckAction.swift +51 -0
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
- package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
- package/swift/Sources/DeckKit/DeckHost.swift +7 -0
- package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
- package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
- package/swift/Sources/DeckKit/DeckValue.swift +93 -0
- package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
- package/app/Sources/AppShell/AppDelegate.swift +0 -408
- package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
- package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
- package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
- package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
- /package/{app → apps/mac}/Info.plist +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
- /package/{app → apps/mac}/Lattices.entitlements +0 -0
- /package/{app → apps/mac}/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
- /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
- /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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ? "
|
|
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
|
|
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
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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("
|
|
955
|
+
appendSystemMessage("Auth: \(instructions) If nothing opened, use OPEN AGAIN.")
|
|
789
956
|
} else {
|
|
790
|
-
appendSystemMessage("
|
|
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 = "
|
|
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
|
|
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
|
|
986
|
+
let message = json["message"] as? String ?? "Unknown auth error."
|
|
820
987
|
authErrorText = message
|
|
821
|
-
appendSystemMessage("
|
|
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
|
|
1056
|
+
Welcome to the Workspace Assistant.
|
|
890
1057
|
|
|
891
|
-
|
|
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
|
|
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
|
|
1077
|
+
Welcome to the Workspace Assistant.
|
|
911
1078
|
|
|
912
1079
|
Next step: connect \(currentProvider.name).
|
|
913
1080
|
|
|
914
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 {
|