@lattices/cli 0.3.0 → 0.4.0
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 +85 -9
- package/app/Package.swift +8 -1
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +44 -12
- package/app/Sources/AppShellView.swift +81 -8
- package/app/Sources/AudioProvider.swift +386 -0
- package/app/Sources/CheatSheetHUD.swift +261 -19
- package/app/Sources/DaemonProtocol.swift +13 -0
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DesktopModel.swift +164 -5
- package/app/Sources/DesktopModelTypes.swift +2 -0
- package/app/Sources/DiagnosticLog.swift +104 -2
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HUDBottomBar.swift +279 -0
- package/app/Sources/HUDController.swift +1158 -0
- package/app/Sources/HUDLeftBar.swift +849 -0
- package/app/Sources/HUDMinimap.swift +179 -0
- package/app/Sources/HUDRightBar.swift +774 -0
- package/app/Sources/HUDState.swift +367 -0
- package/app/Sources/HUDTopBar.swift +243 -0
- package/app/Sources/HandsOffSession.swift +733 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +45 -9
- package/app/Sources/IntentEngine.swift +925 -0
- package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
- package/app/Sources/Intents/DistributeIntent.swift +56 -0
- package/app/Sources/Intents/FocusIntent.swift +69 -0
- package/app/Sources/Intents/HelpIntent.swift +41 -0
- package/app/Sources/Intents/KillIntent.swift +47 -0
- package/app/Sources/Intents/LatticeIntent.swift +78 -0
- package/app/Sources/Intents/LaunchIntent.swift +67 -0
- package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
- package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
- package/app/Sources/Intents/ScanIntent.swift +52 -0
- package/app/Sources/Intents/SearchIntent.swift +190 -0
- package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
- package/app/Sources/Intents/TileIntent.swift +61 -0
- package/app/Sources/LatticesApi.swift +1235 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- package/app/Sources/OcrModel.swift +34 -1
- package/app/Sources/OmniSearchState.swift +99 -102
- package/app/Sources/OnboardingView.swift +457 -0
- package/app/Sources/PermissionChecker.swift +2 -12
- package/app/Sources/PiChatDock.swift +454 -0
- package/app/Sources/PiChatSession.swift +815 -0
- package/app/Sources/PiWorkspaceView.swift +364 -0
- package/app/Sources/PlacementSpec.swift +195 -0
- package/app/Sources/Preferences.swift +59 -0
- package/app/Sources/ProjectScanner.swift +1 -1
- package/app/Sources/ScreenMapState.swift +701 -55
- package/app/Sources/ScreenMapView.swift +843 -103
- package/app/Sources/ScreenMapWindowController.swift +22 -0
- package/app/Sources/SessionLayerStore.swift +285 -0
- package/app/Sources/SessionManager.swift +4 -1
- package/app/Sources/SettingsView.swift +186 -3
- package/app/Sources/Theme.swift +9 -8
- package/app/Sources/TmuxModel.swift +7 -0
- package/app/Sources/TmuxQuery.swift +27 -3
- package/app/Sources/VoiceChatView.swift +192 -0
- package/app/Sources/VoiceCommandWindow.swift +1594 -0
- package/app/Sources/VoiceIntentResolver.swift +671 -0
- package/app/Sources/VoxClient.swift +454 -0
- package/app/Sources/WindowTiler.swift +348 -87
- package/app/Sources/WorkspaceManager.swift +127 -18
- package/bin/client.ts +16 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +280 -0
- package/bin/handsoff-worker.ts +731 -0
- package/bin/{lattices-app.js → lattices-app.ts} +67 -32
- package/bin/lattices-dev +160 -0
- package/bin/{lattices.js → lattices.ts} +600 -137
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agents.md +142 -0
- package/docs/api.md +153 -34
- package/docs/app.md +29 -1
- package/docs/config.md +5 -1
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/layers.md +20 -20
- package/docs/ocr.md +14 -5
- package/docs/overview.md +5 -1
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +374 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/tiling-reference.md +167 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice.md +219 -0
- package/package.json +21 -10
- package/bin/client.js +0 -4
|
@@ -13,14 +13,14 @@ final class PermissionChecker: ObservableObject {
|
|
|
13
13
|
|
|
14
14
|
var allGranted: Bool { accessibility && screenRecording }
|
|
15
15
|
|
|
16
|
-
/// Check current permission state
|
|
16
|
+
/// Check current permission state without prompting.
|
|
17
17
|
func check() {
|
|
18
18
|
let diag = DiagnosticLog.shared
|
|
19
19
|
|
|
20
20
|
let ax = AXIsProcessTrusted()
|
|
21
21
|
let sr = CGPreflightScreenCaptureAccess()
|
|
22
22
|
|
|
23
|
-
// First check: log identity info
|
|
23
|
+
// First check: log identity info only
|
|
24
24
|
if !hasLoggedInitial {
|
|
25
25
|
hasLoggedInitial = true
|
|
26
26
|
let bundleId = Bundle.main.bundleIdentifier ?? "<no bundle id>"
|
|
@@ -30,16 +30,6 @@ final class PermissionChecker: ObservableObject {
|
|
|
30
30
|
diag.info("PermissionChecker: exec=\(execPath)")
|
|
31
31
|
diag.info("AXIsProcessTrusted() → \(ax)")
|
|
32
32
|
diag.info("CGPreflightScreenCaptureAccess() → \(sr)")
|
|
33
|
-
|
|
34
|
-
// Prompt for missing permissions on first check
|
|
35
|
-
if !ax {
|
|
36
|
-
requestAccessibility()
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
if !sr {
|
|
40
|
-
requestScreenRecording()
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
33
|
}
|
|
44
34
|
|
|
45
35
|
// Log on state changes
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
struct PiChatDock: View {
|
|
4
|
+
@ObservedObject var session: PiChatSession
|
|
5
|
+
@FocusState private var composerFocused: Bool
|
|
6
|
+
@FocusState private var authFieldFocused: Bool
|
|
7
|
+
@State private var resizeStartHeight: CGFloat?
|
|
8
|
+
|
|
9
|
+
private static let timeFormatter: DateFormatter = {
|
|
10
|
+
let formatter = DateFormatter()
|
|
11
|
+
formatter.dateFormat = "HH:mm"
|
|
12
|
+
return formatter
|
|
13
|
+
}()
|
|
14
|
+
|
|
15
|
+
var body: some View {
|
|
16
|
+
VStack(spacing: 0) {
|
|
17
|
+
topHandle
|
|
18
|
+
|
|
19
|
+
if session.isAuthPanelVisible {
|
|
20
|
+
Rectangle()
|
|
21
|
+
.fill(Palette.border)
|
|
22
|
+
.frame(height: 0.5)
|
|
23
|
+
|
|
24
|
+
authPanel
|
|
25
|
+
|
|
26
|
+
Rectangle()
|
|
27
|
+
.fill(Palette.border)
|
|
28
|
+
.frame(height: 0.5)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
transcript
|
|
32
|
+
|
|
33
|
+
Rectangle()
|
|
34
|
+
.fill(Palette.border)
|
|
35
|
+
.frame(height: 0.5)
|
|
36
|
+
|
|
37
|
+
composer
|
|
38
|
+
|
|
39
|
+
Rectangle()
|
|
40
|
+
.fill(Palette.border)
|
|
41
|
+
.frame(height: 0.5)
|
|
42
|
+
|
|
43
|
+
footerBar
|
|
44
|
+
}
|
|
45
|
+
.frame(maxWidth: .infinity)
|
|
46
|
+
.frame(height: session.dockHeight)
|
|
47
|
+
.background(
|
|
48
|
+
LinearGradient(
|
|
49
|
+
colors: [
|
|
50
|
+
Color.black.opacity(0.96),
|
|
51
|
+
Color(red: 0.02, green: 0.05, blue: 0.03),
|
|
52
|
+
],
|
|
53
|
+
startPoint: .top,
|
|
54
|
+
endPoint: .bottom
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
.overlay(
|
|
58
|
+
RoundedRectangle(cornerRadius: 0)
|
|
59
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
60
|
+
)
|
|
61
|
+
.onAppear {
|
|
62
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
63
|
+
composerFocused = true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private var topHandle: some View {
|
|
69
|
+
HStack {
|
|
70
|
+
Spacer()
|
|
71
|
+
|
|
72
|
+
Capsule()
|
|
73
|
+
.fill(Palette.borderLit)
|
|
74
|
+
.frame(width: 64, height: 4)
|
|
75
|
+
|
|
76
|
+
Spacer()
|
|
77
|
+
|
|
78
|
+
Button {
|
|
79
|
+
session.isVisible = false
|
|
80
|
+
} label: {
|
|
81
|
+
Image(systemName: "xmark")
|
|
82
|
+
.font(.system(size: 9, weight: .bold))
|
|
83
|
+
.foregroundColor(Palette.textMuted)
|
|
84
|
+
.padding(6)
|
|
85
|
+
.background(
|
|
86
|
+
Circle()
|
|
87
|
+
.fill(Color.white.opacity(0.03))
|
|
88
|
+
.overlay(
|
|
89
|
+
Circle()
|
|
90
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
.buttonStyle(.plain)
|
|
95
|
+
}
|
|
96
|
+
.padding(.horizontal, 10)
|
|
97
|
+
.padding(.vertical, 8)
|
|
98
|
+
.contentShape(Rectangle())
|
|
99
|
+
.gesture(
|
|
100
|
+
DragGesture(minimumDistance: 1)
|
|
101
|
+
.onChanged { value in
|
|
102
|
+
if resizeStartHeight == nil {
|
|
103
|
+
resizeStartHeight = session.dockHeight
|
|
104
|
+
}
|
|
105
|
+
let start = resizeStartHeight ?? session.dockHeight
|
|
106
|
+
session.dockHeight = start - value.translation.height
|
|
107
|
+
}
|
|
108
|
+
.onEnded { _ in
|
|
109
|
+
resizeStartHeight = nil
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private var authPanel: some View {
|
|
115
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
116
|
+
HStack(spacing: 8) {
|
|
117
|
+
Text("provider")
|
|
118
|
+
.font(Typo.geistMonoBold(9))
|
|
119
|
+
.foregroundColor(Palette.textMuted)
|
|
120
|
+
|
|
121
|
+
Picker("Provider", selection: $session.authProviderID) {
|
|
122
|
+
ForEach(session.providerOptions) { provider in
|
|
123
|
+
Text(provider.name).tag(provider.id)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
.labelsHidden()
|
|
127
|
+
.pickerStyle(.menu)
|
|
128
|
+
.font(Typo.mono(10))
|
|
129
|
+
|
|
130
|
+
Spacer()
|
|
131
|
+
|
|
132
|
+
capsuleLabel(
|
|
133
|
+
session.currentProvider.authMode == .oauth ? "OAUTH" : "TOKEN",
|
|
134
|
+
tint: session.currentProvider.authMode == .oauth ? Palette.detach : Palette.running
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Text(session.currentProvider.helpText)
|
|
139
|
+
.font(Typo.mono(10))
|
|
140
|
+
.foregroundColor(Palette.textDim)
|
|
141
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
142
|
+
|
|
143
|
+
if session.currentProvider.authMode == .apiKey {
|
|
144
|
+
HStack(spacing: 8) {
|
|
145
|
+
SecureField(session.currentProvider.tokenPlaceholder, text: $session.authToken)
|
|
146
|
+
.textFieldStyle(.plain)
|
|
147
|
+
.font(Typo.mono(11))
|
|
148
|
+
.foregroundColor(Palette.text)
|
|
149
|
+
.focused($authFieldFocused)
|
|
150
|
+
.onSubmit {
|
|
151
|
+
session.saveSelectedToken()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
footerButton("save") {
|
|
155
|
+
session.saveSelectedToken()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if session.hasSelectedCredential {
|
|
159
|
+
footerButton("clear") {
|
|
160
|
+
session.removeSelectedCredential()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
.padding(.horizontal, 10)
|
|
165
|
+
.padding(.vertical, 8)
|
|
166
|
+
.background(authCardBackground(tint: Palette.running))
|
|
167
|
+
} else {
|
|
168
|
+
HStack(spacing: 8) {
|
|
169
|
+
footerButton(session.isAuthenticating ? "cancel" : "login") {
|
|
170
|
+
if session.isAuthenticating {
|
|
171
|
+
session.cancelAuthFlow()
|
|
172
|
+
} else {
|
|
173
|
+
session.startSelectedAuthFlow()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if session.hasSelectedCredential {
|
|
178
|
+
footerButton("clear") {
|
|
179
|
+
session.removeSelectedCredential()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
.padding(.horizontal, 10)
|
|
184
|
+
.padding(.vertical, 8)
|
|
185
|
+
.background(authCardBackground(tint: session.isAuthenticating ? Palette.detach : Palette.running))
|
|
186
|
+
|
|
187
|
+
if let prompt = session.pendingAuthPrompt {
|
|
188
|
+
HStack(spacing: 8) {
|
|
189
|
+
TextField(prompt.placeholder ?? prompt.message, text: $session.authPromptInput)
|
|
190
|
+
.textFieldStyle(.plain)
|
|
191
|
+
.font(Typo.mono(11))
|
|
192
|
+
.foregroundColor(Palette.text)
|
|
193
|
+
.focused($authFieldFocused)
|
|
194
|
+
.onSubmit {
|
|
195
|
+
session.submitAuthPrompt()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
footerButton("continue") {
|
|
199
|
+
session.submitAuthPrompt()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
.padding(.horizontal, 10)
|
|
203
|
+
.padding(.vertical, 8)
|
|
204
|
+
.background(authCardBackground(tint: Palette.detach))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if let notice = session.authNoticeText, !notice.isEmpty {
|
|
209
|
+
Text(notice)
|
|
210
|
+
.font(Typo.mono(9))
|
|
211
|
+
.foregroundColor(Palette.textDim)
|
|
212
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if let error = session.authErrorText, !error.isEmpty {
|
|
216
|
+
Text(error)
|
|
217
|
+
.font(Typo.mono(9))
|
|
218
|
+
.foregroundColor(Palette.kill)
|
|
219
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
.padding(.horizontal, 10)
|
|
223
|
+
.padding(.vertical, 10)
|
|
224
|
+
.background(
|
|
225
|
+
LinearGradient(
|
|
226
|
+
colors: [
|
|
227
|
+
Palette.running.opacity(0.07),
|
|
228
|
+
Color.black.opacity(0.26),
|
|
229
|
+
],
|
|
230
|
+
startPoint: .topLeading,
|
|
231
|
+
endPoint: .bottomTrailing
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
.onAppear {
|
|
235
|
+
focusAuthFieldIfNeeded()
|
|
236
|
+
}
|
|
237
|
+
.onChange(of: session.authProviderID) { _ in
|
|
238
|
+
focusAuthFieldIfNeeded()
|
|
239
|
+
}
|
|
240
|
+
.onChange(of: session.pendingAuthPrompt?.message) { prompt in
|
|
241
|
+
if prompt != nil {
|
|
242
|
+
focusAuthFieldIfNeeded()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private var transcript: some View {
|
|
248
|
+
ScrollViewReader { proxy in
|
|
249
|
+
ScrollView(.vertical, showsIndicators: true) {
|
|
250
|
+
LazyVStack(alignment: .leading, spacing: 8) {
|
|
251
|
+
ForEach(session.messages) { message in
|
|
252
|
+
row(message)
|
|
253
|
+
.id(message.id)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
.padding(.horizontal, 10)
|
|
257
|
+
.padding(.vertical, 10)
|
|
258
|
+
}
|
|
259
|
+
.background(Color.black.opacity(0.35))
|
|
260
|
+
.onAppear {
|
|
261
|
+
if let last = session.messages.last?.id {
|
|
262
|
+
proxy.scrollTo(last, anchor: .bottom)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
.onChange(of: session.messages.count) { _ in
|
|
266
|
+
if let last = session.messages.last?.id {
|
|
267
|
+
withAnimation(.easeOut(duration: 0.15)) {
|
|
268
|
+
proxy.scrollTo(last, anchor: .bottom)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private func row(_ message: PiChatMessage) -> some View {
|
|
276
|
+
HStack(alignment: .top, spacing: 10) {
|
|
277
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
278
|
+
capsuleLabel(roleLabel(for: message.role).uppercased(), tint: roleColor(for: message.role))
|
|
279
|
+
|
|
280
|
+
Text(timestamp(for: message.timestamp))
|
|
281
|
+
.font(Typo.mono(8))
|
|
282
|
+
.foregroundColor(Palette.textMuted)
|
|
283
|
+
}
|
|
284
|
+
.frame(width: 52, alignment: .leading)
|
|
285
|
+
|
|
286
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
287
|
+
Rectangle()
|
|
288
|
+
.fill(roleColor(for: message.role).opacity(0.9))
|
|
289
|
+
.frame(width: 14, height: 1.5)
|
|
290
|
+
|
|
291
|
+
Text(message.text)
|
|
292
|
+
.font(Typo.mono(11))
|
|
293
|
+
.foregroundColor(Palette.text)
|
|
294
|
+
.textSelection(.enabled)
|
|
295
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
.padding(.horizontal, 10)
|
|
299
|
+
.padding(.vertical, 9)
|
|
300
|
+
.background(
|
|
301
|
+
RoundedRectangle(cornerRadius: 6)
|
|
302
|
+
.fill(roleColor(for: message.role).opacity(message.role == .assistant ? 0.11 : 0.06))
|
|
303
|
+
.overlay(
|
|
304
|
+
RoundedRectangle(cornerRadius: 6)
|
|
305
|
+
.strokeBorder(roleColor(for: message.role).opacity(0.22), lineWidth: 0.5)
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private var composer: some View {
|
|
311
|
+
HStack(spacing: 10) {
|
|
312
|
+
HStack(spacing: 8) {
|
|
313
|
+
Text(">")
|
|
314
|
+
.font(Typo.geistMonoBold(11))
|
|
315
|
+
.foregroundColor(Palette.running)
|
|
316
|
+
|
|
317
|
+
TextField("Ask Pi something lightweight...", text: $session.draft, axis: .vertical)
|
|
318
|
+
.textFieldStyle(.plain)
|
|
319
|
+
.font(Typo.mono(11))
|
|
320
|
+
.foregroundColor(Palette.text)
|
|
321
|
+
.lineLimit(1...4)
|
|
322
|
+
.focused($composerFocused)
|
|
323
|
+
.onSubmit {
|
|
324
|
+
session.sendDraft()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
.padding(.horizontal, 10)
|
|
328
|
+
.padding(.vertical, 9)
|
|
329
|
+
.background(
|
|
330
|
+
RoundedRectangle(cornerRadius: 6)
|
|
331
|
+
.fill(Color.black.opacity(0.38))
|
|
332
|
+
.overlay(
|
|
333
|
+
RoundedRectangle(cornerRadius: 6)
|
|
334
|
+
.strokeBorder(Palette.running.opacity(0.16), lineWidth: 0.5)
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
footerButton("send", tint: Palette.running) {
|
|
339
|
+
session.sendDraft()
|
|
340
|
+
}
|
|
341
|
+
.disabled(session.isSending)
|
|
342
|
+
}
|
|
343
|
+
.padding(.horizontal, 10)
|
|
344
|
+
.padding(.vertical, 10)
|
|
345
|
+
.background(Color.black.opacity(0.62))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private var footerBar: some View {
|
|
349
|
+
HStack(spacing: 8) {
|
|
350
|
+
Circle()
|
|
351
|
+
.fill(session.hasPiBinary ? Palette.running : Palette.kill)
|
|
352
|
+
.frame(width: 6, height: 6)
|
|
353
|
+
|
|
354
|
+
Text("PI DOCK")
|
|
355
|
+
.font(Typo.geistMonoBold(9))
|
|
356
|
+
.foregroundColor(Palette.text)
|
|
357
|
+
|
|
358
|
+
Text(footerStatusText)
|
|
359
|
+
.font(Typo.mono(9))
|
|
360
|
+
.foregroundColor(Palette.textMuted)
|
|
361
|
+
.lineLimit(1)
|
|
362
|
+
|
|
363
|
+
Spacer()
|
|
364
|
+
|
|
365
|
+
footerButton(session.isAuthPanelVisible ? "auth -" : "auth +") {
|
|
366
|
+
session.toggleAuthPanel()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
footerButton("reset") {
|
|
370
|
+
session.clearConversation()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
.padding(.horizontal, 10)
|
|
374
|
+
.padding(.vertical, 7)
|
|
375
|
+
.background(Color.white.opacity(0.015))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private var footerStatusText: String {
|
|
379
|
+
if session.statusText == "idle" {
|
|
380
|
+
return session.currentProvider.name
|
|
381
|
+
}
|
|
382
|
+
return "\(session.currentProvider.name) · \(session.statusText)"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private func focusAuthFieldIfNeeded() {
|
|
386
|
+
if session.currentProvider.authMode == .apiKey || session.pendingAuthPrompt != nil {
|
|
387
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
388
|
+
authFieldFocused = true
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private func roleLabel(for role: PiChatMessage.Role) -> String {
|
|
394
|
+
switch role {
|
|
395
|
+
case .system: return "system"
|
|
396
|
+
case .user: return "you"
|
|
397
|
+
case .assistant: return "pi"
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private func roleColor(for role: PiChatMessage.Role) -> Color {
|
|
402
|
+
switch role {
|
|
403
|
+
case .system: return Palette.detach
|
|
404
|
+
case .user: return Palette.textDim
|
|
405
|
+
case .assistant: return Palette.running
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private func timestamp(for date: Date) -> String {
|
|
410
|
+
Self.timeFormatter.string(from: date)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private func capsuleLabel(_ text: String, tint: Color) -> some View {
|
|
414
|
+
Text(text)
|
|
415
|
+
.font(Typo.geistMonoBold(9))
|
|
416
|
+
.foregroundColor(tint.opacity(0.95))
|
|
417
|
+
.padding(.horizontal, 7)
|
|
418
|
+
.padding(.vertical, 4)
|
|
419
|
+
.background(
|
|
420
|
+
Capsule()
|
|
421
|
+
.fill(tint.opacity(0.10))
|
|
422
|
+
.overlay(
|
|
423
|
+
Capsule()
|
|
424
|
+
.strokeBorder(tint.opacity(0.28), lineWidth: 0.5)
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private func footerButton(_ label: String, tint: Color = Palette.textMuted, action: @escaping () -> Void) -> some View {
|
|
430
|
+
Button(label, action: action)
|
|
431
|
+
.buttonStyle(.plain)
|
|
432
|
+
.font(Typo.geistMonoBold(9))
|
|
433
|
+
.foregroundColor(tint)
|
|
434
|
+
.padding(.horizontal, 8)
|
|
435
|
+
.padding(.vertical, 5)
|
|
436
|
+
.background(
|
|
437
|
+
Capsule()
|
|
438
|
+
.fill(Color.white.opacity(0.03))
|
|
439
|
+
.overlay(
|
|
440
|
+
Capsule()
|
|
441
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private func authCardBackground(tint: Color) -> some View {
|
|
447
|
+
RoundedRectangle(cornerRadius: 6)
|
|
448
|
+
.fill(tint.opacity(0.06))
|
|
449
|
+
.overlay(
|
|
450
|
+
RoundedRectangle(cornerRadius: 6)
|
|
451
|
+
.strokeBorder(tint.opacity(0.24), lineWidth: 0.5)
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
}
|