@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.
Files changed (70) hide show
  1. package/README.md +3 -0
  2. package/app/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/Info.plist +2 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Package.swift +6 -0
  6. package/app/Sources/App.swift +10 -0
  7. package/app/Sources/AppDelegate.swift +90 -34
  8. package/app/Sources/AppShellView.swift +2 -0
  9. package/app/Sources/AppTypeClassifier.swift +36 -0
  10. package/app/Sources/AppUpdater.swift +92 -0
  11. package/app/Sources/CheatSheetHUD.swift +1 -0
  12. package/app/Sources/CliActionLauncher.swift +50 -0
  13. package/app/Sources/CommandModeView.swift +4 -24
  14. package/app/Sources/CompanionActivityLog.swift +70 -0
  15. package/app/Sources/CompanionKeyboardController.swift +141 -0
  16. package/app/Sources/DesktopModel.swift +4 -0
  17. package/app/Sources/HandsOffSession.swift +15 -4
  18. package/app/Sources/HomeDashboardView.swift +18 -10
  19. package/app/Sources/HotkeyStore.swift +8 -5
  20. package/app/Sources/IntentEngine.swift +7 -1
  21. package/app/Sources/LatticesApi.swift +125 -4
  22. package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
  23. package/app/Sources/LatticesCompanionCockpit.swift +555 -0
  24. package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
  25. package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
  26. package/app/Sources/LatticesDeckHost.swift +1463 -0
  27. package/app/Sources/LatticesRuntime.swift +61 -0
  28. package/app/Sources/MainView.swift +351 -191
  29. package/app/Sources/MouseFinder.swift +335 -30
  30. package/app/Sources/MouseGestureConfig.swift +364 -0
  31. package/app/Sources/MouseGestureController.swift +1203 -0
  32. package/app/Sources/MouseInputDeviceStore.swift +98 -0
  33. package/app/Sources/MouseInputEventViewer.swift +272 -0
  34. package/app/Sources/MouseShortcutStore.swift +107 -0
  35. package/app/Sources/OmniSearchView.swift +136 -2
  36. package/app/Sources/OmniSearchWindow.swift +65 -5
  37. package/app/Sources/OnboardingView.swift +30 -16
  38. package/app/Sources/PaletteCommand.swift +26 -6
  39. package/app/Sources/PermissionChecker.swift +76 -2
  40. package/app/Sources/PiAuthNextStepCard.swift +148 -0
  41. package/app/Sources/PiAuthPromptCard.swift +90 -0
  42. package/app/Sources/PiChatDock.swift +137 -74
  43. package/app/Sources/PiChatSession.swift +608 -108
  44. package/app/Sources/PiInstallCallout.swift +86 -0
  45. package/app/Sources/PiProviderSetupCallout.swift +99 -0
  46. package/app/Sources/PiWorkspaceView.swift +174 -77
  47. package/app/Sources/Preferences.swift +78 -0
  48. package/app/Sources/ScreenMapState.swift +91 -31
  49. package/app/Sources/ScreenMapView.swift +510 -524
  50. package/app/Sources/ScreenMapWindowController.swift +12 -4
  51. package/app/Sources/SettingsView.swift +869 -152
  52. package/app/Sources/SystemTelemetryMonitor.swift +273 -0
  53. package/app/Sources/VoiceCommandWindow.swift +23 -2
  54. package/app/Sources/WindowDragSnapController.swift +628 -0
  55. package/app/Sources/WindowTiler.swift +328 -65
  56. package/app/Sources/WorkspaceManager.swift +288 -0
  57. package/bin/assistant-intelligence.ts +874 -0
  58. package/bin/handsoff-infer.ts +16 -209
  59. package/bin/handsoff-worker.ts +45 -258
  60. package/bin/lattices-app.ts +62 -0
  61. package/bin/lattices-dev +4 -0
  62. package/bin/lattices.ts +125 -14
  63. package/docs/agents.md +14 -0
  64. package/docs/api.md +55 -0
  65. package/docs/app.md +3 -0
  66. package/docs/companion-deck.md +180 -0
  67. package/docs/config.md +25 -0
  68. package/docs/tiling-reference.md +55 -0
  69. package/docs/voice-error-model.md +73 -0
  70. 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 = NSHostingController(rootView: view)
38
- hosting.preferredContentSize = NSSize(width: 520, height: 480)
59
+ let hosting = OmniSearchHostingView(rootView: view)
60
+ hosting.translatesAutoresizingMaskIntoConstraints = false
39
61
 
40
- let p = NSPanel(
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.contentViewController = hosting
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: 420)
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 Recording",
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("brew install tmux")
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 will ask you to toggle this on in System Settings.")
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: 420),
435
- minSize: NSSize(width: 480, height: 420),
436
- maxSize: NSSize(width: 480, height: 420),
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: "Windows List",
370
- subtitle: "Browse all windows across displays",
371
- icon: "rectangle.split.2x1",
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: { CommandModeWindow.shared.show() }
384
+ action: { ScreenMapWindowController.shared.showPage(.desktopInventory) }
375
385
  ))
376
386
 
377
387
  commands.append(PaletteCommand(
378
388
  id: "app-screen-map",
379
- title: "Window Map",
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.show() }
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), prompting…")
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 Recording not granted — opening System Settings. Toggle ON in Privacy → Screen Recording.")
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
+ }