@lattices/cli 0.4.2 → 0.4.6

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 (146) 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/AppShell/App.swift +20 -0
  7. package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
  8. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
  9. package/app/Sources/AppShell/AppUpdater.swift +92 -0
  10. package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
  11. package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
  12. package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
  13. package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
  14. package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
  15. package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
  16. package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
  17. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
  18. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
  19. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  20. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  21. package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
  22. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  23. package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
  24. package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
  25. package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
  26. package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
  27. package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
  28. package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
  29. package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
  30. package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
  31. package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
  32. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
  33. package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
  34. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  35. package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
  36. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  37. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  38. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  39. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
  40. package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
  41. package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
  42. package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
  43. package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
  44. package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
  45. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
  46. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
  47. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  48. package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
  49. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  50. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  51. package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
  52. package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
  53. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  54. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
  55. package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
  56. package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
  57. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
  58. package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
  59. package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
  60. package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
  61. package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
  62. package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
  63. package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
  64. package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
  65. package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
  66. package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
  67. package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
  68. package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
  69. package/bin/assistant-intelligence.ts +874 -0
  70. package/bin/handsoff-infer.ts +16 -209
  71. package/bin/handsoff-worker.ts +45 -258
  72. package/bin/lattices-app.ts +62 -0
  73. package/bin/lattices-dev +4 -0
  74. package/bin/lattices.ts +125 -14
  75. package/docs/agents.md +14 -0
  76. package/docs/api.md +55 -0
  77. package/docs/app.md +3 -0
  78. package/docs/companion-deck.md +180 -0
  79. package/docs/component-extraction-roadmap.md +392 -0
  80. package/docs/config.md +25 -0
  81. package/docs/tiling-reference.md +55 -0
  82. package/docs/voice-error-model.md +73 -0
  83. package/package.json +4 -1
  84. package/app/Sources/App.swift +0 -10
  85. package/app/Sources/CommandPaletteWindow.swift +0 -134
  86. package/app/Sources/MouseFinder.swift +0 -222
  87. /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
  88. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  89. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  90. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  91. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  92. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  93. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  94. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  95. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  96. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  97. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  98. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  99. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  100. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  101. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  102. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  103. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  104. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  105. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  106. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  107. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  108. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  109. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  110. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  111. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  112. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  113. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  114. /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
  115. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  116. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  117. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  118. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  119. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  120. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  121. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  122. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  123. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  124. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  125. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  126. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  127. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  128. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  129. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  130. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  131. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  132. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  133. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  134. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  135. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  136. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  137. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  138. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  139. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  140. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  141. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
  142. /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
  143. /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
  144. /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
  145. /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
  146. /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
