@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
|
@@ -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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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("
|
|
73
|
+
Text("WORKSPACE CHAT")
|
|
55
74
|
.font(Typo.geistMonoBold(11))
|
|
56
75
|
.foregroundColor(Palette.text)
|
|
57
76
|
|
|
58
|
-
capsuleLabel(
|
|
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(
|
|
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
|
-
|
|
72
|
-
session.
|
|
103
|
+
if session.hasPiBinary && !session.needsProviderSetup {
|
|
104
|
+
actionChip(session.isAuthPanelVisible ? "AUTH -" : "AUTH +") {
|
|
105
|
+
session.toggleAuthPanel()
|
|
106
|
+
}
|
|
73
107
|
}
|
|
74
108
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
135
|
-
.padding(.vertical, 10)
|
|
136
|
-
.background(authCardBackground(tint: Palette.running))
|
|
137
|
-
} else {
|
|
158
|
+
|
|
138
159
|
HStack(spacing: 8) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
169
|
-
|
|
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.
|
|
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,3 +1,4 @@
|
|
|
1
|
+
import DeckKit
|
|
1
2
|
import Foundation
|
|
2
3
|
|
|
3
4
|
enum InteractionMode: String {
|
|
@@ -8,6 +9,11 @@ enum InteractionMode: String {
|
|
|
8
9
|
class Preferences: ObservableObject {
|
|
9
10
|
static let shared = Preferences()
|
|
10
11
|
|
|
12
|
+
private enum CompanionDefaultsKey {
|
|
13
|
+
static let trackpadEnabled = "companion.trackpad.enabled"
|
|
14
|
+
static let cockpitLayout = "companion.cockpit.layout"
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
@Published var terminal: Terminal {
|
|
12
18
|
didSet { UserDefaults.standard.set(terminal.rawValue, forKey: "terminal") }
|
|
13
19
|
}
|
|
@@ -20,6 +26,22 @@ class Preferences: ObservableObject {
|
|
|
20
26
|
didSet { UserDefaults.standard.set(mode.rawValue, forKey: "mode") }
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
@Published var dragSnapEnabled: Bool {
|
|
30
|
+
didSet { UserDefaults.standard.set(dragSnapEnabled, forKey: "windowSnap.enabled") }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Published var companionTrackpadEnabled: Bool {
|
|
34
|
+
didSet { UserDefaults.standard.set(companionTrackpadEnabled, forKey: CompanionDefaultsKey.trackpadEnabled) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Published var companionCockpitLayout: LatticesCompanionCockpitLayout {
|
|
38
|
+
didSet { persistCompanionCockpitLayout() }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Published var mouseGesturesEnabled: Bool {
|
|
42
|
+
didSet { UserDefaults.standard.set(mouseGesturesEnabled, forKey: "mouseGestures.enabled") }
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
// MARK: - AI / Claude
|
|
24
46
|
|
|
25
47
|
@Published var claudePath: String {
|
|
@@ -128,6 +150,25 @@ class Preferences: ObservableObject {
|
|
|
128
150
|
self.mode = .learning
|
|
129
151
|
}
|
|
130
152
|
|
|
153
|
+
if UserDefaults.standard.object(forKey: "windowSnap.enabled") != nil {
|
|
154
|
+
self.dragSnapEnabled = UserDefaults.standard.bool(forKey: "windowSnap.enabled")
|
|
155
|
+
} else {
|
|
156
|
+
self.dragSnapEnabled = true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if UserDefaults.standard.object(forKey: CompanionDefaultsKey.trackpadEnabled) != nil {
|
|
160
|
+
self.companionTrackpadEnabled = UserDefaults.standard.bool(forKey: CompanionDefaultsKey.trackpadEnabled)
|
|
161
|
+
} else {
|
|
162
|
+
self.companionTrackpadEnabled = true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
self.companionCockpitLayout = Self.loadCompanionCockpitLayout()
|
|
166
|
+
|
|
167
|
+
if UserDefaults.standard.object(forKey: "mouseGestures.enabled") != nil {
|
|
168
|
+
self.mouseGesturesEnabled = UserDefaults.standard.bool(forKey: "mouseGestures.enabled")
|
|
169
|
+
} else {
|
|
170
|
+
self.mouseGesturesEnabled = false
|
|
171
|
+
}
|
|
131
172
|
// AI / Claude
|
|
132
173
|
self.claudePath = UserDefaults.standard.string(forKey: "claude.path") ?? ""
|
|
133
174
|
self.advisorModel = UserDefaults.standard.string(forKey: "claude.advisorModel") ?? "haiku"
|
|
@@ -155,4 +196,41 @@ class Preferences: ObservableObject {
|
|
|
155
196
|
let savedAcc = UserDefaults.standard.string(forKey: "ocr.accuracy") ?? "accurate"
|
|
156
197
|
self.ocrAccuracy = savedAcc
|
|
157
198
|
}
|
|
199
|
+
|
|
200
|
+
func updateCompanionCockpitSlot(
|
|
201
|
+
pageID: String,
|
|
202
|
+
index: Int,
|
|
203
|
+
shortcutID: String
|
|
204
|
+
) {
|
|
205
|
+
var normalized = LatticesCompanionCockpitCatalog.normalized(companionCockpitLayout)
|
|
206
|
+
guard let pageIndex = normalized.pages.firstIndex(where: { $0.id == pageID }),
|
|
207
|
+
normalized.pages[pageIndex].slotIDs.indices.contains(index) else {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
normalized.pages[pageIndex].slotIDs[index] = shortcutID
|
|
211
|
+
companionCockpitLayout = normalized
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func resetCompanionCockpitLayout() {
|
|
215
|
+
companionCockpitLayout = LatticesCompanionCockpitCatalog.defaultLayout
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private static func loadCompanionCockpitLayout() -> LatticesCompanionCockpitLayout {
|
|
219
|
+
guard let data = UserDefaults.standard.data(forKey: CompanionDefaultsKey.cockpitLayout),
|
|
220
|
+
let decoded = try? JSONDecoder().decode(LatticesCompanionCockpitLayout.self, from: data) else {
|
|
221
|
+
return LatticesCompanionCockpitCatalog.defaultLayout
|
|
222
|
+
}
|
|
223
|
+
return LatticesCompanionCockpitCatalog.normalized(decoded)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private func persistCompanionCockpitLayout() {
|
|
227
|
+
let normalized = LatticesCompanionCockpitCatalog.normalized(companionCockpitLayout)
|
|
228
|
+
if normalized != companionCockpitLayout {
|
|
229
|
+
companionCockpitLayout = normalized
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
guard let data = try? JSONEncoder().encode(normalized) else { return }
|
|
234
|
+
UserDefaults.standard.set(data, forKey: CompanionDefaultsKey.cockpitLayout)
|
|
235
|
+
}
|
|
158
236
|
}
|