@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
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import SwiftUI
|
|
3
3
|
|
|
4
|
+
private final class OmniSearchPanel: NSPanel {
|
|
5
|
+
override var canBecomeKey: Bool { true }
|
|
6
|
+
override var canBecomeMain: Bool { true }
|
|
7
|
+
|
|
8
|
+
override func sendEvent(_ event: NSEvent) {
|
|
9
|
+
if event.type == .leftMouseDown || event.type == .rightMouseDown {
|
|
10
|
+
if !NSApp.isActive {
|
|
11
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
12
|
+
}
|
|
13
|
+
if !isKeyWindow {
|
|
14
|
+
makeKey()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
super.sendEvent(event)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private final class OmniSearchHostingView<Content: View>: NSHostingView<Content> {
|
|
22
|
+
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
|
|
23
|
+
override var focusRingType: NSFocusRingType { get { .none } set {} }
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
final class OmniSearchWindow {
|
|
5
27
|
static let shared = OmniSearchWindow()
|
|
6
28
|
|
|
@@ -34,17 +56,16 @@ final class OmniSearchWindow {
|
|
|
34
56
|
}
|
|
35
57
|
.preferredColorScheme(.dark)
|
|
36
58
|
|
|
37
|
-
let hosting =
|
|
38
|
-
hosting.
|
|
59
|
+
let hosting = OmniSearchHostingView(rootView: view)
|
|
60
|
+
hosting.translatesAutoresizingMaskIntoConstraints = false
|
|
39
61
|
|
|
40
|
-
let p =
|
|
62
|
+
let p = OmniSearchPanel(
|
|
41
63
|
contentRect: NSRect(x: 0, y: 0, width: 520, height: 480),
|
|
42
64
|
styleMask: [.titled, .closable, .resizable, .utilityWindow, .nonactivatingPanel],
|
|
43
65
|
backing: .buffered,
|
|
44
66
|
defer: false
|
|
45
67
|
)
|
|
46
|
-
p.
|
|
47
|
-
p.title = "Omni Search"
|
|
68
|
+
p.title = "Search"
|
|
48
69
|
p.titlebarAppearsTransparent = true
|
|
49
70
|
p.titleVisibility = .hidden
|
|
50
71
|
p.isMovableByWindowBackground = true
|
|
@@ -56,6 +77,24 @@ final class OmniSearchWindow {
|
|
|
56
77
|
p.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
57
78
|
p.minSize = NSSize(width: 400, height: 300)
|
|
58
79
|
p.maxSize = NSSize(width: 700, height: 700)
|
|
80
|
+
p.hidesOnDeactivate = false
|
|
81
|
+
p.becomesKeyOnlyIfNeeded = false
|
|
82
|
+
|
|
83
|
+
let effectView = NSVisualEffectView()
|
|
84
|
+
effectView.blendingMode = .behindWindow
|
|
85
|
+
effectView.material = .popover
|
|
86
|
+
effectView.state = .active
|
|
87
|
+
effectView.wantsLayer = true
|
|
88
|
+
effectView.maskImage = Self.maskImage(cornerRadius: 14)
|
|
89
|
+
p.contentView = effectView
|
|
90
|
+
|
|
91
|
+
effectView.addSubview(hosting)
|
|
92
|
+
NSLayoutConstraint.activate([
|
|
93
|
+
hosting.leadingAnchor.constraint(equalTo: effectView.leadingAnchor),
|
|
94
|
+
hosting.trailingAnchor.constraint(equalTo: effectView.trailingAnchor),
|
|
95
|
+
hosting.topAnchor.constraint(equalTo: effectView.topAnchor),
|
|
96
|
+
hosting.bottomAnchor.constraint(equalTo: effectView.bottomAnchor),
|
|
97
|
+
])
|
|
59
98
|
|
|
60
99
|
// Center on screen
|
|
61
100
|
if let screen = NSScreen.main {
|
|
@@ -102,4 +141,25 @@ final class OmniSearchWindow {
|
|
|
102
141
|
keyMonitor = nil
|
|
103
142
|
}
|
|
104
143
|
}
|
|
144
|
+
|
|
145
|
+
private static func maskImage(cornerRadius: CGFloat) -> NSImage {
|
|
146
|
+
let edgeLength = 2.0 * cornerRadius + 1.0
|
|
147
|
+
let maskImage = NSImage(
|
|
148
|
+
size: NSSize(width: edgeLength, height: edgeLength),
|
|
149
|
+
flipped: false
|
|
150
|
+
) { rect in
|
|
151
|
+
let path = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
|
|
152
|
+
NSColor.black.set()
|
|
153
|
+
path.fill()
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
maskImage.capInsets = NSEdgeInsets(
|
|
157
|
+
top: cornerRadius,
|
|
158
|
+
left: cornerRadius,
|
|
159
|
+
bottom: cornerRadius,
|
|
160
|
+
right: cornerRadius
|
|
161
|
+
)
|
|
162
|
+
maskImage.resizingMode = .stretch
|
|
163
|
+
return maskImage
|
|
164
|
+
}
|
|
105
165
|
}
|
|
@@ -77,7 +77,7 @@ struct OnboardingView: View {
|
|
|
77
77
|
.padding(.horizontal, 40)
|
|
78
78
|
.padding(.bottom, 28)
|
|
79
79
|
}
|
|
80
|
-
.frame(width: 480, height:
|
|
80
|
+
.frame(width: 480, height: 470)
|
|
81
81
|
.background(Palette.bg)
|
|
82
82
|
.preferredColorScheme(.dark)
|
|
83
83
|
}
|
|
@@ -111,8 +111,8 @@ struct OnboardingView: View {
|
|
|
111
111
|
private var screenRecordingStep: some View {
|
|
112
112
|
permissionStep(
|
|
113
113
|
icon: "rectangle.dashed.badge.record",
|
|
114
|
-
title: "Screen
|
|
115
|
-
description: "Allows Lattices to index on-screen text with OCR so you can search across all your windows.",
|
|
114
|
+
title: "Screen Capture",
|
|
115
|
+
description: "Allows Lattices to index on-screen text with OCR so you can search across all your windows. On newer macOS versions this finishes in System Settings under Screen & System Audio Recording.",
|
|
116
116
|
granted: permChecker.screenRecording,
|
|
117
117
|
action: { permChecker.requestScreenRecording() }
|
|
118
118
|
)
|
|
@@ -220,18 +220,11 @@ struct OnboardingView: View {
|
|
|
220
220
|
.multilineTextAlignment(.center)
|
|
221
221
|
.lineSpacing(3)
|
|
222
222
|
|
|
223
|
-
Button(action: {
|
|
224
|
-
let task = Process()
|
|
225
|
-
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
226
|
-
task.arguments = ["-lc", "brew install tmux"]
|
|
227
|
-
task.standardOutput = FileHandle.nullDevice
|
|
228
|
-
task.standardError = FileHandle.nullDevice
|
|
229
|
-
try? task.run()
|
|
230
|
-
}) {
|
|
223
|
+
Button(action: CliActionLauncher.installTmuxInTerminal) {
|
|
231
224
|
HStack(spacing: 6) {
|
|
232
225
|
Image(systemName: "arrow.down.circle")
|
|
233
226
|
.font(.system(size: 11))
|
|
234
|
-
Text("
|
|
227
|
+
Text("Install tmux in Terminal")
|
|
235
228
|
.font(Typo.monoBold(11))
|
|
236
229
|
}
|
|
237
230
|
.angularButton(.white, filled: false)
|
|
@@ -274,6 +267,27 @@ struct OnboardingView: View {
|
|
|
274
267
|
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
275
268
|
)
|
|
276
269
|
)
|
|
270
|
+
|
|
271
|
+
VStack(spacing: 10) {
|
|
272
|
+
Text("Pick a repo and let the CLI do the setup in your terminal.")
|
|
273
|
+
.font(Typo.mono(10))
|
|
274
|
+
.foregroundColor(Palette.textMuted)
|
|
275
|
+
.multilineTextAlignment(.center)
|
|
276
|
+
|
|
277
|
+
HStack(spacing: 10) {
|
|
278
|
+
Button(action: CliActionLauncher.initializeProjectInTerminal) {
|
|
279
|
+
Text("Initialize Project")
|
|
280
|
+
.angularButton(Palette.running)
|
|
281
|
+
}
|
|
282
|
+
.buttonStyle(.plain)
|
|
283
|
+
|
|
284
|
+
Button(action: CliActionLauncher.launchProjectInTerminal) {
|
|
285
|
+
Text("Launch Project")
|
|
286
|
+
.angularButton(.white, filled: false)
|
|
287
|
+
}
|
|
288
|
+
.buttonStyle(.plain)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
277
291
|
}
|
|
278
292
|
}
|
|
279
293
|
|
|
@@ -310,7 +324,7 @@ struct OnboardingView: View {
|
|
|
310
324
|
}
|
|
311
325
|
.buttonStyle(.plain)
|
|
312
326
|
|
|
313
|
-
Text("macOS
|
|
327
|
+
Text("macOS may send you to System Settings to finish this step.")
|
|
314
328
|
.font(Typo.mono(10))
|
|
315
329
|
.foregroundColor(Palette.textMuted)
|
|
316
330
|
.multilineTextAlignment(.center)
|
|
@@ -431,9 +445,9 @@ final class OnboardingWindowController {
|
|
|
431
445
|
config: .init(
|
|
432
446
|
title: "Welcome to Lattices",
|
|
433
447
|
titleVisible: false,
|
|
434
|
-
initialSize: NSSize(width: 480, height:
|
|
435
|
-
minSize: NSSize(width: 480, height:
|
|
436
|
-
maxSize: NSSize(width: 480, height:
|
|
448
|
+
initialSize: NSSize(width: 480, height: 470),
|
|
449
|
+
minSize: NSSize(width: 480, height: 470),
|
|
450
|
+
maxSize: NSSize(width: 480, height: 470),
|
|
437
451
|
miniaturizable: false
|
|
438
452
|
),
|
|
439
453
|
rootView: view
|
|
@@ -364,24 +364,44 @@ enum CommandBuilder {
|
|
|
364
364
|
}
|
|
365
365
|
))
|
|
366
366
|
|
|
367
|
+
commands.append(PaletteCommand(
|
|
368
|
+
id: "app-home",
|
|
369
|
+
title: "Home",
|
|
370
|
+
subtitle: "Open the workspace home view",
|
|
371
|
+
icon: "house",
|
|
372
|
+
category: .app,
|
|
373
|
+
badge: nil,
|
|
374
|
+
action: { ScreenMapWindowController.shared.showPage(.home) }
|
|
375
|
+
))
|
|
376
|
+
|
|
367
377
|
commands.append(PaletteCommand(
|
|
368
378
|
id: "app-windows-list",
|
|
369
|
-
title: "
|
|
370
|
-
subtitle: "Browse
|
|
371
|
-
icon: "
|
|
379
|
+
title: "Search",
|
|
380
|
+
subtitle: "Browse windows, displays, spaces, and screen text",
|
|
381
|
+
icon: "magnifyingglass",
|
|
372
382
|
category: .app,
|
|
373
383
|
badge: nil,
|
|
374
|
-
action: {
|
|
384
|
+
action: { ScreenMapWindowController.shared.showPage(.desktopInventory) }
|
|
375
385
|
))
|
|
376
386
|
|
|
377
387
|
commands.append(PaletteCommand(
|
|
378
388
|
id: "app-screen-map",
|
|
379
|
-
title: "
|
|
389
|
+
title: "Layout",
|
|
380
390
|
subtitle: "Visual window editor",
|
|
381
391
|
icon: "rectangle.3.group",
|
|
382
392
|
category: .app,
|
|
383
393
|
badge: nil,
|
|
384
|
-
action: { ScreenMapWindowController.shared.
|
|
394
|
+
action: { ScreenMapWindowController.shared.showPage(.screenMap) }
|
|
395
|
+
))
|
|
396
|
+
|
|
397
|
+
commands.append(PaletteCommand(
|
|
398
|
+
id: "app-workspace-chat",
|
|
399
|
+
title: "Workspace Chat",
|
|
400
|
+
subtitle: "Open the longer-form assistant surface",
|
|
401
|
+
icon: "bubble.left.and.bubble.right",
|
|
402
|
+
category: .app,
|
|
403
|
+
badge: nil,
|
|
404
|
+
action: { ScreenMapWindowController.shared.showPage(.pi) }
|
|
385
405
|
))
|
|
386
406
|
|
|
387
407
|
commands.append(PaletteCommand(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import SwiftUI
|
|
3
3
|
import Combine
|
|
4
|
+
import ScreenCaptureKit
|
|
4
5
|
|
|
5
6
|
final class PermissionChecker: ObservableObject {
|
|
6
7
|
static let shared = PermissionChecker()
|
|
@@ -71,12 +72,46 @@ final class PermissionChecker: ObservableObject {
|
|
|
71
72
|
func requestScreenRecording() {
|
|
72
73
|
let diag = DiagnosticLog.shared
|
|
73
74
|
let beforeCheck = CGPreflightScreenCaptureAccess()
|
|
74
|
-
diag.info("requestScreenRecording: before=\(beforeCheck),
|
|
75
|
+
diag.info("requestScreenRecording: before=\(beforeCheck), probing…")
|
|
76
|
+
|
|
77
|
+
// On newer macOS releases TCC no longer reliably prompts for screen capture
|
|
78
|
+
// through the legacy CoreGraphics request API. Warm up ScreenCaptureKit first,
|
|
79
|
+
// then fall back to opening System Settings if access is still denied.
|
|
80
|
+
if #available(macOS 15.0, *) {
|
|
81
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
82
|
+
Task { @MainActor [weak self] in
|
|
83
|
+
guard let self else { return }
|
|
84
|
+
|
|
85
|
+
let shareableProbe = await self.probeScreenCaptureShareableContent()
|
|
86
|
+
diag.info("ScreenCaptureKit shareable probe → \(shareableProbe)")
|
|
87
|
+
|
|
88
|
+
if #available(macOS 15.2, *) {
|
|
89
|
+
let afterShareable = CGPreflightScreenCaptureAccess()
|
|
90
|
+
if !afterShareable {
|
|
91
|
+
let screenshotProbe = await self.probeScreenCaptureScreenshot()
|
|
92
|
+
diag.info("ScreenCaptureKit screenshot probe → \(screenshotProbe)")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let afterCheck = CGPreflightScreenCaptureAccess()
|
|
97
|
+
diag.info("requestScreenRecording: after=\(afterCheck)")
|
|
98
|
+
self.screenRecording = afterCheck
|
|
99
|
+
|
|
100
|
+
if !afterCheck {
|
|
101
|
+
diag.warn("Screen capture not granted — opening System Settings. On newer macOS versions this may require enabling Lattices in Privacy → Screen & System Audio Recording.")
|
|
102
|
+
self.openScreenRecordingSettings()
|
|
103
|
+
self.startPolling()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
diag.info("requestScreenRecording: using legacy CoreGraphics request API")
|
|
75
110
|
let result = CGRequestScreenCaptureAccess()
|
|
76
111
|
diag.info("CGRequestScreenCaptureAccess() → \(result)")
|
|
77
112
|
screenRecording = result
|
|
78
113
|
if !result {
|
|
79
|
-
diag.warn("Screen
|
|
114
|
+
diag.warn("Screen capture not granted — opening System Settings. Toggle ON in Privacy → Screen Recording.")
|
|
80
115
|
openScreenRecordingSettings()
|
|
81
116
|
startPolling()
|
|
82
117
|
}
|
|
@@ -96,6 +131,45 @@ final class PermissionChecker: ObservableObject {
|
|
|
96
131
|
}
|
|
97
132
|
}
|
|
98
133
|
|
|
134
|
+
@available(macOS 15.0, *)
|
|
135
|
+
private func probeScreenCaptureShareableContent() async -> String {
|
|
136
|
+
await withCheckedContinuation { continuation in
|
|
137
|
+
SCShareableContent.getExcludingDesktopWindows(true, onScreenWindowsOnly: true) { content, error in
|
|
138
|
+
if let error {
|
|
139
|
+
continuation.resume(returning: "error \(Self.describe(error))")
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let windows = content?.windows.count ?? 0
|
|
144
|
+
let displays = content?.displays.count ?? 0
|
|
145
|
+
let apps = content?.applications.count ?? 0
|
|
146
|
+
continuation.resume(returning: "ok windows=\(windows) displays=\(displays) apps=\(apps)")
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@available(macOS 15.2, *)
|
|
152
|
+
private func probeScreenCaptureScreenshot() async -> String {
|
|
153
|
+
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
|
|
154
|
+
return await withCheckedContinuation { continuation in
|
|
155
|
+
SCScreenshotManager.captureImage(in: rect) { _, error in
|
|
156
|
+
if let error {
|
|
157
|
+
continuation.resume(returning: "error \(Self.describe(error))")
|
|
158
|
+
} else {
|
|
159
|
+
continuation.resume(returning: "ok")
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private static func describe(_ error: Error) -> String {
|
|
166
|
+
let nsError = error as NSError
|
|
167
|
+
if nsError.localizedDescription.isEmpty {
|
|
168
|
+
return "\(nsError.domain)#\(nsError.code)"
|
|
169
|
+
}
|
|
170
|
+
return "\(nsError.domain)#\(nsError.code) \(nsError.localizedDescription)"
|
|
171
|
+
}
|
|
172
|
+
|
|
99
173
|
// MARK: - Polling
|
|
100
174
|
|
|
101
175
|
/// Poll every 2 seconds to detect permission changes made in System Settings.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
struct PiAuthNextStepCard: View {
|
|
4
|
+
@ObservedObject var session: PiChatSession
|
|
5
|
+
let compact: Bool
|
|
6
|
+
|
|
7
|
+
var body: some View {
|
|
8
|
+
VStack(alignment: .leading, spacing: compact ? 10 : 12) {
|
|
9
|
+
HStack(spacing: 8) {
|
|
10
|
+
Circle()
|
|
11
|
+
.fill(Palette.running)
|
|
12
|
+
.frame(width: compact ? 6 : 7, height: compact ? 6 : 7)
|
|
13
|
+
|
|
14
|
+
Text(session.authStepLabel)
|
|
15
|
+
.font(Typo.geistMonoBold(compact ? 9 : 10))
|
|
16
|
+
.foregroundColor(Palette.running.opacity(0.95))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Text(session.authStepTitle)
|
|
20
|
+
.font(Typo.geistMonoBold(compact ? 11 : 13))
|
|
21
|
+
.foregroundColor(Palette.text)
|
|
22
|
+
|
|
23
|
+
Text(session.authStepDescription)
|
|
24
|
+
.font(Typo.mono(compact ? 10 : 11))
|
|
25
|
+
.foregroundColor(Palette.textDim)
|
|
26
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
27
|
+
|
|
28
|
+
if let code = session.authVerificationCode {
|
|
29
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
30
|
+
HStack(spacing: 6) {
|
|
31
|
+
Text("CODE READY")
|
|
32
|
+
.font(Typo.geistMonoBold(compact ? 8 : 9))
|
|
33
|
+
.foregroundColor(Palette.textMuted)
|
|
34
|
+
|
|
35
|
+
if session.authVerificationCodeCopied {
|
|
36
|
+
statusCapsule("COPIED")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Text(code)
|
|
41
|
+
.font(Typo.geistMonoBold(compact ? 14 : 16))
|
|
42
|
+
.foregroundColor(Palette.text)
|
|
43
|
+
.textSelection(.enabled)
|
|
44
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
45
|
+
.padding(.horizontal, compact ? 10 : 12)
|
|
46
|
+
.padding(.vertical, compact ? 9 : 10)
|
|
47
|
+
.background(
|
|
48
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
49
|
+
.fill(Color.white.opacity(0.04))
|
|
50
|
+
.overlay(
|
|
51
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
52
|
+
.strokeBorder(Palette.borderLit.opacity(0.5), lineWidth: 0.5)
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
primaryActionButton(
|
|
59
|
+
session.latestAuthURL == nil
|
|
60
|
+
? "OPENING BROWSER..."
|
|
61
|
+
: (session.authVerificationCode != nil ? "OPEN PAGE AGAIN" : "OPEN BROWSER AGAIN"),
|
|
62
|
+
tint: Palette.running,
|
|
63
|
+
disabled: session.latestAuthURL == nil
|
|
64
|
+
) {
|
|
65
|
+
session.reopenLatestAuthURL()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
HStack(spacing: 8) {
|
|
69
|
+
if session.authVerificationCode != nil {
|
|
70
|
+
secondaryActionButton("COPY AGAIN", tint: Palette.text) {
|
|
71
|
+
session.copyAuthVerificationCode()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Spacer(minLength: 0)
|
|
76
|
+
|
|
77
|
+
secondaryActionButton("CANCEL", tint: Palette.textMuted) {
|
|
78
|
+
session.cancelAuthFlow()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
.padding(.horizontal, compact ? 10 : 16)
|
|
83
|
+
.padding(.vertical, compact ? 10 : 14)
|
|
84
|
+
.background(
|
|
85
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
86
|
+
.fill(Palette.running.opacity(0.06))
|
|
87
|
+
.overlay(
|
|
88
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
89
|
+
.strokeBorder(Palette.running.opacity(0.22), lineWidth: 0.5)
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private func primaryActionButton(_ label: String, tint: Color, disabled: Bool = false, action: @escaping () -> Void) -> some View {
|
|
95
|
+
Button(action: action) {
|
|
96
|
+
Text(label)
|
|
97
|
+
.font(Typo.geistMonoBold(compact ? 10 : 11))
|
|
98
|
+
.foregroundColor(disabled ? Palette.textMuted : tint)
|
|
99
|
+
.frame(maxWidth: .infinity)
|
|
100
|
+
.padding(.horizontal, compact ? 10 : 12)
|
|
101
|
+
.padding(.vertical, compact ? 8 : 10)
|
|
102
|
+
.background(
|
|
103
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
104
|
+
.fill(tint.opacity(disabled ? 0.05 : 0.12))
|
|
105
|
+
.overlay(
|
|
106
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
107
|
+
.strokeBorder((disabled ? Palette.border : tint.opacity(0.35)), lineWidth: 0.5)
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
.buttonStyle(.plain)
|
|
112
|
+
.opacity(disabled ? 0.7 : 1)
|
|
113
|
+
.disabled(disabled)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func secondaryActionButton(_ label: String, tint: Color, action: @escaping () -> Void) -> some View {
|
|
117
|
+
Button(label, action: action)
|
|
118
|
+
.buttonStyle(.plain)
|
|
119
|
+
.font(Typo.geistMonoBold(compact ? 9 : 10))
|
|
120
|
+
.foregroundColor(tint)
|
|
121
|
+
.padding(.horizontal, compact ? 8 : 10)
|
|
122
|
+
.padding(.vertical, compact ? 5 : 6)
|
|
123
|
+
.background(
|
|
124
|
+
Capsule()
|
|
125
|
+
.fill(Color.white.opacity(0.03))
|
|
126
|
+
.overlay(
|
|
127
|
+
Capsule()
|
|
128
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func statusCapsule(_ text: String) -> some View {
|
|
134
|
+
Text(text)
|
|
135
|
+
.font(Typo.geistMonoBold(compact ? 8 : 9))
|
|
136
|
+
.foregroundColor(Palette.running.opacity(0.95))
|
|
137
|
+
.padding(.horizontal, compact ? 6 : 7)
|
|
138
|
+
.padding(.vertical, compact ? 3 : 4)
|
|
139
|
+
.background(
|
|
140
|
+
Capsule()
|
|
141
|
+
.fill(Palette.running.opacity(0.12))
|
|
142
|
+
.overlay(
|
|
143
|
+
Capsule()
|
|
144
|
+
.strokeBorder(Palette.running.opacity(0.28), lineWidth: 0.5)
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
struct PiAuthPromptCard: View {
|
|
4
|
+
@ObservedObject var session: PiChatSession
|
|
5
|
+
let prompt: PiAuthPrompt
|
|
6
|
+
let compact: Bool
|
|
7
|
+
var focus: FocusState<Bool>.Binding
|
|
8
|
+
|
|
9
|
+
var body: some View {
|
|
10
|
+
VStack(alignment: .leading, spacing: compact ? 10 : 12) {
|
|
11
|
+
HStack(spacing: 8) {
|
|
12
|
+
Circle()
|
|
13
|
+
.fill(Palette.detach)
|
|
14
|
+
.frame(width: compact ? 6 : 7, height: compact ? 6 : 7)
|
|
15
|
+
|
|
16
|
+
Text("STEP 1")
|
|
17
|
+
.font(Typo.geistMonoBold(compact ? 9 : 10))
|
|
18
|
+
.foregroundColor(Palette.detach.opacity(0.95))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Text("One quick question")
|
|
22
|
+
.font(Typo.geistMonoBold(compact ? 11 : 13))
|
|
23
|
+
.foregroundColor(Palette.text)
|
|
24
|
+
|
|
25
|
+
Text(prompt.message)
|
|
26
|
+
.font(Typo.mono(compact ? 10 : 11))
|
|
27
|
+
.foregroundColor(Palette.textDim)
|
|
28
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
29
|
+
|
|
30
|
+
HStack(spacing: 8) {
|
|
31
|
+
TextField(prompt.placeholder ?? "Type your answer", text: $session.authPromptInput)
|
|
32
|
+
.textFieldStyle(.plain)
|
|
33
|
+
.font(Typo.mono(compact ? 11 : 12))
|
|
34
|
+
.foregroundColor(Palette.text)
|
|
35
|
+
.focused(focus)
|
|
36
|
+
|
|
37
|
+
actionButton("CONTINUE", tint: Palette.detach, disabled: !session.canSubmitAuthPrompt) {
|
|
38
|
+
session.submitAuthPrompt()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
.padding(.horizontal, compact ? 10 : 12)
|
|
42
|
+
.padding(.vertical, compact ? 8 : 10)
|
|
43
|
+
.background(
|
|
44
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
45
|
+
.fill(Color.white.opacity(0.04))
|
|
46
|
+
.overlay(
|
|
47
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
48
|
+
.strokeBorder(Palette.border.opacity(0.85), lineWidth: 0.5)
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
HStack(spacing: 8) {
|
|
53
|
+
Spacer(minLength: 0)
|
|
54
|
+
|
|
55
|
+
actionButton("CANCEL", tint: Palette.textMuted) {
|
|
56
|
+
session.cancelAuthFlow()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
.padding(.horizontal, compact ? 10 : 16)
|
|
61
|
+
.padding(.vertical, compact ? 10 : 14)
|
|
62
|
+
.background(
|
|
63
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
64
|
+
.fill(Palette.detach.opacity(0.06))
|
|
65
|
+
.overlay(
|
|
66
|
+
RoundedRectangle(cornerRadius: compact ? 6 : 8)
|
|
67
|
+
.strokeBorder(Palette.detach.opacity(0.22), lineWidth: 0.5)
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private func actionButton(_ label: String, tint: Color, disabled: Bool = false, action: @escaping () -> Void) -> some View {
|
|
73
|
+
Button(label, action: action)
|
|
74
|
+
.buttonStyle(.plain)
|
|
75
|
+
.font(Typo.geistMonoBold(compact ? 9 : 10))
|
|
76
|
+
.foregroundColor(disabled ? Palette.textMuted : tint)
|
|
77
|
+
.padding(.horizontal, compact ? 8 : 10)
|
|
78
|
+
.padding(.vertical, compact ? 5 : 6)
|
|
79
|
+
.background(
|
|
80
|
+
Capsule()
|
|
81
|
+
.fill(Color.white.opacity(0.03))
|
|
82
|
+
.overlay(
|
|
83
|
+
Capsule()
|
|
84
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
.opacity(disabled ? 0.65 : 1)
|
|
88
|
+
.disabled(disabled)
|
|
89
|
+
}
|
|
90
|
+
}
|