@@ -0,0 +1,86 @@
1
+ import SwiftUI
2
+
3
+ struct PiInstallCallout: 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.kill)
12
+ .frame(width: compact ? 6 : 7, height: compact ? 6 : 7)
13
+
14
+ Text("PI REQUIRED")
15
+ .font(Typo.geistMonoBold(compact ? 9 : 10))
16
+ .foregroundColor(Palette.kill.opacity(0.95))
17
+
18
+ Text("assistant unavailable")
19
+ .font(Typo.mono(compact ? 9 : 10))
20
+ .foregroundColor(Palette.textMuted)
21
+ }
22
+
23
+ Text("Install the official Pi coding agent CLI to use the in-app assistant. Lattices can copy the command or run it in \(Preferences.shared.terminal.rawValue).")
24
+ .font(Typo.mono(compact ? 10 : 11))
25
+ .foregroundColor(Palette.textDim)
26
+ .fixedSize(horizontal: false, vertical: true)
27
+
28
+ Text(session.piInstallCommand)
29
+ .font(Typo.mono(compact ? 10 : 11))
30
+ .foregroundColor(Palette.text)
31
+ .textSelection(.enabled)
32
+ .frame(maxWidth: .infinity, alignment: .leading)
33
+ .padding(.horizontal, compact ? 10 : 12)
34
+ .padding(.vertical, compact ? 8 : 10)
35
+ .background(
36
+ RoundedRectangle(cornerRadius: compact ? 6 : 8)
37
+ .fill(Color.black.opacity(0.35))
38
+ .overlay(
39
+ RoundedRectangle(cornerRadius: compact ? 6 : 8)
40
+ .strokeBorder(Palette.border, lineWidth: 0.5)
41
+ )
42
+ )
43
+
44
+ HStack(spacing: 8) {
45
+ actionButton(compact ? "COPY" : "COPY CMD", tint: Palette.running) {
46
+ session.copyPiInstallCommand()
47
+ }
48
+
49
+ actionButton(compact ? "INSTALL" : "INSTALL IN TERMINAL", tint: Palette.detach) {
50
+ session.installPiInTerminal()
51
+ }
52
+
53
+ actionButton("REFRESH", tint: Palette.textMuted) {
54
+ session.refreshBinaryAvailability()
55
+ }
56
+ }
57
+ }
58
+ .padding(.horizontal, compact ? 10 : 16)
59
+ .padding(.vertical, compact ? 10 : 14)
60
+ .background(
61
+ RoundedRectangle(cornerRadius: compact ? 6 : 8)
62
+ .fill(Palette.kill.opacity(0.06))
63
+ .overlay(
64
+ RoundedRectangle(cornerRadius: compact ? 6 : 8)
65
+ .strokeBorder(Palette.kill.opacity(0.22), lineWidth: 0.5)
66
+ )
67
+ )
68
+ }
69
+
70
+ private func actionButton(_ label: String, tint: Color, action: @escaping () -> Void) -> some View {
71
+ Button(label, action: action)
72
+ .buttonStyle(.plain)
73
+ .font(Typo.geistMonoBold(compact ? 9 : 10))
74
+ .foregroundColor(tint)
75
+ .padding(.horizontal, compact ? 8 : 10)
76
+ .padding(.vertical, compact ? 5 : 6)
77
+ .background(
78
+ Capsule()
79
+ .fill(Color.white.opacity(0.03))
80
+ .overlay(
81
+ Capsule()
82
+ .strokeBorder(Palette.border, lineWidth: 0.5)
83
+ )
84
+ )
85
+ }
86
+ }
@@ -0,0 +1,99 @@
1
+ import SwiftUI
2
+
3
+ struct PiProviderSetupCallout: 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.detach)
12
+ .frame(width: compact ? 6 : 7, height: compact ? 6 : 7)
13
+
14
+ Text("SET UP YOUR AI")
15
+ .font(Typo.geistMonoBold(compact ? 9 : 10))
16
+ .foregroundColor(Palette.detach.opacity(0.95))
17
+ }
18
+
19
+ Text(session.isAuthenticating
20
+ ? "Finish the setup above. As soon as that one step is done, the chat box unlocks."
21
+ : "Next step: connect \(session.currentProvider.name). You only have to do this once.")
22
+ .font(Typo.mono(compact ? 10 : 11))
23
+ .foregroundColor(Palette.textDim)
24
+ .fixedSize(horizontal: false, vertical: true)
25
+
26
+ HStack(spacing: 8) {
27
+ capsuleLabel(session.currentProvider.name.uppercased(), tint: Palette.text)
28
+ capsuleLabel(session.currentProvider.authMode == .oauth ? "SIGN IN" : "API KEY", tint: Palette.running)
29
+ }
30
+
31
+ if session.isAuthenticating {
32
+ PiAuthNextStepCard(session: session, compact: compact)
33
+ } else {
34
+ Text(session.currentProvider.authMode == .oauth
35
+ ? "The setup panel above is already open, so you can connect right now."
36
+ : "Paste your key in the setup panel above, save it once, and you are done.")
37
+ .font(Typo.mono(compact ? 9 : 10))
38
+ .foregroundColor(Palette.textMuted)
39
+ }
40
+
41
+ if session.currentProvider.authMode == .oauth && !session.isAuthenticating {
42
+ primaryActionButton(
43
+ "CONNECT \(session.currentProvider.name.uppercased())",
44
+ tint: Palette.running
45
+ ) {
46
+ session.startSelectedAuthFlow()
47
+ }
48
+ }
49
+ }
50
+ .padding(.horizontal, compact ? 10 : 16)
51
+ .padding(.vertical, compact ? 10 : 14)
52
+ .background(
53
+ RoundedRectangle(cornerRadius: compact ? 6 : 8)
54
+ .fill(Palette.detach.opacity(0.06))
55
+ .overlay(
56
+ RoundedRectangle(cornerRadius: compact ? 6 : 8)
57
+ .strokeBorder(Palette.detach.opacity(0.22), lineWidth: 0.5)
58
+ )
59
+ )
60
+ }
61
+
62
+ private func primaryActionButton(_ label: String, tint: Color, disabled: Bool = false, action: @escaping () -> Void) -> some View {
63
+ Button(action: action) {
64
+ Text(label)
65
+ .font(Typo.geistMonoBold(compact ? 10 : 11))
66
+ .foregroundColor(disabled ? Palette.textMuted : tint)
67
+ .frame(maxWidth: .infinity)
68
+ .padding(.horizontal, compact ? 10 : 12)
69
+ .padding(.vertical, compact ? 8 : 10)
70
+ .background(
71
+ RoundedRectangle(cornerRadius: compact ? 6 : 8)
72
+ .fill(tint.opacity(disabled ? 0.05 : 0.12))
73
+ .overlay(
74
+ RoundedRectangle(cornerRadius: compact ? 6 : 8)
75
+ .strokeBorder((disabled ? Palette.border : tint.opacity(0.35)), lineWidth: 0.5)
76
+ )
77
+ )
78
+ }
79
+ .buttonStyle(.plain)
80
+ .opacity(disabled ? 0.65 : 1)
81
+ .disabled(disabled)
82
+ }
83
+
84
+ private func capsuleLabel(_ text: String, tint: Color) -> some View {
85
+ Text(text)
86
+ .font(Typo.geistMonoBold(compact ? 9 : 10))
87
+ .foregroundColor(tint.opacity(0.95))
88
+ .padding(.horizontal, compact ? 7 : 8)
89
+ .padding(.vertical, compact ? 4 : 5)
90
+ .background(
91
+ Capsule()
92
+ .fill(tint.opacity(0.10))
93
+ .overlay(
94
+ Capsule()
95
+ .strokeBorder(tint.opacity(0.28), lineWidth: 0.5)
96
+ )
97
+ )
98
+ }
99
+ }
@@ -19,7 +19,7 @@ struct PiWorkspaceView: View {
19
19
  .fill(Palette.border)
20
20
  .frame(height: 0.5)
21
21
 
22
- if session.isAuthPanelVisible {
22
+ if session.hasPiBinary && session.isAuthPanelVisible {
23
23
  authPanel
24
24
 
25
25
  Rectangle()
@@ -33,12 +33,31 @@ struct PiWorkspaceView: View {
33
33
  .fill(Palette.border)
34
34
  .frame(height: 0.5)
35
35
 
36
- composer
36
+ if session.hasPiBinary && !session.needsProviderSetup {
37
+ composer
38
+ } else if session.needsProviderSetup {
39
+ if session.isAuthPanelVisible {
40
+ setupLockedPanel
41
+ } else {
42
+ PiProviderSetupCallout(session: session, compact: false)
43
+ .padding(.horizontal, 16)
44
+ .padding(.vertical, 14)
45
+ .background(Palette.surface.opacity(0.22))
46
+ }
47
+ } else {
48
+ PiInstallCallout(session: session, compact: false)
49
+ .padding(.horizontal, 16)
50
+ .padding(.vertical, 14)
51
+ .background(Palette.surface.opacity(0.22))
52
+ }
37
53
  }
38
54
  .background(Palette.bg)
39
55
  .onAppear {
40
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
41
- composerFocused = true
56
+ session.prepareForDisplay()
57
+ if session.hasPiBinary && !session.needsProviderSetup {
58
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
59
+ composerFocused = true
60
+ }
42
61
  }
43
62
  }
44
63
  }
@@ -51,14 +70,27 @@ struct PiWorkspaceView: View {
51
70
  .fill(session.hasPiBinary ? Palette.running : Palette.kill)
52
71
  .frame(width: 7, height: 7)
53
72
 
54
- Text("PI WORKSPACE")
73
+ Text("WORKSPACE CHAT")
55
74
  .font(Typo.geistMonoBold(11))
56
75
  .foregroundColor(Palette.text)
57
76
 
58
- capsuleLabel(session.statusText.uppercased(), tint: session.isSending ? Palette.detach : Palette.running)
77
+ capsuleLabel(
78
+ session.statusText.uppercased(),
79
+ tint: session.statusText == "missing pi"
80
+ ? Palette.kill
81
+ : ((session.statusText == "setup ai" || session.statusText == "connecting...")
82
+ ? Palette.detach
83
+ : (session.isSending ? Palette.detach : Palette.running))
84
+ )
59
85
  }
60
86
 
61
- Text("Full conversation surface for longer prompts, auth, and provider switching.")
87
+ Text(session.hasPiBinary
88
+ ? (session.isAuthenticating
89
+ ? session.authStepDescription
90
+ : (session.needsProviderSetup
91
+ ? "Next step: connect a provider to unlock chat."
92
+ : "Full conversation surface for longer prompts, auth, and provider switching."))
93
+ : "Install Pi to unlock longer prompts, provider auth, and the full in-app assistant surface.")
62
94
  .font(Typo.mono(10))
63
95
  .foregroundColor(Palette.textDim)
64
96
  }
