@lattices/cli 0.4.2 → 0.4.5
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/README.md +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +90 -34
- package/app/Sources/AppShellView.swift +2 -0
- package/app/Sources/AppTypeClassifier.swift +36 -0
- package/app/Sources/AppUpdater.swift +92 -0
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CliActionLauncher.swift +50 -0
- package/app/Sources/CommandModeView.swift +4 -24
- package/app/Sources/CompanionActivityLog.swift +70 -0
- package/app/Sources/CompanionKeyboardController.swift +141 -0
- package/app/Sources/DesktopModel.swift +4 -0
- package/app/Sources/HandsOffSession.swift +15 -4
- package/app/Sources/HomeDashboardView.swift +18 -10
- package/app/Sources/HotkeyStore.swift +8 -5
- package/app/Sources/IntentEngine.swift +7 -1
- package/app/Sources/LatticesApi.swift +125 -4
- package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/LatticesDeckHost.swift +1463 -0
- package/app/Sources/LatticesRuntime.swift +61 -0
- package/app/Sources/MainView.swift +351 -191
- package/app/Sources/MouseFinder.swift +335 -30
- package/app/Sources/MouseGestureConfig.swift +364 -0
- package/app/Sources/MouseGestureController.swift +1203 -0
- package/app/Sources/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/MouseInputEventViewer.swift +272 -0
- package/app/Sources/MouseShortcutStore.swift +107 -0
- package/app/Sources/OmniSearchView.swift +136 -2
- package/app/Sources/OmniSearchWindow.swift +65 -5
- package/app/Sources/OnboardingView.swift +30 -16
- package/app/Sources/PaletteCommand.swift +26 -6
- package/app/Sources/PermissionChecker.swift +76 -2
- package/app/Sources/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/PiAuthPromptCard.swift +90 -0
- package/app/Sources/PiChatDock.swift +137 -74
- package/app/Sources/PiChatSession.swift +608 -108
- package/app/Sources/PiInstallCallout.swift +86 -0
- package/app/Sources/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/PiWorkspaceView.swift +174 -77
- package/app/Sources/Preferences.swift +78 -0
- package/app/Sources/ScreenMapState.swift +91 -31
- package/app/Sources/ScreenMapView.swift +510 -524
- package/app/Sources/ScreenMapWindowController.swift +12 -4
- package/app/Sources/SettingsView.swift +869 -152
- package/app/Sources/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/VoiceCommandWindow.swift +23 -2
- package/app/Sources/WindowDragSnapController.swift +628 -0
- package/app/Sources/WindowTiler.swift +328 -65
- package/app/Sources/WorkspaceManager.swift +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +2 -1
|
@@ -123,6 +123,7 @@ struct PiProvider: Identifiable, Equatable {
|
|
|
123
123
|
|
|
124
124
|
final class PiChatSession: ObservableObject {
|
|
125
125
|
static let shared = PiChatSession()
|
|
126
|
+
private static let installCommand = "npm install -g @mariozechner/pi-coding-agent@latest"
|
|
126
127
|
|
|
127
128
|
@Published private(set) var messages: [PiChatMessage] = [
|
|
128
129
|
PiChatMessage(
|
|
@@ -142,32 +143,50 @@ final class PiChatSession: ObservableObject {
|
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
@Published var isAuthPanelVisible: Bool = false
|
|
145
|
-
@Published var authProviderID: String = "
|
|
146
|
+
@Published var authProviderID: String = "openai-codex" {
|
|
146
147
|
didSet {
|
|
147
148
|
guard oldValue != authProviderID else { return }
|
|
149
|
+
if isAuthenticating {
|
|
150
|
+
cancelAuthFlow(silently: true)
|
|
151
|
+
}
|
|
148
152
|
UserDefaults.standard.set(authProviderID, forKey: Self.selectedProviderDefaultsKey)
|
|
149
153
|
authToken = ""
|
|
150
154
|
authPromptInput = ""
|
|
151
155
|
pendingAuthPrompt = nil
|
|
152
156
|
authNoticeText = nil
|
|
153
157
|
authErrorText = nil
|
|
158
|
+
latestAuthURL = nil
|
|
159
|
+
latestAuthInstructions = nil
|
|
160
|
+
authVerificationCodeCopied = false
|
|
161
|
+
lastCopiedAuthVerificationCode = nil
|
|
162
|
+
prepareForDisplay()
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
165
|
@Published var authToken: String = ""
|
|
157
166
|
@Published var authPromptInput: String = ""
|
|
158
167
|
@Published private(set) var isAuthenticating: Bool = false
|
|
168
|
+
@Published private(set) var authenticatingProviderID: String?
|
|
159
169
|
@Published private(set) var pendingAuthPrompt: PiAuthPrompt?
|
|
160
170
|
@Published private(set) var authNoticeText: String?
|
|
161
171
|
@Published private(set) var authErrorText: String?
|
|
162
172
|
@Published private(set) var storedCredentialKinds: [String: String] = [:]
|
|
173
|
+
@Published private(set) var piBinaryPath: String?
|
|
174
|
+
@Published private(set) var latestAuthURL: URL?
|
|
175
|
+
@Published private(set) var latestAuthInstructions: String?
|
|
176
|
+
@Published private(set) var authVerificationCodeCopied: Bool = false
|
|
163
177
|
|
|
164
178
|
private let queue = DispatchQueue(label: "pi-chat-session", qos: .userInitiated)
|
|
165
179
|
private let sessionFileURL: URL
|
|
166
180
|
private let authFileURL: URL
|
|
167
181
|
private var authProcess: Process?
|
|
182
|
+
private var authProcessIdentifier: Int32?
|
|
168
183
|
private var authInputHandle: FileHandle?
|
|
184
|
+
private var authStdoutPipe: Pipe?
|
|
185
|
+
private var authStderrPipe: Pipe?
|
|
169
186
|
private var authStdoutBuffer: String = ""
|
|
170
187
|
private var authStderrBuffer: String = ""
|
|
188
|
+
private var nodeBinaryPath: String?
|
|
189
|
+
private var lastCopiedAuthVerificationCode: String?
|
|
171
190
|
|
|
172
191
|
private static let selectedProviderDefaultsKey = "PiChatSelectedProvider"
|
|
173
192
|
private static let dockHeightDefaultsKey = "PiChatDockHeight"
|
|
@@ -191,10 +210,16 @@ final class PiChatSession: ObservableObject {
|
|
|
191
210
|
}
|
|
192
211
|
|
|
193
212
|
reloadAuthState()
|
|
213
|
+
refreshBinaryAvailability()
|
|
214
|
+
cleanupLingeringAuthHelpers()
|
|
194
215
|
}
|
|
195
216
|
|
|
196
217
|
var hasPiBinary: Bool {
|
|
197
|
-
|
|
218
|
+
piBinaryPath != nil
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
var piInstallCommand: String {
|
|
222
|
+
Self.installCommand
|
|
198
223
|
}
|
|
199
224
|
|
|
200
225
|
var providerOptions: [PiProvider] {
|
|
@@ -205,6 +230,19 @@ final class PiChatSession: ObservableObject {
|
|
|
205
230
|
PiProvider.provider(id: authProviderID)
|
|
206
231
|
}
|
|
207
232
|
|
|
233
|
+
var authenticatingProvider: PiProvider? {
|
|
234
|
+
guard let authenticatingProviderID else { return nil }
|
|
235
|
+
return PiProvider.provider(id: authenticatingProviderID)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
var needsProviderSetup: Bool {
|
|
239
|
+
hasPiBinary && !hasSelectedCredential
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
var hasConversationHistory: Bool {
|
|
243
|
+
messages.contains { $0.role != .system }
|
|
244
|
+
}
|
|
245
|
+
|
|
208
246
|
var selectedCredentialSummary: String {
|
|
209
247
|
guard let kind = storedCredentialKinds[authProviderID] else { return "not authenticated" }
|
|
210
248
|
return kind == "oauth" ? "oauth saved" : "token saved"
|
|
@@ -214,6 +252,78 @@ final class PiChatSession: ObservableObject {
|
|
|
214
252
|
storedCredentialKinds[authProviderID] != nil
|
|
215
253
|
}
|
|
216
254
|
|
|
255
|
+
var authVerificationCode: String? {
|
|
256
|
+
guard let latestAuthInstructions else { return nil }
|
|
257
|
+
let prefix = "Enter code:"
|
|
258
|
+
guard let range = latestAuthInstructions.range(of: prefix, options: [.caseInsensitive]) else { return nil }
|
|
259
|
+
let value = latestAuthInstructions[range.upperBound...]
|
|
260
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
261
|
+
return value.isEmpty ? nil : value
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
var authStepLabel: String {
|
|
265
|
+
if pendingAuthPrompt != nil || latestAuthURL == nil {
|
|
266
|
+
return "STEP 1"
|
|
267
|
+
}
|
|
268
|
+
return "STEP 2"
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var authStepTitle: String {
|
|
272
|
+
if pendingAuthPrompt != nil {
|
|
273
|
+
return "Answer one quick question"
|
|
274
|
+
}
|
|
275
|
+
if latestAuthURL == nil {
|
|
276
|
+
return "Opening your sign-in page"
|
|
277
|
+
}
|
|
278
|
+
if authVerificationCode != nil {
|
|
279
|
+
return authVerificationCodeCopied
|
|
280
|
+
? "Paste the copied code in your browser"
|
|
281
|
+
: "Copy the code, then paste it in your browser"
|
|
282
|
+
}
|
|
283
|
+
return "Finish sign-in in your browser"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
var authStepDescription: String {
|
|
287
|
+
if let prompt = pendingAuthPrompt {
|
|
288
|
+
return prompt.message
|
|
289
|
+
}
|
|
290
|
+
if latestAuthURL == nil {
|
|
291
|
+
return "Stay here for a second while Pi prepares the browser step."
|
|
292
|
+
}
|
|
293
|
+
if authVerificationCode != nil {
|
|
294
|
+
return authVerificationCodeCopied
|
|
295
|
+
? "The code is already on your clipboard. Switch to the browser page and paste it."
|
|
296
|
+
: "Use the code below on the browser page, or copy it here first."
|
|
297
|
+
}
|
|
298
|
+
return "Your browser sign-in page is ready. Finish the provider flow there."
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
var authStepShortText: String {
|
|
302
|
+
if pendingAuthPrompt != nil {
|
|
303
|
+
return "Answer one quick question"
|
|
304
|
+
}
|
|
305
|
+
if latestAuthURL == nil {
|
|
306
|
+
return "Opening browser sign-in"
|
|
307
|
+
}
|
|
308
|
+
if authVerificationCode != nil {
|
|
309
|
+
return authVerificationCodeCopied ? "Paste the copied code" : "Copy the code and paste it"
|
|
310
|
+
}
|
|
311
|
+
return "Finish sign-in in browser"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
var setupStatusSummary: String {
|
|
315
|
+
if !hasPiBinary {
|
|
316
|
+
return "Install Pi to enable the assistant"
|
|
317
|
+
}
|
|
318
|
+
if isAuthenticating {
|
|
319
|
+
return authStepShortText
|
|
320
|
+
}
|
|
321
|
+
if needsProviderSetup {
|
|
322
|
+
return "Next: connect \(currentProvider.name)"
|
|
323
|
+
}
|
|
324
|
+
return currentProvider.name
|
|
325
|
+
}
|
|
326
|
+
|
|
217
327
|
var canSubmitAuthPrompt: Bool {
|
|
218
328
|
guard let prompt = pendingAuthPrompt else { return false }
|
|
219
329
|
let value = authPromptInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
@@ -225,6 +335,11 @@ final class PiChatSession: ObservableObject {
|
|
|
225
335
|
}
|
|
226
336
|
|
|
227
337
|
func toggleAuthPanel() {
|
|
338
|
+
if needsProviderSetup || isAuthenticating {
|
|
339
|
+
isAuthPanelVisible = true
|
|
340
|
+
dockHeight = max(dockHeight, 300)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
228
343
|
isAuthPanelVisible.toggle()
|
|
229
344
|
if isAuthPanelVisible {
|
|
230
345
|
dockHeight = max(dockHeight, 300)
|
|
@@ -233,14 +348,53 @@ final class PiChatSession: ObservableObject {
|
|
|
233
348
|
|
|
234
349
|
func clearConversation() {
|
|
235
350
|
try? FileManager.default.removeItem(at: sessionFileURL)
|
|
236
|
-
messages = [
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
351
|
+
messages = []
|
|
352
|
+
prepareForDisplay()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
func prepareForDisplay() {
|
|
356
|
+
reconcileAuthState()
|
|
357
|
+
refreshBinaryAvailability()
|
|
358
|
+
|
|
359
|
+
if isAuthenticating {
|
|
360
|
+
isAuthPanelVisible = true
|
|
361
|
+
statusText = "connecting..."
|
|
362
|
+
} else if needsProviderSetup {
|
|
363
|
+
isAuthPanelVisible = true
|
|
364
|
+
statusText = "setup ai"
|
|
365
|
+
} else if hasPiBinary && (statusText == "setup ai" || statusText == "missing pi") {
|
|
366
|
+
statusText = "idle"
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
syncStructuredWelcomeMessage()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
func refreshBinaryAvailability() {
|
|
373
|
+
piBinaryPath = resolvePiPath()
|
|
374
|
+
nodeBinaryPath = resolveNodePath()
|
|
375
|
+
|
|
376
|
+
if piBinaryPath == nil {
|
|
377
|
+
if statusText == "idle" || statusText == "missing pi" {
|
|
378
|
+
statusText = "missing pi"
|
|
379
|
+
}
|
|
380
|
+
} else if !hasSelectedCredential {
|
|
381
|
+
if statusText == "idle" || statusText == "setup ai" {
|
|
382
|
+
statusText = "setup ai"
|
|
383
|
+
}
|
|
384
|
+
} else if statusText == "missing pi" {
|
|
385
|
+
statusText = "idle"
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
func copyPiInstallCommand() {
|
|
390
|
+
NSPasteboard.general.clearContents()
|
|
391
|
+
NSPasteboard.general.setString(piInstallCommand, forType: .string)
|
|
392
|
+
appendSystemMessage("Copied the Pi install command to the clipboard.")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
func installPiInTerminal() {
|
|
396
|
+
Preferences.shared.terminal.launch(command: piInstallCommand, in: NSHomeDirectory())
|
|
397
|
+
appendSystemMessage("Opened \(Preferences.shared.terminal.rawValue) and started the Pi install.")
|
|
244
398
|
}
|
|
245
399
|
|
|
246
400
|
func sendDraft() {
|
|
@@ -255,14 +409,21 @@ final class PiChatSession: ObservableObject {
|
|
|
255
409
|
guard !trimmed.isEmpty else { return }
|
|
256
410
|
guard !isSending else { return }
|
|
257
411
|
|
|
258
|
-
|
|
412
|
+
refreshBinaryAvailability()
|
|
259
413
|
|
|
260
|
-
guard let piPath =
|
|
261
|
-
|
|
414
|
+
guard let piPath = piBinaryPath else {
|
|
415
|
+
prepareForDisplay()
|
|
262
416
|
statusText = "missing pi"
|
|
263
417
|
return
|
|
264
418
|
}
|
|
265
419
|
|
|
420
|
+
guard !needsProviderSetup else {
|
|
421
|
+
prepareForDisplay()
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
messages.append(PiChatMessage(role: .user, text: trimmed, timestamp: Date()))
|
|
426
|
+
|
|
266
427
|
let provider = currentProvider
|
|
267
428
|
isSending = true
|
|
268
429
|
statusText = "thinking..."
|
|
@@ -327,6 +488,13 @@ final class PiChatSession: ObservableObject {
|
|
|
327
488
|
}
|
|
328
489
|
|
|
329
490
|
let message = !stderr.isEmpty ? stderr : (stdout.isEmpty ? "Pi returned no output." : stdout)
|
|
491
|
+
if let friendly = self.friendlyAuthFailureMessage(for: message) {
|
|
492
|
+
self.statusText = "setup ai"
|
|
493
|
+
self.authErrorText = friendly
|
|
494
|
+
self.isAuthPanelVisible = true
|
|
495
|
+
self.syncStructuredWelcomeMessage()
|
|
496
|
+
return
|
|
497
|
+
}
|
|
330
498
|
self.statusText = "error"
|
|
331
499
|
self.appendSystemMessage(message)
|
|
332
500
|
if Self.looksLikeAuthError(message) {
|
|
@@ -355,6 +523,8 @@ final class PiChatSession: ObservableObject {
|
|
|
355
523
|
authErrorText = nil
|
|
356
524
|
reloadAuthState()
|
|
357
525
|
appendSystemMessage("Saved \(currentProvider.name) credentials to Pi auth storage.")
|
|
526
|
+
isAuthPanelVisible = false
|
|
527
|
+
prepareForDisplay()
|
|
358
528
|
} catch {
|
|
359
529
|
authErrorText = "Failed to save token: \(error.localizedDescription)"
|
|
360
530
|
}
|
|
@@ -369,6 +539,7 @@ final class PiChatSession: ObservableObject {
|
|
|
369
539
|
authErrorText = nil
|
|
370
540
|
reloadAuthState()
|
|
371
541
|
appendSystemMessage("Removed saved \(currentProvider.name) credentials from Pi auth storage.")
|
|
542
|
+
prepareForDisplay()
|
|
372
543
|
} catch {
|
|
373
544
|
authErrorText = "Failed to remove credentials: \(error.localizedDescription)"
|
|
374
545
|
}
|
|
@@ -387,7 +558,10 @@ final class PiChatSession: ObservableObject {
|
|
|
387
558
|
guard let prompt = pendingAuthPrompt else { return }
|
|
388
559
|
let value = authPromptInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
389
560
|
guard prompt.allowEmpty || !value.isEmpty else { return }
|
|
561
|
+
submitAuthPromptValue(value)
|
|
562
|
+
}
|
|
390
563
|
|
|
564
|
+
private func submitAuthPromptValue(_ value: String) {
|
|
391
565
|
guard let handle = authInputHandle else {
|
|
392
566
|
authErrorText = "Pi auth input pipe is no longer available."
|
|
393
567
|
return
|
|
@@ -405,20 +579,71 @@ final class PiChatSession: ObservableObject {
|
|
|
405
579
|
}
|
|
406
580
|
}
|
|
407
581
|
|
|
408
|
-
func
|
|
409
|
-
|
|
582
|
+
func reopenLatestAuthURL() {
|
|
583
|
+
guard let latestAuthURL else {
|
|
584
|
+
authNoticeText = "Still preparing the browser sign-in link..."
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
autoCopyAuthVerificationCodeIfNeeded()
|
|
589
|
+
NSWorkspace.shared.open(latestAuthURL)
|
|
590
|
+
authNoticeText = authVerificationCode != nil
|
|
591
|
+
? "Reopened the sign-in page. Paste the copied code there."
|
|
592
|
+
: "Reopened \(authenticatingProvider?.name ?? currentProvider.name) sign-in in your browser."
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
func copyAuthVerificationCode() {
|
|
596
|
+
copyAuthVerificationCode(silently: false)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private func copyAuthVerificationCode(silently: Bool) {
|
|
600
|
+
guard let authVerificationCode else {
|
|
601
|
+
authNoticeText = "No sign-in code is ready yet."
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
NSPasteboard.general.clearContents()
|
|
606
|
+
NSPasteboard.general.setString(authVerificationCode, forType: .string)
|
|
607
|
+
authVerificationCodeCopied = true
|
|
608
|
+
lastCopiedAuthVerificationCode = authVerificationCode
|
|
609
|
+
if !silently {
|
|
610
|
+
authNoticeText = "Copied the sign-in code. Paste it into the browser page."
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private func autoCopyAuthVerificationCodeIfNeeded() {
|
|
615
|
+
guard let authVerificationCode else { return }
|
|
616
|
+
guard !authVerificationCodeCopied || lastCopiedAuthVerificationCode != authVerificationCode else { return }
|
|
617
|
+
copyAuthVerificationCode(silently: true)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
func cancelAuthFlow(silently: Bool = false) {
|
|
621
|
+
let process = authProcess
|
|
410
622
|
cleanupAuthProcess()
|
|
623
|
+
terminateProcess(process, escalateAfter: 0.8)
|
|
411
624
|
isAuthenticating = false
|
|
412
|
-
|
|
625
|
+
statusText = hasPiBinary && !hasSelectedCredential ? "setup ai" : "idle"
|
|
626
|
+
if !silently {
|
|
627
|
+
authNoticeText = "Cancelled auth flow."
|
|
628
|
+
}
|
|
413
629
|
}
|
|
414
630
|
|
|
415
631
|
private func startOAuthLogin(for provider: PiProvider) {
|
|
416
|
-
|
|
417
|
-
|
|
632
|
+
reconcileAuthState()
|
|
633
|
+
cleanupLingeringAuthHelpers()
|
|
634
|
+
|
|
635
|
+
if isAuthenticating {
|
|
636
|
+
cancelAuthFlow(silently: true)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
refreshBinaryAvailability()
|
|
640
|
+
|
|
641
|
+
guard hasPiBinary else {
|
|
642
|
+
authErrorText = "Install Pi before starting auth."
|
|
418
643
|
return
|
|
419
644
|
}
|
|
420
645
|
|
|
421
|
-
guard let nodePath =
|
|
646
|
+
guard let nodePath = nodeBinaryPath else {
|
|
422
647
|
authErrorText = "Node.js is required for Pi OAuth login."
|
|
423
648
|
return
|
|
424
649
|
}
|
|
@@ -449,9 +674,15 @@ final class PiChatSession: ObservableObject {
|
|
|
449
674
|
authStderrBuffer = ""
|
|
450
675
|
authPromptInput = ""
|
|
451
676
|
pendingAuthPrompt = nil
|
|
452
|
-
|
|
677
|
+
latestAuthURL = nil
|
|
678
|
+
latestAuthInstructions = nil
|
|
679
|
+
authVerificationCodeCopied = false
|
|
680
|
+
lastCopiedAuthVerificationCode = nil
|
|
681
|
+
authNoticeText = "Preparing \(provider.name) sign-in..."
|
|
453
682
|
authErrorText = nil
|
|
454
683
|
isAuthenticating = true
|
|
684
|
+
authenticatingProviderID = provider.id
|
|
685
|
+
statusText = "connecting..."
|
|
455
686
|
|
|
456
687
|
stdoutPipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
|
|
457
688
|
let data = handle.availableData
|
|
@@ -467,18 +698,23 @@ final class PiChatSession: ObservableObject {
|
|
|
467
698
|
|
|
468
699
|
proc.terminationHandler = { [weak self] process in
|
|
469
700
|
DispatchQueue.main.async {
|
|
470
|
-
self?.handleAuthProcessExit(status: process.terminationStatus)
|
|
701
|
+
self?.handleAuthProcessExit(processID: process.processIdentifier, status: process.terminationStatus)
|
|
471
702
|
}
|
|
472
703
|
}
|
|
473
704
|
|
|
474
705
|
do {
|
|
475
706
|
try proc.run()
|
|
476
707
|
authProcess = proc
|
|
708
|
+
authProcessIdentifier = proc.processIdentifier
|
|
477
709
|
authInputHandle = stdinPipe.fileHandleForWriting
|
|
710
|
+
authStdoutPipe = stdoutPipe
|
|
711
|
+
authStderrPipe = stderrPipe
|
|
712
|
+
recordAuthHelperProcess(proc.processIdentifier)
|
|
478
713
|
appendSystemMessage("Started \(provider.name) auth flow.")
|
|
479
714
|
} catch {
|
|
480
715
|
cleanupAuthProcess()
|
|
481
716
|
isAuthenticating = false
|
|
717
|
+
statusText = hasPiBinary && !hasSelectedCredential ? "setup ai" : "idle"
|
|
482
718
|
authErrorText = "Failed to launch auth flow: \(error.localizedDescription)"
|
|
483
719
|
}
|
|
484
720
|
}
|
|
@@ -519,22 +755,39 @@ final class PiChatSession: ObservableObject {
|
|
|
519
755
|
|
|
520
756
|
switch type {
|
|
521
757
|
case "prompt":
|
|
522
|
-
|
|
758
|
+
let prompt = PiAuthPrompt(
|
|
523
759
|
message: json["message"] as? String ?? "Continue",
|
|
524
760
|
placeholder: json["placeholder"] as? String,
|
|
525
761
|
allowEmpty: json["allowEmpty"] as? Bool ?? false
|
|
526
762
|
)
|
|
527
|
-
|
|
763
|
+
pendingAuthPrompt = prompt
|
|
764
|
+
authNoticeText = prompt.message
|
|
765
|
+
if shouldAutoSubmitPrompt(prompt) {
|
|
766
|
+
authNoticeText = "Using github.com. If you need GitHub Enterprise, cancel and enter your domain instead."
|
|
767
|
+
submitAuthPromptValue("")
|
|
768
|
+
}
|
|
528
769
|
|
|
529
770
|
case "auth":
|
|
530
771
|
let urlString = json["url"] as? String ?? ""
|
|
531
772
|
let instructions = json["instructions"] as? String
|
|
532
|
-
|
|
533
|
-
|
|
773
|
+
latestAuthURL = URL(string: urlString)
|
|
774
|
+
latestAuthInstructions = instructions
|
|
775
|
+
if authVerificationCode != lastCopiedAuthVerificationCode {
|
|
776
|
+
authVerificationCodeCopied = false
|
|
777
|
+
}
|
|
778
|
+
autoCopyAuthVerificationCodeIfNeeded()
|
|
779
|
+
authNoticeText = authVerificationCode != nil
|
|
780
|
+
? "The sign-in code is copied. Paste it into the browser page."
|
|
781
|
+
: "Your browser sign-in page is ready."
|
|
782
|
+
if let url = latestAuthURL {
|
|
534
783
|
NSWorkspace.shared.open(url)
|
|
535
784
|
}
|
|
536
|
-
if
|
|
537
|
-
appendSystemMessage("Pi auth
|
|
785
|
+
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.")
|
|
787
|
+
} else if let instructions, !instructions.isEmpty {
|
|
788
|
+
appendSystemMessage("Pi auth: \(instructions) If nothing opened, use OPEN AGAIN.")
|
|
789
|
+
} else {
|
|
790
|
+
appendSystemMessage("Pi auth is ready in your browser. If nothing opened, use OPEN AGAIN.")
|
|
538
791
|
}
|
|
539
792
|
|
|
540
793
|
case "progress":
|
|
@@ -545,15 +798,19 @@ final class PiChatSession: ObservableObject {
|
|
|
545
798
|
authErrorText = "Pi auth completed but returned no credentials."
|
|
546
799
|
return
|
|
547
800
|
}
|
|
801
|
+
let providerID = authenticatingProviderID ?? authProviderID
|
|
802
|
+
let provider = PiProvider.provider(id: providerID)
|
|
548
803
|
credentials["type"] = "oauth"
|
|
549
804
|
do {
|
|
550
805
|
try mutateAuthFile { auth in
|
|
551
|
-
auth[
|
|
806
|
+
auth[providerID] = credentials
|
|
552
807
|
}
|
|
553
808
|
reloadAuthState()
|
|
554
|
-
authNoticeText = "Saved OAuth credentials for \(
|
|
809
|
+
authNoticeText = "Saved OAuth credentials for \(provider.name)."
|
|
555
810
|
authErrorText = nil
|
|
556
|
-
appendSystemMessage("Saved \(
|
|
811
|
+
appendSystemMessage("Saved \(provider.name) OAuth credentials to Pi auth storage.")
|
|
812
|
+
isAuthPanelVisible = false
|
|
813
|
+
prepareForDisplay()
|
|
557
814
|
} catch {
|
|
558
815
|
authErrorText = "Failed to save OAuth credentials: \(error.localizedDescription)"
|
|
559
816
|
}
|
|
@@ -568,7 +825,9 @@ final class PiChatSession: ObservableObject {
|
|
|
568
825
|
}
|
|
569
826
|
}
|
|
570
827
|
|
|
571
|
-
private func handleAuthProcessExit(status: Int32) {
|
|
828
|
+
private func handleAuthProcessExit(processID: Int32, status: Int32) {
|
|
829
|
+
guard authProcessIdentifier == processID else { return }
|
|
830
|
+
|
|
572
831
|
let hadExplicitError = authErrorText != nil
|
|
573
832
|
cleanupAuthProcess()
|
|
574
833
|
isAuthenticating = false
|
|
@@ -581,24 +840,119 @@ final class PiChatSession: ObservableObject {
|
|
|
581
840
|
} else if !hadExplicitError {
|
|
582
841
|
authErrorText = "Auth flow exited with status \(status)."
|
|
583
842
|
}
|
|
843
|
+
|
|
844
|
+
if status == 0, hasSelectedCredential {
|
|
845
|
+
statusText = "idle"
|
|
846
|
+
} else if hasPiBinary && !hasSelectedCredential {
|
|
847
|
+
statusText = "setup ai"
|
|
848
|
+
}
|
|
584
849
|
}
|
|
585
850
|
|
|
586
851
|
private func cleanupAuthProcess() {
|
|
587
|
-
authProcess?.
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if let error = authProcess?.standardError as? Pipe {
|
|
592
|
-
error.fileHandleForReading.readabilityHandler = nil
|
|
593
|
-
}
|
|
852
|
+
authProcess?.terminationHandler = nil
|
|
853
|
+
authStdoutPipe?.fileHandleForReading.readabilityHandler = nil
|
|
854
|
+
authStderrPipe?.fileHandleForReading.readabilityHandler = nil
|
|
855
|
+
try? authInputHandle?.close()
|
|
594
856
|
authInputHandle = nil
|
|
857
|
+
authStdoutPipe = nil
|
|
858
|
+
authStderrPipe = nil
|
|
859
|
+
authStdoutBuffer = ""
|
|
860
|
+
authStderrBuffer = ""
|
|
861
|
+
latestAuthURL = nil
|
|
862
|
+
latestAuthInstructions = nil
|
|
863
|
+
authVerificationCodeCopied = false
|
|
864
|
+
lastCopiedAuthVerificationCode = nil
|
|
595
865
|
authProcess = nil
|
|
866
|
+
authProcessIdentifier = nil
|
|
867
|
+
authenticatingProviderID = nil
|
|
868
|
+
clearRecordedAuthHelperProcess()
|
|
596
869
|
}
|
|
597
870
|
|
|
598
871
|
private func appendSystemMessage(_ text: String) {
|
|
599
872
|
messages.append(PiChatMessage(role: .system, text: text, timestamp: Date()))
|
|
600
873
|
}
|
|
601
874
|
|
|
875
|
+
private func syncStructuredWelcomeMessage() {
|
|
876
|
+
guard !hasConversationHistory else { return }
|
|
877
|
+
messages = [
|
|
878
|
+
PiChatMessage(
|
|
879
|
+
role: .system,
|
|
880
|
+
text: structuredWelcomeMessage(),
|
|
881
|
+
timestamp: Date()
|
|
882
|
+
)
|
|
883
|
+
]
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
private func structuredWelcomeMessage() -> String {
|
|
887
|
+
if !hasPiBinary {
|
|
888
|
+
return """
|
|
889
|
+
Welcome to Pi Workspace.
|
|
890
|
+
|
|
891
|
+
Pi powers the in-app assistant. Install it first, then come back here and refresh.
|
|
892
|
+
|
|
893
|
+
Install command:
|
|
894
|
+
\(piInstallCommand)
|
|
895
|
+
"""
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if isAuthenticating {
|
|
899
|
+
return """
|
|
900
|
+
Welcome to Pi Workspace.
|
|
901
|
+
|
|
902
|
+
\(authStepTitle)
|
|
903
|
+
|
|
904
|
+
\(authStepDescription)
|
|
905
|
+
"""
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if needsProviderSetup {
|
|
909
|
+
return """
|
|
910
|
+
Welcome to Pi Workspace.
|
|
911
|
+
|
|
912
|
+
Next step: connect \(currentProvider.name).
|
|
913
|
+
|
|
914
|
+
The setup panel above is open. Once you finish that one step, the chat box unlocks automatically.
|
|
915
|
+
"""
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return """
|
|
919
|
+
Welcome to Pi Workspace.
|
|
920
|
+
|
|
921
|
+
You're connected with \(currentProvider.name). Ask for code help, planning, debugging, or a second opinion.
|
|
922
|
+
"""
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private func friendlyAuthFailureMessage(for message: String) -> String? {
|
|
926
|
+
let lowercased = message.lowercased()
|
|
927
|
+
let authHints = [
|
|
928
|
+
"use /login",
|
|
929
|
+
"set an api key environment variable",
|
|
930
|
+
"authentication",
|
|
931
|
+
"unauthorized",
|
|
932
|
+
"api key",
|
|
933
|
+
"oauth",
|
|
934
|
+
"token",
|
|
935
|
+
]
|
|
936
|
+
|
|
937
|
+
guard authHints.contains(where: lowercased.contains) else { return nil }
|
|
938
|
+
|
|
939
|
+
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."
|
|
941
|
+
}
|
|
942
|
+
|
|
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."
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
private func shouldAutoSubmitPrompt(_ prompt: PiAuthPrompt) -> Bool {
|
|
947
|
+
guard authenticatingProviderID == "github-copilot" else { return false }
|
|
948
|
+
guard prompt.allowEmpty else { return false }
|
|
949
|
+
|
|
950
|
+
let message = prompt.message.lowercased()
|
|
951
|
+
return message.contains("github enterprise url")
|
|
952
|
+
|| message.contains("github enterprise")
|
|
953
|
+
|| message.contains("blank for github.com")
|
|
954
|
+
}
|
|
955
|
+
|
|
602
956
|
private func reloadAuthState() {
|
|
603
957
|
let auth = loadAuthFile()
|
|
604
958
|
var kinds: [String: String] = [:]
|
|
@@ -612,6 +966,21 @@ final class PiChatSession: ObservableObject {
|
|
|
612
966
|
storedCredentialKinds = kinds
|
|
613
967
|
}
|
|
614
968
|
|
|
969
|
+
private func reconcileAuthState() {
|
|
970
|
+
guard isAuthenticating else { return }
|
|
971
|
+
|
|
972
|
+
if let authProcess, authProcess.isRunning {
|
|
973
|
+
return
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
cleanupAuthProcess()
|
|
977
|
+
isAuthenticating = false
|
|
978
|
+
pendingAuthPrompt = nil
|
|
979
|
+
if hasPiBinary && !hasSelectedCredential {
|
|
980
|
+
statusText = "setup ai"
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
615
984
|
private func loadAuthFile() -> [String: Any] {
|
|
616
985
|
guard let data = try? Data(contentsOf: authFileURL), !data.isEmpty else { return [:] }
|
|
617
986
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] }
|
|
@@ -639,6 +1008,7 @@ final class PiChatSession: ObservableObject {
|
|
|
639
1008
|
"/opt/homebrew/bin/pi",
|
|
640
1009
|
"/usr/local/bin/pi",
|
|
641
1010
|
NSHomeDirectory() + "/.local/bin/pi",
|
|
1011
|
+
NSHomeDirectory() + "/.bun/bin/pi",
|
|
642
1012
|
]
|
|
643
1013
|
)
|
|
644
1014
|
}
|
|
@@ -650,26 +1020,78 @@ final class PiChatSession: ObservableObject {
|
|
|
650
1020
|
"/opt/homebrew/bin/node",
|
|
651
1021
|
"/usr/local/bin/node",
|
|
652
1022
|
"/usr/bin/node",
|
|
1023
|
+
NSHomeDirectory() + "/.local/bin/node",
|
|
653
1024
|
]
|
|
654
1025
|
)
|
|
655
1026
|
}
|
|
656
1027
|
|
|
657
1028
|
private func resolveCommandPath(named command: String, candidates: [String]) -> String? {
|
|
658
|
-
|
|
1029
|
+
var orderedCandidates: [String] = []
|
|
1030
|
+
var seen: Set<String> = []
|
|
1031
|
+
|
|
1032
|
+
for rawPath in candidates + managedInstallCandidates(for: command) {
|
|
1033
|
+
let path = (rawPath as NSString).expandingTildeInPath
|
|
1034
|
+
guard !path.isEmpty else { continue }
|
|
1035
|
+
guard seen.insert(path).inserted else { continue }
|
|
1036
|
+
orderedCandidates.append(path)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
for path in orderedCandidates where FileManager.default.isExecutableFile(atPath: path) {
|
|
659
1040
|
return path
|
|
660
1041
|
}
|
|
661
1042
|
|
|
662
|
-
let
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1043
|
+
let lookups = [
|
|
1044
|
+
ProcessQuery.shell(["/usr/bin/which", command]),
|
|
1045
|
+
ProcessQuery.shell(["/bin/sh", "-lc", "command -v \(command) 2>/dev/null"]),
|
|
1046
|
+
ProcessQuery.shell(["/bin/zsh", "-lc", "command -v \(command) 2>/dev/null"]),
|
|
1047
|
+
]
|
|
1048
|
+
|
|
1049
|
+
for output in lookups {
|
|
1050
|
+
let path = output.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1051
|
+
if !path.isEmpty, FileManager.default.isExecutableFile(atPath: path) {
|
|
1052
|
+
return path
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return nil
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
private func managedInstallCandidates(for command: String) -> [String] {
|
|
1060
|
+
let home = NSHomeDirectory()
|
|
1061
|
+
var candidates = [
|
|
1062
|
+
"\(home)/.bun/bin/\(command)",
|
|
1063
|
+
"\(home)/.npm-global/bin/\(command)",
|
|
1064
|
+
"\(home)/Library/pnpm/\(command)",
|
|
1065
|
+
"\(home)/.local/share/mise/shims/\(command)",
|
|
1066
|
+
]
|
|
1067
|
+
|
|
1068
|
+
let fnmRoot = URL(fileURLWithPath: home, isDirectory: true)
|
|
1069
|
+
.appendingPathComponent(".local", isDirectory: true)
|
|
1070
|
+
.appendingPathComponent("share", isDirectory: true)
|
|
1071
|
+
.appendingPathComponent("fnm", isDirectory: true)
|
|
1072
|
+
.appendingPathComponent("node-versions", isDirectory: true)
|
|
1073
|
+
|
|
1074
|
+
if let installs = try? FileManager.default.contentsOfDirectory(
|
|
1075
|
+
at: fnmRoot,
|
|
1076
|
+
includingPropertiesForKeys: nil,
|
|
1077
|
+
options: [.skipsHiddenFiles]
|
|
1078
|
+
) {
|
|
1079
|
+
let sortedInstalls = installs.sorted {
|
|
1080
|
+
$0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedDescending
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
for install in sortedInstalls {
|
|
1084
|
+
candidates.append(
|
|
1085
|
+
install
|
|
1086
|
+
.appendingPathComponent("installation", isDirectory: true)
|
|
1087
|
+
.appendingPathComponent("bin", isDirectory: true)
|
|
1088
|
+
.appendingPathComponent(command)
|
|
1089
|
+
.path
|
|
1090
|
+
)
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return candidates
|
|
673
1095
|
}
|
|
674
1096
|
|
|
675
1097
|
private func resolveOAuthModuleURL() -> URL? {
|
|
@@ -693,6 +1115,74 @@ final class PiChatSession: ObservableObject {
|
|
|
693
1115
|
return resolved.deletingLastPathComponent().deletingLastPathComponent()
|
|
694
1116
|
}
|
|
695
1117
|
|
|
1118
|
+
private func recordAuthHelperProcess(_ pid: Int32) {
|
|
1119
|
+
let payload: [String: Any] = [
|
|
1120
|
+
"pid": Int(pid),
|
|
1121
|
+
"recordedAt": Date().timeIntervalSince1970,
|
|
1122
|
+
]
|
|
1123
|
+
guard let data = try? JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys]) else {
|
|
1124
|
+
return
|
|
1125
|
+
}
|
|
1126
|
+
try? data.write(to: authRuntimeURL, options: .atomic)
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
private func clearRecordedAuthHelperProcess() {
|
|
1130
|
+
try? FileManager.default.removeItem(at: authRuntimeURL)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
private func cleanupLingeringAuthHelpers() {
|
|
1134
|
+
let fm = FileManager.default
|
|
1135
|
+
|
|
1136
|
+
if let data = try? Data(contentsOf: authRuntimeURL),
|
|
1137
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
1138
|
+
let pid = json["pid"] as? Int {
|
|
1139
|
+
terminateRecordedAuthHelper(pid)
|
|
1140
|
+
try? fm.removeItem(at: authRuntimeURL)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
let currentPID = Int(ProcessInfo.processInfo.processIdentifier)
|
|
1144
|
+
for entry in ProcessQuery.snapshot().values where Self.looksLikePiOAuthHelper(entry.args) {
|
|
1145
|
+
guard entry.pid != currentPID else { continue }
|
|
1146
|
+
guard authProcess?.processIdentifier != Int32(entry.pid) else { continue }
|
|
1147
|
+
terminateRecordedAuthHelper(entry.pid)
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
private func terminateRecordedAuthHelper(_ pid: Int) {
|
|
1152
|
+
guard pid > 1 else { return }
|
|
1153
|
+
guard kill(Int32(pid), 0) == 0 else { return }
|
|
1154
|
+
|
|
1155
|
+
let args = ProcessQuery.shell(["/bin/ps", "-p", "\(pid)", "-o", "args="])
|
|
1156
|
+
guard Self.looksLikePiOAuthHelper(args) else { return }
|
|
1157
|
+
|
|
1158
|
+
_ = kill(Int32(pid), SIGTERM)
|
|
1159
|
+
let deadline = Date().addingTimeInterval(1.0)
|
|
1160
|
+
while Date() < deadline {
|
|
1161
|
+
if kill(Int32(pid), 0) != 0 {
|
|
1162
|
+
return
|
|
1163
|
+
}
|
|
1164
|
+
usleep(100_000)
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
_ = kill(Int32(pid), SIGKILL)
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private func terminateProcess(_ process: Process?, escalateAfter delay: TimeInterval) {
|
|
1171
|
+
guard let process else { return }
|
|
1172
|
+
let pid = process.processIdentifier
|
|
1173
|
+
process.terminate()
|
|
1174
|
+
|
|
1175
|
+
let deadline = Date().addingTimeInterval(delay)
|
|
1176
|
+
while Date() < deadline {
|
|
1177
|
+
if kill(pid, 0) != 0 {
|
|
1178
|
+
return
|
|
1179
|
+
}
|
|
1180
|
+
usleep(100_000)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
_ = kill(pid, SIGKILL)
|
|
1184
|
+
}
|
|
1185
|
+
|
|
696
1186
|
private static func piAgentDirURL() -> URL {
|
|
697
1187
|
if let override = ProcessInfo.processInfo.environment["PI_CODING_AGENT_DIR"],
|
|
698
1188
|
!override.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
@@ -714,6 +1204,12 @@ final class PiChatSession: ObservableObject {
|
|
|
714
1204
|
|| lowercased.contains("bad request")
|
|
715
1205
|
}
|
|
716
1206
|
|
|
1207
|
+
private static func looksLikePiOAuthHelper(_ args: String) -> Bool {
|
|
1208
|
+
args.contains("node:readline")
|
|
1209
|
+
&& args.contains("getOAuthProvider")
|
|
1210
|
+
&& args.contains("oauthModuleUrl")
|
|
1211
|
+
}
|
|
1212
|
+
|
|
717
1213
|
private static func clampDockHeight(_ height: CGFloat) -> CGFloat {
|
|
718
1214
|
min(max(height, 170), 520)
|
|
719
1215
|
}
|
|
@@ -746,70 +1242,74 @@ final class PiChatSession: ObservableObject {
|
|
|
746
1242
|
}
|
|
747
1243
|
|
|
748
1244
|
private static let oauthDriverScript = #"""
|
|
749
|
-
import readline from 'node:readline';
|
|
1245
|
+
import readline from 'node:readline';
|
|
750
1246
|
|
|
751
|
-
const providerId = process.argv[1];
|
|
752
|
-
const oauthModuleUrl = process.argv[2];
|
|
753
|
-
const { getOAuthProvider } = await import(oauthModuleUrl);
|
|
1247
|
+
const providerId = process.argv[1];
|
|
1248
|
+
const oauthModuleUrl = process.argv[2];
|
|
1249
|
+
const { getOAuthProvider } = await import(oauthModuleUrl);
|
|
754
1250
|
|
|
755
|
-
const provider = getOAuthProvider(providerId);
|
|
756
|
-
if (!provider) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
1251
|
+
const provider = getOAuthProvider(providerId);
|
|
1252
|
+
if (!provider) {
|
|
1253
|
+
process.stdout.write(JSON.stringify({ type: 'error', message: `Unknown OAuth provider: ${providerId}` }) + '\n');
|
|
1254
|
+
process.exit(1);
|
|
1255
|
+
}
|
|
760
1256
|
|
|
761
|
-
const rl = readline.createInterface({
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
});
|
|
1257
|
+
const rl = readline.createInterface({
|
|
1258
|
+
input: process.stdin,
|
|
1259
|
+
output: process.stderr,
|
|
1260
|
+
terminal: false,
|
|
1261
|
+
});
|
|
766
1262
|
|
|
767
|
-
function emit(event) {
|
|
768
|
-
|
|
769
|
-
}
|
|
1263
|
+
function emit(event) {
|
|
1264
|
+
process.stdout.write(JSON.stringify(event) + '\n');
|
|
1265
|
+
}
|
|
770
1266
|
|
|
771
|
-
function readLine() {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
1267
|
+
function readLine() {
|
|
1268
|
+
return new Promise((resolve) => {
|
|
1269
|
+
rl.once('line', (line) => resolve(line));
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
try {
|
|
1274
|
+
const credentials = await provider.login({
|
|
1275
|
+
onAuth: (info) => emit({
|
|
1276
|
+
type: 'auth',
|
|
1277
|
+
url: info.url,
|
|
1278
|
+
instructions: info.instructions ?? null,
|
|
1279
|
+
}),
|
|
1280
|
+
onPrompt: async (prompt) => {
|
|
1281
|
+
emit({
|
|
1282
|
+
type: 'prompt',
|
|
1283
|
+
message: prompt.message,
|
|
1284
|
+
placeholder: prompt.placeholder ?? null,
|
|
1285
|
+
allowEmpty: Boolean(prompt.allowEmpty),
|
|
1286
|
+
});
|
|
1287
|
+
const input = await readLine();
|
|
1288
|
+
return typeof input === 'string' ? input : '';
|
|
1289
|
+
},
|
|
1290
|
+
onProgress: (message) => emit({
|
|
1291
|
+
type: 'progress',
|
|
1292
|
+
message,
|
|
1293
|
+
}),
|
|
1294
|
+
});
|
|
776
1295
|
|
|
777
|
-
try {
|
|
778
|
-
const credentials = await provider.login({
|
|
779
|
-
onAuth: (info) => emit({
|
|
780
|
-
type: 'auth',
|
|
781
|
-
url: info.url,
|
|
782
|
-
instructions: info.instructions ?? null,
|
|
783
|
-
}),
|
|
784
|
-
onPrompt: async (prompt) => {
|
|
785
1296
|
emit({
|
|
786
|
-
type: '
|
|
787
|
-
|
|
788
|
-
placeholder: prompt.placeholder ?? null,
|
|
789
|
-
allowEmpty: Boolean(prompt.allowEmpty),
|
|
1297
|
+
type: 'success',
|
|
1298
|
+
credentials,
|
|
790
1299
|
});
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
} catch (error) {
|
|
807
|
-
emit({
|
|
808
|
-
type: 'error',
|
|
809
|
-
message: error instanceof Error ? error.message : String(error),
|
|
810
|
-
});
|
|
811
|
-
rl.close();
|
|
812
|
-
process.exit(1);
|
|
813
|
-
}
|
|
814
|
-
"""#
|
|
1300
|
+
rl.close();
|
|
1301
|
+
process.exit(0);
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
emit({
|
|
1304
|
+
type: 'error',
|
|
1305
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1306
|
+
});
|
|
1307
|
+
rl.close();
|
|
1308
|
+
process.exit(1);
|
|
1309
|
+
}
|
|
1310
|
+
"""#
|
|
1311
|
+
|
|
1312
|
+
private var authRuntimeURL: URL {
|
|
1313
|
+
sessionFileURL.deletingLastPathComponent().appendingPathComponent("oauth-runtime.json")
|
|
1314
|
+
}
|
|
815
1315
|
}
|