@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.
Files changed (95) hide show
  1. package/README.md +85 -9
  2. package/app/Package.swift +8 -1
  3. package/app/Sources/AdvisorLearningStore.swift +90 -0
  4. package/app/Sources/AgentSession.swift +377 -0
  5. package/app/Sources/AppDelegate.swift +44 -12
  6. package/app/Sources/AppShellView.swift +81 -8
  7. package/app/Sources/AudioProvider.swift +386 -0
  8. package/app/Sources/CheatSheetHUD.swift +261 -19
  9. package/app/Sources/DaemonProtocol.swift +13 -0
  10. package/app/Sources/DaemonServer.swift +8 -0
  11. package/app/Sources/DesktopModel.swift +164 -5
  12. package/app/Sources/DesktopModelTypes.swift +2 -0
  13. package/app/Sources/DiagnosticLog.swift +104 -2
  14. package/app/Sources/EventBus.swift +1 -0
  15. package/app/Sources/HUDBottomBar.swift +279 -0
  16. package/app/Sources/HUDController.swift +1158 -0
  17. package/app/Sources/HUDLeftBar.swift +849 -0
  18. package/app/Sources/HUDMinimap.swift +179 -0
  19. package/app/Sources/HUDRightBar.swift +774 -0
  20. package/app/Sources/HUDState.swift +367 -0
  21. package/app/Sources/HUDTopBar.swift +243 -0
  22. package/app/Sources/HandsOffSession.swift +733 -0
  23. package/app/Sources/HomeDashboardView.swift +125 -0
  24. package/app/Sources/HotkeyManager.swift +2 -0
  25. package/app/Sources/HotkeyStore.swift +45 -9
  26. package/app/Sources/IntentEngine.swift +925 -0
  27. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  28. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  29. package/app/Sources/Intents/FocusIntent.swift +69 -0
  30. package/app/Sources/Intents/HelpIntent.swift +41 -0
  31. package/app/Sources/Intents/KillIntent.swift +47 -0
  32. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  33. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  34. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  35. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  36. package/app/Sources/Intents/ScanIntent.swift +52 -0
  37. package/app/Sources/Intents/SearchIntent.swift +190 -0
  38. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  39. package/app/Sources/Intents/TileIntent.swift +61 -0
  40. package/app/Sources/LatticesApi.swift +1235 -30
  41. package/app/Sources/LauncherHUD.swift +348 -0
  42. package/app/Sources/MainView.swift +147 -44
  43. package/app/Sources/OcrModel.swift +34 -1
  44. package/app/Sources/OmniSearchState.swift +99 -102
  45. package/app/Sources/OnboardingView.swift +457 -0
  46. package/app/Sources/PermissionChecker.swift +2 -12
  47. package/app/Sources/PiChatDock.swift +454 -0
  48. package/app/Sources/PiChatSession.swift +815 -0
  49. package/app/Sources/PiWorkspaceView.swift +364 -0
  50. package/app/Sources/PlacementSpec.swift +195 -0
  51. package/app/Sources/Preferences.swift +59 -0
  52. package/app/Sources/ProjectScanner.swift +1 -1
  53. package/app/Sources/ScreenMapState.swift +701 -55
  54. package/app/Sources/ScreenMapView.swift +843 -103
  55. package/app/Sources/ScreenMapWindowController.swift +22 -0
  56. package/app/Sources/SessionLayerStore.swift +285 -0
  57. package/app/Sources/SessionManager.swift +4 -1
  58. package/app/Sources/SettingsView.swift +186 -3
  59. package/app/Sources/Theme.swift +9 -8
  60. package/app/Sources/TmuxModel.swift +7 -0
  61. package/app/Sources/TmuxQuery.swift +27 -3
  62. package/app/Sources/VoiceChatView.swift +192 -0
  63. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  64. package/app/Sources/VoiceIntentResolver.swift +671 -0
  65. package/app/Sources/VoxClient.swift +454 -0
  66. package/app/Sources/WindowTiler.swift +348 -87
  67. package/app/Sources/WorkspaceManager.swift +127 -18
  68. package/bin/client.ts +16 -0
  69. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  70. package/bin/handsoff-infer.ts +280 -0
  71. package/bin/handsoff-worker.ts +731 -0
  72. package/bin/{lattices-app.js → lattices-app.ts} +67 -32
  73. package/bin/lattices-dev +160 -0
  74. package/bin/{lattices.js → lattices.ts} +600 -137
  75. package/bin/project-twin.ts +645 -0
  76. package/docs/agent-execution-plan.md +562 -0
  77. package/docs/agents.md +142 -0
  78. package/docs/api.md +153 -34
  79. package/docs/app.md +29 -1
  80. package/docs/config.md +5 -1
  81. package/docs/handsoff-test-scenarios.md +84 -0
  82. package/docs/layers.md +20 -20
  83. package/docs/ocr.md +14 -5
  84. package/docs/overview.md +5 -1
  85. package/docs/presentation-execution-review.md +491 -0
  86. package/docs/prompts/hands-off-system.md +374 -0
  87. package/docs/prompts/hands-off-turn.md +30 -0
  88. package/docs/prompts/voice-advisor.md +31 -0
  89. package/docs/prompts/voice-fallback.md +23 -0
  90. package/docs/tiling-reference.md +167 -0
  91. package/docs/twins.md +138 -0
  92. package/docs/voice-command-protocol.md +278 -0
  93. package/docs/voice.md +219 -0
  94. package/package.json +21 -10
  95. package/bin/client.js +0 -4
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Hands-off inference script — called by HandsOffSession.swift.
4
+ *
5
+ * Usage: echo '{"transcript":"tile chrome left","snapshot":{...}}' | bun run bin/handsoff-infer.ts
6
+ *
7
+ * Reads JSON from stdin, calls Groq via lib/infer.ts, prints JSON result to stdout.
8
+ * All logging goes to stderr so it doesn't pollute the JSON output.
9
+ */
10
+
11
+ import { inferJSON } from "../lib/infer.ts";
12
+ import { readFileSync } from "fs";
13
+ import { join, dirname } from "path";
14
+ import { homedir } from "os";
15
+
16
+ // ── Read input from stdin ──────────────────────────────────────────
17
+
18
+ const input = await Bun.stdin.text();
19
+ const req = JSON.parse(input) as {
20
+ transcript: string;
21
+ snapshot: {
22
+ stageManager?: boolean;
23
+ smGrouping?: string;
24
+ activeStage?: Array<{ wid: number; app: string; title: string; frame: string }>;
25
+ stripApps?: string[];
26
+ hiddenApps?: string[];
27
+ currentLayer?: string;
28
+ screen?: string;
29
+ };
30
+ history?: Array<{ role: "user" | "assistant"; content: string }>;
31
+ };
32
+
33
+ // ── Load system prompt from file ───────────────────────────────────
34
+
35
+ const promptDir = join(dirname(import.meta.dir), "docs", "prompts");
36
+ let systemPrompt: string;
37
+ try {
38
+ systemPrompt = readFileSync(join(promptDir, "hands-off-system.md"), "utf-8")
39
+ .split("\n")
40
+ .filter((l) => !l.startsWith("# "))
41
+ .join("\n")
42
+ .trim();
43
+ } catch {
44
+ systemPrompt = "You are a workspace assistant. Respond with JSON: {actions, spoken}.";
45
+ }
46
+
47
+ // Replace {{intent_catalog}} with the actual tiling reference
48
+ const intentCatalog = `
49
+ tile_window: Tile a window to a screen position
50
+ Slots:
51
+ position (required): Named position or grid:CxR:C,R syntax.
52
+ Halves: left, right, top, bottom
53
+ Quarters (2x2): top-left, top-right, bottom-left, bottom-right
54
+ Thirds (3x1): left-third, center-third, right-third
55
+ Sixths (3x2): top-left-third, top-center-third, top-right-third, bottom-left-third, bottom-center-third, bottom-right-third
56
+ Fourths (4x1): first-fourth, second-fourth, third-fourth, last-fourth
57
+ Eighths (4x2): top-first-fourth, top-second-fourth, top-third-fourth, top-last-fourth, bottom-first-fourth, bottom-second-fourth, bottom-third-fourth, bottom-last-fourth
58
+ Special: maximize (full screen), center (centered floating)
59
+ Grid syntax: grid:CxR:C,R (e.g. grid:5x3:2,1 = center cell of 5x3 grid)
60
+ app (optional): Target app name — match loosely (e.g. "chrome" matches "Google Chrome")
61
+ wid (optional): Target window ID (from snapshot)
62
+ session (optional): Tmux session name
63
+ If no app/wid/session given, tiles the frontmost window.
64
+ "quarter" = 2x2 cell (top-left etc.), NOT a 4x1 fourth.
65
+ "top quarter" = top-left or top-right (2x2). "top third" = top-left-third (3x2).
66
+ Examples: "tile chrome left" → {intent:"tile_window", slots:{app:"chrome", position:"left"}}
67
+
68
+ focus: Focus a window, app, or session
69
+ Slots:
70
+ app (optional): App name to focus
71
+ session (optional): Session name to focus
72
+ wid (optional): Window ID to focus
73
+
74
+ distribute: Arrange windows in an even grid — with optional app filter and region constraint
75
+ Slots:
76
+ app (optional): Filter to windows of this app (e.g. "iTerm2", "Google Chrome"). Without this, distributes ALL visible windows.
77
+ region (optional): Constrain the grid to a screen region. Uses the same position names as tile_window:
78
+ Halves: left, right, top, bottom
79
+ Quarters: top-left, top-right, bottom-left, bottom-right
80
+ Thirds: left-third, center-third, right-third
81
+ Without this, uses the full screen.
82
+ Examples:
83
+ "grid the terminals on the right" → {intent:"distribute", slots:{app:"iTerm2", region:"right"}}
84
+ "organize my chrome windows in the bottom half" → {intent:"distribute", slots:{app:"Google Chrome", region:"bottom"}}
85
+ "spread everything out" → {intent:"distribute", slots:{}}
86
+ "tile all terminals" → {intent:"distribute", slots:{app:"iTerm2"}}
87
+
88
+ swap: Swap the positions of two windows
89
+ Slots:
90
+ wid_a (required): Window ID of the first window (from snapshot)
91
+ wid_b (required): Window ID of the second window (from snapshot)
92
+ Examples:
93
+ "swap Chrome and iTerm" → {intent:"swap", slots:{wid_a:12345, wid_b:67890}}
94
+
95
+ hide: Hide or minimize a window or app
96
+ Slots:
97
+ app (optional): App name to hide (hides the entire app)
98
+ wid (optional): Window ID to minimize (minimizes just that window)
99
+ Use app to hide all windows of an app. Use wid to minimize a single window.
100
+ Examples:
101
+ "hide Slack" → {intent:"hide", slots:{app:"Slack"}}
102
+ "minimize that" → {intent:"hide", slots:{wid:12345}}
103
+
104
+ highlight: Flash a window's border to identify it visually
105
+ Slots:
106
+ wid (optional): Window ID to highlight (from snapshot)
107
+ app (optional): App name to highlight
108
+ Use when the user asks "which one is that?" or wants to visually identify a window.
109
+ Examples:
110
+ "show me the lattices terminal" → {intent:"highlight", slots:{wid:12345}}
111
+ "which one is Chrome?" → {intent:"highlight", slots:{app:"Google Chrome"}}
112
+
113
+ move_to_display: Move a window to another monitor/display
114
+ Slots:
115
+ display (required): Target display index (0 = main/primary, 1 = second, etc.)
116
+ wid (optional): Window ID to move (from snapshot)
117
+ app (optional): App name to move
118
+ position (optional): Tile position on the target display (e.g. "left", "maximize")
119
+ If no wid/app given, moves the frontmost window.
120
+ Examples:
121
+ "put this on my second monitor" → {intent:"move_to_display", slots:{wid:12345, display:1}}
122
+ "move Chrome to the main screen" → {intent:"move_to_display", slots:{app:"Google Chrome", display:0}}
123
+ "send iTerm to the other monitor, left half" → {intent:"move_to_display", slots:{app:"iTerm2", display:1, position:"left"}}
124
+
125
+ undo: Undo the last window move — restore windows to their previous positions
126
+ No slots needed.
127
+ Examples:
128
+ "put it back" → {intent:"undo"}
129
+ "undo that" → {intent:"undo"}
130
+
131
+ search: Search windows by text
132
+ Slots:
133
+ query (required): Search text
134
+ Examples:
135
+ "find the error message" → {intent:"search", slots:{query:"error"}}
136
+ "find all terminal windows" → {intent:"search", slots:{query:"terminal"}}
137
+
138
+ list_windows: List all visible windows
139
+ No slots needed. Use when the user asks "what's on screen?" or "what windows do I have?"
140
+
141
+ list_sessions: List active terminal sessions
142
+ No slots needed. Use when the user asks "what sessions are running?" or "show my projects."
143
+
144
+ switch_layer: Switch to a workspace layer
145
+ Slots:
146
+ layer (required): Layer name or index
147
+ Examples:
148
+ "switch to the web layer" → {intent:"switch_layer", slots:{layer:"web"}}
149
+ "go to layer 2" → {intent:"switch_layer", slots:{layer:"2"}}
150
+
151
+ create_layer: Save current window arrangement as a named layer
152
+ Slots:
153
+ name (required): Layer name
154
+ Examples:
155
+ "save this layout as review" → {intent:"create_layer", slots:{name:"review"}}
156
+
157
+ launch: Launch a project session
158
+ Slots:
159
+ project (required): Project name or path
160
+ Examples:
161
+ "open my frontend project" → {intent:"launch", slots:{project:"frontend"}}
162
+ "start working on lattices" → {intent:"launch", slots:{project:"lattices"}}
163
+
164
+ kill: Kill a terminal session
165
+ Slots:
166
+ session (required): Session name or project name
167
+ Examples:
168
+ "stop the frontend session" → {intent:"kill", slots:{session:"frontend"}}
169
+
170
+ scan: Trigger an immediate screen text scan (OCR)
171
+ No slots needed. Use when the user asks you to read or scan screen content.
172
+
173
+ CHOOSING THE RIGHT INTENT:
174
+ Positioning:
175
+ tile_window = position ONE specific window at a specific spot. Use for 1-6 named windows.
176
+ distribute = auto-grid MANY windows. Use when the user says "all", "my terminals", "everything", or names more windows than the 6-action limit.
177
+ distribute with app+region is the most powerful combo: "grid my terminals on the right" → distribute(app:"iTerm2", region:"right")
178
+ Rearranging:
179
+ swap = exchange positions of exactly two windows. "swap Chrome and iTerm"
180
+ move_to_display = move a window to a different monitor. "put this on my other screen"
181
+ Visibility:
182
+ hide = hide an app or minimize a window. "hide Slack", "minimize that"
183
+ highlight = flash a window's border to identify it. "which one is the lattices terminal?"
184
+ focus = bring a window to the front. "focus Slack", "show me Chrome"
185
+ Recovery:
186
+ undo = restore previous positions after a move. "put it back", "undo that"
187
+ Information:
188
+ list_windows, list_sessions, search = answer questions about the desktop. NO actions needed for pure questions.
189
+ Session lifecycle:
190
+ launch = start a project session. "open my frontend project"
191
+ kill = stop a session. "kill the API"
192
+
193
+ TILING PRESETS (use multiple tile_window actions):
194
+ "split screen" / "side by side" → left + right
195
+ "thirds" → left-third, center-third, right-third
196
+ "main + sidebar" → main app left (or maximize), others stacked right
197
+ "stack" → top + bottom
198
+ "corners" / "quadrants" → top-left, top-right, bottom-left, bottom-right
199
+ "six-up" / "3 by 2" → 3x2 grid using sixth positions
200
+ "eight-up" / "4 by 2" → 4x2 grid using eighth positions
201
+
202
+ TILING PRESETS (use distribute intent):
203
+ "mosaic" / "grid" / "spread out" → distribute (all windows, full screen)
204
+ "grid the terminals" → distribute with app:"iTerm2"
205
+ "terminals on the right" → distribute with app:"iTerm2", region:"right"
206
+ "organize chrome on the left" → distribute with app:"Google Chrome", region:"left"
207
+ `;
208
+
209
+ systemPrompt = systemPrompt.replace("{{intent_catalog}}", intentCatalog);
210
+
211
+ // ── Build the per-turn message ─────────────────────────────────────
212
+
213
+ let userMessage = `USER: "${req.transcript}"\n\n`;
214
+ userMessage += "--- DESKTOP SNAPSHOT ---\n";
215
+
216
+ const snap = req.snapshot;
217
+ if (snap.stageManager) {
218
+ userMessage += `Stage Manager: ON (grouping: ${snap.smGrouping ?? "all-at-once"})\n\n`;
219
+ userMessage += `Active stage (${snap.activeStage?.length ?? 0} windows):\n`;
220
+ for (const w of snap.activeStage ?? []) {
221
+ userMessage += ` [${w.wid}] ${w.app}: "${w.title}" — ${w.frame}\n`;
222
+ }
223
+ userMessage += `\nStrip: ${snap.stripApps?.join(", ") ?? "none"}\n`;
224
+ userMessage += `Other stages: ${snap.hiddenApps?.join(", ") ?? "none"}\n`;
225
+ } else {
226
+ userMessage += "Stage Manager: OFF\n";
227
+ userMessage += `Visible windows (${snap.activeStage?.length ?? 0}):\n`;
228
+ for (const w of snap.activeStage ?? []) {
229
+ userMessage += ` [${w.wid}] ${w.app}: "${w.title}" — ${w.frame}\n`;
230
+ }
231
+ }
232
+
233
+ if (snap.currentLayer) {
234
+ userMessage += `\nCurrent layer: ${snap.currentLayer}\n`;
235
+ }
236
+ if (snap.screen) {
237
+ userMessage += `Screen: ${snap.screen}\n`;
238
+ }
239
+ userMessage += "--- END SNAPSHOT ---\n";
240
+
241
+ // ── Call inference ──────────────────────────────────────────────────
242
+
243
+ const messages = (req.history ?? []).map((h) => ({
244
+ role: h.role as "user" | "assistant",
245
+ content: h.content,
246
+ }));
247
+
248
+ try {
249
+ const { data, raw } = await inferJSON(userMessage, {
250
+ provider: "groq",
251
+ model: "llama-3.3-70b-versatile",
252
+ system: systemPrompt,
253
+ messages,
254
+ temperature: 0.2,
255
+ maxTokens: 512,
256
+ tag: "hands-off",
257
+ });
258
+
259
+ // Output result as JSON to stdout
260
+ const output = {
261
+ ...data,
262
+ _meta: {
263
+ provider: raw.provider,
264
+ model: raw.model,
265
+ durationMs: raw.durationMs,
266
+ tokens: raw.usage?.totalTokens,
267
+ },
268
+ };
269
+
270
+ console.log(JSON.stringify(output));
271
+ } catch (err: any) {
272
+ console.log(
273
+ JSON.stringify({
274
+ actions: [],
275
+ spoken: "Sorry, I had trouble processing that.",
276
+ _meta: { error: err.message },
277
+ })
278
+ );
279
+ process.exit(1);
280
+ }