@@ -68,12 +100,16 @@ struct PiWorkspaceView: View {
68
100
  HStack(spacing: 6) {
69
101
  capsuleLabel(session.currentProvider.name.uppercased(), tint: Palette.textDim)
70
102
 
71
- actionChip(session.isAuthPanelVisible ? "AUTH -" : "AUTH +") {
72
- session.toggleAuthPanel()
103
+ if session.hasPiBinary && !session.needsProviderSetup {
104
+ actionChip(session.isAuthPanelVisible ? "AUTH -" : "AUTH +") {
105
+ session.toggleAuthPanel()
106
+ }
73
107
  }
74
108
 
75
- actionChip("RESET") {
76
- session.clearConversation()
109
+ if session.hasConversationHistory {
110
+ actionChip("RESET") {
111
+ session.clearConversation()
112
+ }
77
113
  }
78
114
  }
79
115
  }
@@ -83,99 +119,113 @@ struct PiWorkspaceView: View {
83
119
 
84
120
  private var authPanel: some View {
85
121
  VStack(alignment: .leading, spacing: 12) {
86
- HStack(spacing: 8) {
87
- Text("provider")
88
- .font(Typo.geistMonoBold(9))
89
- .foregroundColor(Palette.textMuted)
122
+ if session.isAuthenticating {
123
+ VStack(alignment: .leading, spacing: 4) {
124
+ Text("Finish Setup")
125
+ .font(Typo.geistMonoBold(11))
126
+ .foregroundColor(Palette.text)
90
127
 
91
- Picker("Provider", selection: $session.authProviderID) {
92
- ForEach(session.providerOptions) { provider in
93
- Text(provider.name).tag(provider.id)
94
- }
128
+ Text("Ignore the rest for a second and just do the next step below.")
129
+ .font(Typo.mono(10))
130
+ .foregroundColor(Palette.textDim)
131
+ .fixedSize(horizontal: false, vertical: true)
95
132
  }
96
- .labelsHidden()
97
- .pickerStyle(.menu)
98
- .font(Typo.mono(10))
99
-
100
- Spacer()
101
-
102
- capsuleLabel(
103
- session.currentProvider.authMode == .oauth ? "OAUTH" : "TOKEN",
104
- tint: session.currentProvider.authMode == .oauth ? Palette.detach : Palette.running
105
- )
106
- }
107
-
108
- Text(session.currentProvider.helpText)
109
- .font(Typo.mono(10))
110
- .foregroundColor(Palette.textDim)
111
- .fixedSize(horizontal: false, vertical: true)
112
133
 
113
- if session.currentProvider.authMode == .apiKey {
114
134
  HStack(spacing: 8) {
115
- SecureField(session.currentProvider.tokenPlaceholder, text: $session.authToken)
116
- .textFieldStyle(.plain)
117
- .font(Typo.mono(11))
118
- .foregroundColor(Palette.text)
119
- .focused($authFieldFocused)
120
- .onSubmit {
121
- session.saveSelectedToken()
122
- }
135
+ capsuleLabel(session.currentProvider.name.uppercased(), tint: Palette.text)
136
+ capsuleLabel("IN PROGRESS", tint: Palette.detach)
137
+ Spacer()
138
+ }
123
139
 
124
- actionChip("SAVE") {
125
- session.saveSelectedToken()
126
- }
140
+ if let prompt = session.pendingAuthPrompt {
141
+ PiAuthPromptCard(session: session, prompt: prompt, compact: false, focus: $authFieldFocused)
142
+ } else {
143
+ PiAuthNextStepCard(session: session, compact: false)
144
+ }
145
+ } else {
146
+ if session.needsProviderSetup {
147
+ VStack(alignment: .leading, spacing: 4) {
148
+ Text("Set Up Your AI")
149
+ .font(Typo.geistMonoBold(11))
150
+ .foregroundColor(Palette.text)
127
151
 
128
- if session.hasSelectedCredential {
129
- actionChip("CLEAR") {
130
- session.removeSelectedCredential()
131
- }
152
+ Text("Choose a provider, connect it once, and the chat box unlocks automatically.")
153
+ .font(Typo.mono(10))
154
+ .foregroundColor(Palette.textDim)
155
+ .fixedSize(horizontal: false, vertical: true)
132
156
  }
133
157
  }
134
- .padding(.horizontal, 12)
135
- .padding(.vertical, 10)
136
- .background(authCardBackground(tint: Palette.running))
137
- } else {
158
+
138
159
  HStack(spacing: 8) {
139
- actionChip(session.isAuthenticating ? "CANCEL" : "LOGIN") {
140
- if session.isAuthenticating {
141
- session.cancelAuthFlow()
142
- } else {
143
- session.startSelectedAuthFlow()
144
- }
145
- }
160
+ Text(session.needsProviderSetup ? "choose provider" : "provider")
161
+ .font(Typo.geistMonoBold(9))
162
+ .foregroundColor(Palette.textMuted)
146
163
 
147
- if session.hasSelectedCredential {
148
- actionChip("CLEAR") {
149
- session.removeSelectedCredential()
164
+ Picker("Provider", selection: $session.authProviderID) {
165
+ ForEach(session.providerOptions) { provider in
166
+ Text(provider.name).tag(provider.id)
150
167
  }
151
168
  }
169
+ .labelsHidden()
170
+ .pickerStyle(.menu)
171
+ .font(Typo.mono(10))
172
+
173
+ Spacer()
174
+
175
+ capsuleLabel(
176
+ session.currentProvider.authMode == .oauth ? "OAUTH" : "TOKEN",
177
+ tint: session.currentProvider.authMode == .oauth ? Palette.detach : Palette.running
178
+ )
152
179
  }
153
- .padding(.horizontal, 12)
154
- .padding(.vertical, 10)
155
- .background(authCardBackground(tint: session.isAuthenticating ? Palette.detach : Palette.running))
156
180
 
157
- if let prompt = session.pendingAuthPrompt {
181
+ Text(session.currentProvider.helpText)
182
+ .font(Typo.mono(10))
183
+ .foregroundColor(Palette.textDim)
184
+ .fixedSize(horizontal: false, vertical: true)
185
+
186
+ if session.currentProvider.authMode == .apiKey {
158
187
  HStack(spacing: 8) {
159
- TextField(prompt.placeholder ?? prompt.message, text: $session.authPromptInput)
188
+ SecureField(session.currentProvider.tokenPlaceholder, text: $session.authToken)
160
189
  .textFieldStyle(.plain)
161
190
  .font(Typo.mono(11))
162
191
  .foregroundColor(Palette.text)
163
192
  .focused($authFieldFocused)
164
193
  .onSubmit {
165
- session.submitAuthPrompt()
194
+ session.saveSelectedToken()
195
+ }
196
+
197
+ actionChip("SAVE KEY") {
198
+ session.saveSelectedToken()
199
+ }
200
+
201
+ if session.hasSelectedCredential {
202
+ actionChip("CLEAR") {
203
+ session.removeSelectedCredential()
166
204
  }
205
+ }
206
+ }
207
+ .padding(.horizontal, 12)
208
+ .padding(.vertical, 10)
209
+ .background(authCardBackground(tint: Palette.running))
210
+ } else {
211
+ HStack(spacing: 8) {
212
+ actionChip("CONNECT") {
213
+ session.startSelectedAuthFlow()
214
+ }
167
215
 
168
- actionChip("CONTINUE") {
169
- session.submitAuthPrompt()
216
+ if session.hasSelectedCredential {
217
+ actionChip("CLEAR") {
218
+ session.removeSelectedCredential()
219
+ }
170
220
  }
171
221
  }
172
222
  .padding(.horizontal, 12)
173
223
  .padding(.vertical, 10)
174
- .background(authCardBackground(tint: Palette.detach))
224
+ .background(authCardBackground(tint: Palette.running))
175
225
  }
176
226
  }
177
227
 
178
- if let notice = session.authNoticeText, !notice.isEmpty {
228
+ if !session.isAuthenticating, let notice = session.authNoticeText, !notice.isEmpty {
179
229
  Text(notice)
180
230
  .font(Typo.mono(9))
181
231
  .foregroundColor(Palette.textDim)
@@ -192,6 +242,43 @@ struct PiWorkspaceView: View {
192
242
  .padding(.horizontal, 16)
193
243
  .padding(.vertical, 14)
194
244
  .background(Palette.surface.opacity(0.35))
245
+ .onAppear {
246
+ focusAuthFieldIfNeeded()
247
+ }
248
+ .onChange(of: session.authProviderID) { _ in
249
+ focusAuthFieldIfNeeded()
250
+ }
251
+ .onChange(of: session.pendingAuthPrompt?.message) { prompt in
252
+ if prompt != nil {
253
+ focusAuthFieldIfNeeded()
254
+ }
255
+ }
256
+ }
257
+
258
+ private var setupLockedPanel: some View {
259
+ HStack(alignment: .center, spacing: 10) {
260
+ Circle()
261
+ .fill(Palette.detach)
262
+ .frame(width: 7, height: 7)
263
+
264
+ VStack(alignment: .leading, spacing: 3) {
265
+ Text("SETUP IN PROGRESS")
266
+ .font(Typo.geistMonoBold(10))
267
+ .foregroundColor(Palette.text)
268
+
269
+ Text(session.isAuthenticating
270
+ ? "Stay with the setup panel above for now. The chat box unlocks as soon as you finish that step."
271
+ : "Finish the setup panel above to unlock the chat box.")
272
+ .font(Typo.mono(10))
273
+ .foregroundColor(Palette.textDim)
274
+ .fixedSize(horizontal: false, vertical: true)
275
+ }
276
+
277
+ Spacer()
278
+ }
279
+ .padding(.horizontal, 16)
280
+ .padding(.vertical, 14)
281
+ .background(Palette.surface.opacity(0.22))
195
282
  }
196
283
 
197
284
  private var transcript: some View {
@@ -312,6 +399,14 @@ struct PiWorkspaceView: View {
312
399
  }
313
400
  }
314
401
 
402
+ private func focusAuthFieldIfNeeded() {
403
+ if session.currentProvider.authMode == .apiKey || session.pendingAuthPrompt != nil {
404
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
405
+ authFieldFocused = true
406
+ }
407
+ }
408
+ }
409
+
315
410
  private func roleColor(for role: PiChatMessage.Role) -> Color {
316
411
  switch role {
317
412
  case .system: return Palette.detach
@@ -336,11 +431,11 @@ struct PiWorkspaceView: View {
336
431
  )
337
432
  }
338
433
 
339
- private func actionChip(_ label: String, action: @escaping () -> Void) -> some View {
434
+ private func actionChip(_ label: String, tint: Color = Palette.textMuted, disabled: Bool = false, action: @escaping () -> Void) -> some View {
340
435
  Button(label, action: action)
341
436
  .buttonStyle(.plain)
342
437
  .font(Typo.geistMonoBold(9))
343
- .foregroundColor(Palette.textMuted)
438
+ .foregroundColor(disabled ? Palette.textMuted : tint)
344
439
  .padding(.horizontal, 8)
345
440
  .padding(.vertical, 5)
346
441
  .background(
@@ -351,6 +446,8 @@ struct PiWorkspaceView: View {
351
446
  .strokeBorder(Palette.border, lineWidth: 0.5)
352
447
  )
353
448
  )
449
+ .opacity(disabled ? 0.65 : 1)
450
+ .disabled(disabled)
354
451
  }
355
452
 
356
453
  private func authCardBackground(tint: Color) -> some View {
@@ -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.