@arach/lattices 0.2.1 → 0.6.1
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/LICENSE +21 -0
- package/README.md +144 -69
- package/apps/mac/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
- package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
- package/apps/mac/Lattices.entitlements +21 -0
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Resources/tap.wav +0 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/assistant-intelligence.ts +912 -0
- package/bin/cli/capture.ts +252 -0
- package/bin/cli/daemon.ts +22 -0
- package/bin/cli/helpers.ts +105 -0
- package/bin/cli/layer.ts +178 -0
- package/bin/cli/runs.ts +43 -0
- package/bin/cli/search.ts +141 -0
- package/bin/cli/session.ts +32 -0
- package/bin/client.ts +17 -0
- package/bin/cua.ts +26 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +96 -0
- package/bin/handsoff-worker.ts +531 -0
- package/bin/infer.ts +424 -0
- package/bin/keychain.ts +75 -0
- package/bin/lattices-app.ts +655 -0
- package/bin/lattices-build +125 -0
- package/bin/lattices-build-env.ts +77 -0
- package/bin/lattices-dev +362 -0
- package/bin/lattices.ts +3260 -0
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +233 -0
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +1041 -47
- package/docs/app.md +96 -13
- package/docs/assistant-knowledge.md +130 -0
- package/docs/companion-deck.md +209 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/concepts.md +13 -12
- package/docs/config.md +83 -10
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/hyperspace-grid-snappiness.md +210 -0
- package/docs/layers.md +176 -28
- package/docs/mouse-gestures.md +244 -0
- package/docs/ocr.md +21 -9
- package/docs/overview.md +42 -23
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +382 -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/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-followup-gaps.md +103 -0
- package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/quickstart.md +8 -12
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/release.md +172 -0
- package/docs/repo-structure.md +100 -0
- package/docs/terminal-kit.md +87 -0
- package/docs/tiling-reference.md +224 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice-error-model.md +73 -0
- package/docs/voice.md +221 -0
- package/package.json +69 -16
- package/packages/npm/sdk/cua.d.mts +1 -0
- package/packages/npm/sdk/cua.d.ts +188 -0
- package/packages/npm/sdk/cua.mjs +376 -0
- package/app/Lattices.app/Contents/Info.plist +0 -24
- package/app/Package.swift +0 -13
- package/app/Sources/ActionRow.swift +0 -61
- package/app/Sources/App.swift +0 -10
- package/app/Sources/AppDelegate.swift +0 -234
- package/app/Sources/AppShellView.swift +0 -62
- package/app/Sources/AppTypeClassifier.swift +0 -70
- package/app/Sources/AppWindowShell.swift +0 -63
- package/app/Sources/CheatSheetHUD.swift +0 -332
- package/app/Sources/CommandModeState.swift +0 -1362
- package/app/Sources/CommandModeView.swift +0 -1405
- package/app/Sources/CommandModeWindow.swift +0 -192
- package/app/Sources/CommandPaletteView.swift +0 -307
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/DaemonProtocol.swift +0 -101
- package/app/Sources/DaemonServer.swift +0 -414
- package/app/Sources/DesktopModel.swift +0 -121
- package/app/Sources/DesktopModelTypes.swift +0 -71
- package/app/Sources/DiagnosticLog.swift +0 -271
- package/app/Sources/EventBus.swift +0 -30
- package/app/Sources/HotkeyManager.swift +0 -250
- package/app/Sources/HotkeyStore.swift +0 -338
- package/app/Sources/InventoryManager.swift +0 -35
- package/app/Sources/InventoryPath.swift +0 -43
- package/app/Sources/KeyRecorderView.swift +0 -210
- package/app/Sources/LatticesApi.swift +0 -1125
- package/app/Sources/MainView.swift +0 -467
- package/app/Sources/MainWindow.swift +0 -83
- package/app/Sources/OcrModel.swift +0 -309
- package/app/Sources/OcrStore.swift +0 -295
- package/app/Sources/OmniSearchState.swift +0 -283
- package/app/Sources/OmniSearchView.swift +0 -288
- package/app/Sources/OmniSearchWindow.swift +0 -105
- package/app/Sources/OrphanRow.swift +0 -129
- package/app/Sources/PaletteCommand.swift +0 -419
- package/app/Sources/PermissionChecker.swift +0 -125
- package/app/Sources/Preferences.swift +0 -92
- package/app/Sources/ProcessModel.swift +0 -199
- package/app/Sources/ProcessQuery.swift +0 -151
- package/app/Sources/Project.swift +0 -28
- package/app/Sources/ProjectRow.swift +0 -368
- package/app/Sources/ProjectScanner.swift +0 -121
- package/app/Sources/ScreenMapState.swift +0 -2387
- package/app/Sources/ScreenMapView.swift +0 -2820
- package/app/Sources/ScreenMapWindowController.swift +0 -89
- package/app/Sources/SessionManager.swift +0 -72
- package/app/Sources/SettingsView.swift +0 -1053
- package/app/Sources/SettingsWindow.swift +0 -20
- package/app/Sources/TabGroupRow.swift +0 -178
- package/app/Sources/Terminal.swift +0 -259
- package/app/Sources/TerminalQuery.swift +0 -156
- package/app/Sources/TerminalSynthesizer.swift +0 -200
- package/app/Sources/Theme.swift +0 -163
- package/app/Sources/TilePickerView.swift +0 -209
- package/app/Sources/TmuxModel.swift +0 -53
- package/app/Sources/TmuxQuery.swift +0 -81
- package/app/Sources/WindowTiler.swift +0 -1755
- package/app/Sources/WorkspaceManager.swift +0 -434
- package/bin/lattices-app.js +0 -221
- package/bin/lattices.js +0 -1418
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
|
|
4
|
+
export type AssistantSlotValue = string | number | boolean;
|
|
5
|
+
|
|
6
|
+
export interface AssistantAction {
|
|
7
|
+
intent: string;
|
|
8
|
+
slots?: Record<string, AssistantSlotValue>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AssistantPlan {
|
|
12
|
+
actions: AssistantAction[];
|
|
13
|
+
spoken: string;
|
|
14
|
+
_meta?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DesktopWindowSnapshot {
|
|
18
|
+
wid?: number;
|
|
19
|
+
app?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
frame?: string;
|
|
22
|
+
onScreen?: boolean;
|
|
23
|
+
zIndex?: number;
|
|
24
|
+
session?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DesktopSnapshot {
|
|
28
|
+
windows?: DesktopWindowSnapshot[];
|
|
29
|
+
activeStage?: DesktopWindowSnapshot[];
|
|
30
|
+
screens?: Array<{ width?: number; height?: number; isMain?: boolean }>;
|
|
31
|
+
stageManager?: boolean;
|
|
32
|
+
smGrouping?: string;
|
|
33
|
+
stripApps?: string[];
|
|
34
|
+
hiddenApps?: string[];
|
|
35
|
+
terminals?: Array<Record<string, unknown>>;
|
|
36
|
+
tmuxSessions?: Array<Record<string, unknown>>;
|
|
37
|
+
currentLayer?: { name?: string; index?: number } | string;
|
|
38
|
+
screen?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface IntentDefinition {
|
|
42
|
+
intent: string;
|
|
43
|
+
description: string;
|
|
44
|
+
slots: Array<{ name: string; required?: boolean; description: string }>;
|
|
45
|
+
examples: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const repoRoot = dirname(import.meta.dir);
|
|
49
|
+
export const assistantPromptPath = join(repoRoot, "docs", "prompts", "hands-off-system.md");
|
|
50
|
+
|
|
51
|
+
export const tilePositions = [
|
|
52
|
+
"left",
|
|
53
|
+
"right",
|
|
54
|
+
"top",
|
|
55
|
+
"bottom",
|
|
56
|
+
"top-left",
|
|
57
|
+
"top-right",
|
|
58
|
+
"bottom-left",
|
|
59
|
+
"bottom-right",
|
|
60
|
+
"left-third",
|
|
61
|
+
"center-third",
|
|
62
|
+
"right-third",
|
|
63
|
+
"top-left-third",
|
|
64
|
+
"top-center-third",
|
|
65
|
+
"top-right-third",
|
|
66
|
+
"bottom-left-third",
|
|
67
|
+
"bottom-center-third",
|
|
68
|
+
"bottom-right-third",
|
|
69
|
+
"first-fourth",
|
|
70
|
+
"second-fourth",
|
|
71
|
+
"third-fourth",
|
|
72
|
+
"last-fourth",
|
|
73
|
+
"top-first-fourth",
|
|
74
|
+
"top-second-fourth",
|
|
75
|
+
"top-third-fourth",
|
|
76
|
+
"top-last-fourth",
|
|
77
|
+
"bottom-first-fourth",
|
|
78
|
+
"bottom-second-fourth",
|
|
79
|
+
"bottom-third-fourth",
|
|
80
|
+
"bottom-last-fourth",
|
|
81
|
+
"maximize",
|
|
82
|
+
"center",
|
|
83
|
+
] as const;
|
|
84
|
+
|
|
85
|
+
export const intentDefinitions: IntentDefinition[] = [
|
|
86
|
+
{
|
|
87
|
+
intent: "tile_window",
|
|
88
|
+
description: "Tile one window to a named position or grid cell.",
|
|
89
|
+
slots: [
|
|
90
|
+
{ name: "position", required: true, description: "Named tile position, canonical 0-based grid:CxR:C,R, or compact 1-based CxR:C,R syntax." },
|
|
91
|
+
{ name: "app", description: "Loose app name when no window id is known." },
|
|
92
|
+
{ name: "wid", description: "Specific macOS window id from the desktop snapshot." },
|
|
93
|
+
{ name: "session", description: "Tmux session name." },
|
|
94
|
+
],
|
|
95
|
+
examples: [
|
|
96
|
+
"tile chrome left",
|
|
97
|
+
"snap this to the top right",
|
|
98
|
+
"maximize the window",
|
|
99
|
+
"put chrome in the top-right cell of a 4x4 grid",
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
intent: "focus",
|
|
104
|
+
description: "Focus a window, app, or session.",
|
|
105
|
+
slots: [
|
|
106
|
+
{ name: "app", description: "Loose app name." },
|
|
107
|
+
{ name: "wid", description: "Specific window id." },
|
|
108
|
+
{ name: "session", description: "Tmux session name." },
|
|
109
|
+
],
|
|
110
|
+
examples: ["focus Slack", "show me the lattices terminal"],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
intent: "distribute",
|
|
114
|
+
description: "Arrange visible windows in an even grid, optionally filtered by app and region.",
|
|
115
|
+
slots: [
|
|
116
|
+
{ name: "app", description: "Optional app filter." },
|
|
117
|
+
{ name: "region", description: "Optional screen region such as left, right, top, or bottom." },
|
|
118
|
+
],
|
|
119
|
+
examples: ["organize my terminals", "grid Chrome on the right"],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
intent: "swap",
|
|
123
|
+
description: "Swap two windows by id.",
|
|
124
|
+
slots: [
|
|
125
|
+
{ name: "wid_a", required: true, description: "First window id." },
|
|
126
|
+
{ name: "wid_b", required: true, description: "Second window id." },
|
|
127
|
+
],
|
|
128
|
+
examples: ["swap Chrome and iTerm"],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
intent: "hide",
|
|
132
|
+
description: "Hide an app or minimize a window.",
|
|
133
|
+
slots: [
|
|
134
|
+
{ name: "app", description: "App name to hide." },
|
|
135
|
+
{ name: "wid", description: "Window id to minimize." },
|
|
136
|
+
],
|
|
137
|
+
examples: ["hide Slack", "minimize that"],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
intent: "highlight",
|
|
141
|
+
description: "Flash a window border so the user can identify it.",
|
|
142
|
+
slots: [
|
|
143
|
+
{ name: "app", description: "App name to find." },
|
|
144
|
+
{ name: "wid", description: "Window id to flash." },
|
|
145
|
+
],
|
|
146
|
+
examples: ["which one is the lattices terminal", "highlight Chrome"],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
intent: "move_to_display",
|
|
150
|
+
description: "Move a window to another display, optionally placing it there.",
|
|
151
|
+
slots: [
|
|
152
|
+
{ name: "display", required: true, description: "Display index, where 0 is main." },
|
|
153
|
+
{ name: "position", description: "Optional tile position on the target display." },
|
|
154
|
+
{ name: "app", description: "App name when no window id is known." },
|
|
155
|
+
{ name: "wid", description: "Specific window id." },
|
|
156
|
+
],
|
|
157
|
+
examples: ["move Chrome to my second monitor"],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
intent: "undo",
|
|
161
|
+
description: "Restore the previous window positions.",
|
|
162
|
+
slots: [],
|
|
163
|
+
examples: ["undo that", "put it back"],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
intent: "search",
|
|
167
|
+
description: "Search windows, terminal context, and OCR content.",
|
|
168
|
+
slots: [{ name: "query", required: true, description: "Search text." }],
|
|
169
|
+
examples: ["find the error message", "search for terminal windows"],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
intent: "list_windows",
|
|
173
|
+
description: "List visible windows.",
|
|
174
|
+
slots: [],
|
|
175
|
+
examples: ["what windows are open"],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
intent: "list_sessions",
|
|
179
|
+
description: "List active terminal sessions.",
|
|
180
|
+
slots: [],
|
|
181
|
+
examples: ["what sessions are running"],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
intent: "switch_layer",
|
|
185
|
+
description: "Switch to a workspace layer.",
|
|
186
|
+
slots: [{ name: "layer", required: true, description: "Layer name or index." }],
|
|
187
|
+
examples: ["switch to the review layer", "go to layer 2"],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
intent: "create_layer",
|
|
191
|
+
description: "Save current arrangement as a named layer.",
|
|
192
|
+
slots: [{ name: "name", required: true, description: "Layer name." }],
|
|
193
|
+
examples: ["save this layout as deploy"],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
intent: "launch",
|
|
197
|
+
description: "Launch a project session.",
|
|
198
|
+
slots: [{ name: "project", required: true, description: "Project name or path." }],
|
|
199
|
+
examples: ["open the frontend project"],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
intent: "kill",
|
|
203
|
+
description: "Kill a terminal session.",
|
|
204
|
+
slots: [{ name: "session", required: true, description: "Session name or project name." }],
|
|
205
|
+
examples: ["kill the API session"],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
intent: "scan",
|
|
209
|
+
description: "Trigger immediate OCR scan.",
|
|
210
|
+
slots: [],
|
|
211
|
+
examples: ["scan the screen", "read what's on screen"],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
intent: "find_mouse",
|
|
215
|
+
description: "Show the cursor location with a pulse.",
|
|
216
|
+
slots: [],
|
|
217
|
+
examples: ["find my mouse"],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
intent: "summon_mouse",
|
|
221
|
+
description: "Move the cursor to the center of the screen.",
|
|
222
|
+
slots: [],
|
|
223
|
+
examples: ["summon mouse"],
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const positionAliases: Array<{ position: string; phrases: string[] }> = [
|
|
228
|
+
{ position: "top-left", phrases: ["top left", "upper left", "top-left"] },
|
|
229
|
+
{ position: "top-right", phrases: ["top right", "upper right", "top-right"] },
|
|
230
|
+
{ position: "bottom-left", phrases: ["bottom left", "lower left", "bottom-left"] },
|
|
231
|
+
{ position: "bottom-right", phrases: ["bottom right", "lower right", "bottom-right"] },
|
|
232
|
+
{ position: "left-third", phrases: ["left third", "first third", "left-third"] },
|
|
233
|
+
{ position: "center-third", phrases: ["center third", "middle third", "centre third", "center-third"] },
|
|
234
|
+
{ position: "right-third", phrases: ["right third", "last third", "right-third"] },
|
|
235
|
+
{ position: "left", phrases: ["left half", "left side", "the left", "left"] },
|
|
236
|
+
{ position: "right", phrases: ["right half", "right side", "the right", "right"] },
|
|
237
|
+
{ position: "top", phrases: ["top half", "upper half", "the top", "top"] },
|
|
238
|
+
{ position: "bottom", phrases: ["bottom half", "lower half", "the bottom", "bottom"] },
|
|
239
|
+
{ position: "maximize", phrases: ["maximize", "maximise", "full screen", "fullscreen", "make it big", "max"] },
|
|
240
|
+
{ position: "center", phrases: ["center", "centre", "middle"] },
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const appAliases: Record<string, string[]> = {
|
|
244
|
+
"Google Chrome": ["chrome", "google chrome"],
|
|
245
|
+
iTerm2: ["iterm", "iterm2", "terminal", "terminals"],
|
|
246
|
+
Terminal: ["terminal app", "terminal"],
|
|
247
|
+
"Visual Studio Code": ["vs code", "vscode", "visual studio code"],
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const noisePrefixes = [
|
|
251
|
+
"can you",
|
|
252
|
+
"could you",
|
|
253
|
+
"would you",
|
|
254
|
+
"please",
|
|
255
|
+
"just",
|
|
256
|
+
"go ahead and",
|
|
257
|
+
"let's",
|
|
258
|
+
"lets",
|
|
259
|
+
"i want to",
|
|
260
|
+
"i need to",
|
|
261
|
+
"i'd like to",
|
|
262
|
+
"id like to",
|
|
263
|
+
"ok",
|
|
264
|
+
"okay",
|
|
265
|
+
"hey",
|
|
266
|
+
"yo",
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
const fillerWords = new Set([
|
|
270
|
+
"the",
|
|
271
|
+
"my",
|
|
272
|
+
"a",
|
|
273
|
+
"an",
|
|
274
|
+
"this",
|
|
275
|
+
"that",
|
|
276
|
+
"it",
|
|
277
|
+
"window",
|
|
278
|
+
"windows",
|
|
279
|
+
"app",
|
|
280
|
+
"application",
|
|
281
|
+
"project",
|
|
282
|
+
"session",
|
|
283
|
+
"layer",
|
|
284
|
+
"please",
|
|
285
|
+
"for",
|
|
286
|
+
"me",
|
|
287
|
+
"to",
|
|
288
|
+
"in",
|
|
289
|
+
"on",
|
|
290
|
+
"into",
|
|
291
|
+
"at",
|
|
292
|
+
"side",
|
|
293
|
+
"half",
|
|
294
|
+
"corner",
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
export function renderIntentCatalog(): string {
|
|
298
|
+
const parts = intentDefinitions.map((def) => {
|
|
299
|
+
const slots = def.slots.length
|
|
300
|
+
? def.slots.map((slot) => {
|
|
301
|
+
const marker = slot.required ? " required" : " optional";
|
|
302
|
+
return ` ${slot.name} (${marker}): ${slot.description}`;
|
|
303
|
+
}).join("\n")
|
|
304
|
+
: " none";
|
|
305
|
+
return [
|
|
306
|
+
`${def.intent}: ${def.description}`,
|
|
307
|
+
" Slots:",
|
|
308
|
+
slots,
|
|
309
|
+
` Examples: ${def.examples.map((x) => `"${x}"`).join(", ")}`,
|
|
310
|
+
].join("\n");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return `${parts.join("\n\n")}
|
|
314
|
+
|
|
315
|
+
TILING PRESETS:
|
|
316
|
+
"split screen" / "side by side" -> tile_window left + right
|
|
317
|
+
"thirds" -> left-third + center-third + right-third
|
|
318
|
+
"quadrants" / "four corners" -> top-left + top-right + bottom-left + bottom-right
|
|
319
|
+
"mosaic" / "grid" / "spread out" -> distribute
|
|
320
|
+
|
|
321
|
+
POSITION RULES:
|
|
322
|
+
"quarter" means a 2x2 cell, not a 4x1 fourth.
|
|
323
|
+
Use wid from the snapshot when a target window is clear.
|
|
324
|
+
Use app only when no specific wid is available.`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function buildAssistantSystemPrompt(): string {
|
|
328
|
+
let prompt: string;
|
|
329
|
+
try {
|
|
330
|
+
prompt = readFileSync(assistantPromptPath, "utf-8")
|
|
331
|
+
.split("\n")
|
|
332
|
+
.filter((line) => !line.startsWith("# "))
|
|
333
|
+
.join("\n")
|
|
334
|
+
.trim();
|
|
335
|
+
} catch {
|
|
336
|
+
prompt = "You are a workspace assistant. Respond with JSON: {actions, spoken}.";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return prompt.replace("{{intent_catalog}}", renderIntentCatalog());
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function buildAssistantContextMessage(transcript: string, snapshot: DesktopSnapshot = {}): string {
|
|
343
|
+
let msg = `USER: "${transcript}"\n\n`;
|
|
344
|
+
msg += "--- DESKTOP SNAPSHOT ---\n";
|
|
345
|
+
|
|
346
|
+
const screens = snapshot.screens ?? [];
|
|
347
|
+
if (screens.length > 1) {
|
|
348
|
+
msg += `Displays: ${screens.map((s) => `${s.width}x${s.height}${s.isMain ? " (main)" : ""}`).join(", ")}\n`;
|
|
349
|
+
} else if (screens.length === 1) {
|
|
350
|
+
msg += `Screen: ${screens[0].width}x${screens[0].height}\n`;
|
|
351
|
+
} else if (snapshot.screen) {
|
|
352
|
+
msg += `Screen: ${snapshot.screen}\n`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
msg += `Stage Manager: ${snapshot.stageManager ? `ON (${snapshot.smGrouping ?? "all-at-once"})` : "OFF"}\n`;
|
|
356
|
+
|
|
357
|
+
const windows = listWindows(snapshot);
|
|
358
|
+
const onScreen = windows.filter((w) => w.onScreen !== false);
|
|
359
|
+
const offScreen = windows.filter((w) => w.onScreen === false);
|
|
360
|
+
|
|
361
|
+
msg += `\nVisible windows (${onScreen.length}, front-to-back order):\n`;
|
|
362
|
+
for (const w of onScreen) {
|
|
363
|
+
const flags: string[] = [];
|
|
364
|
+
if (w.zIndex === 0) flags.push("FRONTMOST");
|
|
365
|
+
if (w.session) flags.push(`session:${w.session}`);
|
|
366
|
+
const flagStr = flags.length ? ` [${flags.join(", ")}]` : "";
|
|
367
|
+
msg += ` wid:${w.wid ?? "?"} ${w.app ?? "Unknown"}: "${w.title ?? ""}"`;
|
|
368
|
+
if (w.frame) msg += ` - ${w.frame}`;
|
|
369
|
+
msg += `${flagStr}\n`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (offScreen.length > 0) {
|
|
373
|
+
const hiddenByApp = new Map<string, number>();
|
|
374
|
+
for (const w of offScreen) {
|
|
375
|
+
if (!w.app) continue;
|
|
376
|
+
hiddenByApp.set(w.app, (hiddenByApp.get(w.app) ?? 0) + 1);
|
|
377
|
+
}
|
|
378
|
+
const summary = [...hiddenByApp.entries()].map(([app, count]) => `${app}(${count})`).join(", ");
|
|
379
|
+
if (summary) msg += `\nHidden windows: ${summary}\n`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const terminals = snapshot.terminals ?? [];
|
|
383
|
+
if (terminals.length > 0) {
|
|
384
|
+
msg += `\nTerminal tabs (${terminals.length}):\n`;
|
|
385
|
+
for (const tab of terminals) {
|
|
386
|
+
const displayName = String(tab.displayName ?? tab.app ?? "Terminal");
|
|
387
|
+
const cwd = typeof tab.cwd === "string" ? ` cwd:${tab.cwd.replace(/^\/Users\/[^/]+\//, "~/")}` : "";
|
|
388
|
+
const tmux = tab.tmuxSession ? ` tmux:${String(tab.tmuxSession)}` : "";
|
|
389
|
+
const claude = tab.hasClaude ? " Claude Code" : "";
|
|
390
|
+
const wid = tab.windowId ? ` wid:${String(tab.windowId)}` : "";
|
|
391
|
+
msg += ` ${displayName}${cwd}${tmux}${claude}${wid}\n`;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const sessions = snapshot.tmuxSessions ?? [];
|
|
396
|
+
if (sessions.length > 0) {
|
|
397
|
+
msg += `\nTmux sessions: ${sessions.map((s) => String(s.name ?? "unknown")).join(", ")}\n`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (snapshot.currentLayer) {
|
|
401
|
+
const layer = typeof snapshot.currentLayer === "string"
|
|
402
|
+
? snapshot.currentLayer
|
|
403
|
+
: `${snapshot.currentLayer.name ?? "unknown"} (index: ${snapshot.currentLayer.index ?? "?"})`;
|
|
404
|
+
msg += `\nCurrent layer: ${layer}\n`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
msg += "--- END SNAPSHOT ---\n";
|
|
408
|
+
return msg;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function tryLocalAssistantPlan(transcript: string, snapshot: DesktopSnapshot = {}): AssistantPlan | null {
|
|
412
|
+
const text = normalizeTranscript(transcript);
|
|
413
|
+
if (!text) return null;
|
|
414
|
+
|
|
415
|
+
if (/^(undo|put it back|restore|restore that|that was wrong)/.test(text)) {
|
|
416
|
+
return plan([{ intent: "undo", slots: {} }], "Restoring the previous positions.", "local-rule");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (/(find|show|where).*(mouse|cursor)/.test(text)) {
|
|
420
|
+
return plan([{ intent: "find_mouse", slots: {} }], "Showing your cursor.", "local-rule");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (/(summon|center|bring).*(mouse|cursor)/.test(text)) {
|
|
424
|
+
return plan([{ intent: "summon_mouse", slots: {} }], "Moving the cursor to the center.", "local-rule");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (/^(scan|read|ocr|rescan)/.test(text) && /(screen|window|text|ocr)/.test(text)) {
|
|
428
|
+
return plan([{ intent: "scan", slots: {} }], "Scanning the screen.", "local-rule");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const split = parseSplitPlan(text, snapshot);
|
|
432
|
+
if (split) return split;
|
|
433
|
+
|
|
434
|
+
const layout = parseLayoutPlan(text, snapshot);
|
|
435
|
+
if (layout) return layout;
|
|
436
|
+
|
|
437
|
+
const tile = parseTilePlan(text, snapshot);
|
|
438
|
+
if (tile) return tile;
|
|
439
|
+
|
|
440
|
+
const distribute = parseDistributePlan(text);
|
|
441
|
+
if (distribute) return distribute;
|
|
442
|
+
|
|
443
|
+
const focus = parsePrefixedEntity(text, [
|
|
444
|
+
"focus on",
|
|
445
|
+
"focus",
|
|
446
|
+
"switch to",
|
|
447
|
+
"go to",
|
|
448
|
+
"show me",
|
|
449
|
+
"show",
|
|
450
|
+
"bring up",
|
|
451
|
+
"pull up",
|
|
452
|
+
]);
|
|
453
|
+
if (focus && !/(layer|screen|windows|sessions)/.test(focus)) {
|
|
454
|
+
const target = resolveWindowTarget(focus, snapshot);
|
|
455
|
+
const slots = targetToSlots(target, focus);
|
|
456
|
+
return plan([{ intent: "focus", slots }], `Focusing ${target.label}.`, "local-rule");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const search = parseSearchQuery(text);
|
|
460
|
+
if (search) {
|
|
461
|
+
return plan([{ intent: "search", slots: { query: search } }], `Searching for ${search}.`, "local-rule");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (/what.*windows|list windows|show.*windows|what.*open|what.*on screen/.test(text)) {
|
|
465
|
+
return plan([{ intent: "list_windows", slots: {} }], summarizeWindows(snapshot), "local-rule");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (/sessions|projects.*running|what.*running/.test(text) && !/kill|stop/.test(text)) {
|
|
469
|
+
return plan([{ intent: "list_sessions", slots: {} }], "Listing your sessions.", "local-rule");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const layer = parseLayerSwitch(text);
|
|
473
|
+
if (layer) {
|
|
474
|
+
return plan([{ intent: "switch_layer", slots: { layer } }], `Switching to ${layer}.`, "local-rule");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const layerName = parsePrefixedEntity(text, [
|
|
478
|
+
"save this layout as",
|
|
479
|
+
"save layout as",
|
|
480
|
+
"create a layer called",
|
|
481
|
+
"create layer called",
|
|
482
|
+
"make a layer called",
|
|
483
|
+
"name this layer",
|
|
484
|
+
]);
|
|
485
|
+
if (layerName) {
|
|
486
|
+
return plan([{ intent: "create_layer", slots: { name: cleanEntity(layerName) } }], `Saving this layout as ${cleanEntity(layerName)}.`, "local-rule");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const launch = parsePrefixedEntity(text, [
|
|
490
|
+
"open project",
|
|
491
|
+
"open the project",
|
|
492
|
+
"open",
|
|
493
|
+
"launch",
|
|
494
|
+
"start working on",
|
|
495
|
+
"work on",
|
|
496
|
+
]);
|
|
497
|
+
if (launch && !looksLikeAppCommand(launch)) {
|
|
498
|
+
const project = cleanEntity(launch);
|
|
499
|
+
return plan([{ intent: "launch", slots: { project } }], `Launching ${project}.`, "local-rule");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const kill = parsePrefixedEntity(text, ["kill", "stop", "shut down", "terminate"]);
|
|
503
|
+
if (kill) {
|
|
504
|
+
const session = cleanEntity(kill);
|
|
505
|
+
return plan([{ intent: "kill", slots: { session } }], `Killing ${session}.`, "local-rule");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function normalizeAssistantPlan(raw: unknown, fallbackTranscript = ""): AssistantPlan {
|
|
512
|
+
const obj = isRecord(raw) ? raw : {};
|
|
513
|
+
const rawActions = Array.isArray(obj.actions) ? obj.actions : [];
|
|
514
|
+
const actions = rawActions
|
|
515
|
+
.map(normalizeAction)
|
|
516
|
+
.filter((action): action is AssistantAction => action !== null);
|
|
517
|
+
|
|
518
|
+
const spokenRaw = typeof obj.spoken === "string" ? obj.spoken.trim() : "";
|
|
519
|
+
const spoken = spokenRaw || fallbackSpoken(actions, fallbackTranscript);
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
actions,
|
|
523
|
+
spoken,
|
|
524
|
+
_meta: isRecord(obj._meta) ? obj._meta : undefined,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function normalizeAction(raw: unknown): AssistantAction | null {
|
|
529
|
+
if (!isRecord(raw)) return null;
|
|
530
|
+
|
|
531
|
+
const intent = normalizeIntentName(String(raw.intent ?? raw.action ?? ""));
|
|
532
|
+
if (!intent) return null;
|
|
533
|
+
|
|
534
|
+
const slotsRaw = isRecord(raw.slots) ? raw.slots : {};
|
|
535
|
+
const slots: Record<string, AssistantSlotValue> = {};
|
|
536
|
+
for (const [key, value] of Object.entries(slotsRaw)) {
|
|
537
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
538
|
+
slots[key] = value;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return { intent, slots };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function normalizeIntentName(intent: string): string {
|
|
546
|
+
const lower = intent.trim().toLowerCase().replace(/[.\s-]+/g, "_");
|
|
547
|
+
const aliases: Record<string, string> = {
|
|
548
|
+
window_place: "tile_window",
|
|
549
|
+
window_tile: "tile_window",
|
|
550
|
+
layer_activate: "switch_layer",
|
|
551
|
+
layer_switch: "switch_layer",
|
|
552
|
+
space_optimize: "distribute",
|
|
553
|
+
layout_distribute: "distribute",
|
|
554
|
+
};
|
|
555
|
+
return aliases[lower] ?? lower;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function parseTilePlan(text: string, snapshot: DesktopSnapshot): AssistantPlan | null {
|
|
559
|
+
if (!/(tile|snap|put|move|throw|maximize|maximise|center|centre|full screen|fullscreen)/.test(text)) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const hit = findPosition(text);
|
|
564
|
+
if (!hit) return null;
|
|
565
|
+
|
|
566
|
+
let targetText = text
|
|
567
|
+
.replace(/^(tile|snap|put|move|throw)\s+/, "")
|
|
568
|
+
.replace(hit.phrase, " ")
|
|
569
|
+
.replace(/\b(to|in|on|into|at|the|half|side|corner)\b/g, " ");
|
|
570
|
+
|
|
571
|
+
if (/^(maximize|maximise|full screen|fullscreen|center|centre)/.test(text)) {
|
|
572
|
+
targetText = "";
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const target = resolveWindowTarget(cleanEntity(targetText), snapshot);
|
|
576
|
+
const slots = { ...targetToSlots(target, cleanEntity(targetText)), position: hit.position };
|
|
577
|
+
return plan([{ intent: "tile_window", slots }], tileSpoken(target.label, hit.position), "local-rule");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseSplitPlan(text: string, snapshot: DesktopSnapshot): AssistantPlan | null {
|
|
581
|
+
const match = text.match(/split\s+(.+?)\s+(?:and|with|&)\s+(.+)/);
|
|
582
|
+
if (match) {
|
|
583
|
+
const left = resolveWindowTarget(match[1], snapshot);
|
|
584
|
+
const right = resolveWindowTarget(match[2], snapshot);
|
|
585
|
+
return plan([
|
|
586
|
+
{ intent: "tile_window", slots: { ...targetToSlots(left, match[1]), position: "left" } },
|
|
587
|
+
{ intent: "tile_window", slots: { ...targetToSlots(right, match[2]), position: "right" } },
|
|
588
|
+
], `${left.label} left, ${right.label} right.`, "local-rule");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const explicit = text.match(/(.+?)\s+left\s+(.+?)\s+right$/);
|
|
592
|
+
if (explicit && /(chrome|safari|iterm|terminal|slack|code|cursor|finder)/.test(text)) {
|
|
593
|
+
const left = resolveWindowTarget(explicit[1], snapshot);
|
|
594
|
+
const right = resolveWindowTarget(explicit[2], snapshot);
|
|
595
|
+
return plan([
|
|
596
|
+
{ intent: "tile_window", slots: { ...targetToSlots(left, explicit[1]), position: "left" } },
|
|
597
|
+
{ intent: "tile_window", slots: { ...targetToSlots(right, explicit[2]), position: "right" } },
|
|
598
|
+
], `${left.label} left, ${right.label} right.`, "local-rule");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function parseLayoutPlan(text: string, snapshot: DesktopSnapshot): AssistantPlan | null {
|
|
605
|
+
const windows = listWindows(snapshot).filter((w) => w.onScreen !== false);
|
|
606
|
+
|
|
607
|
+
if (/(quadrants?|four corners?|corners)/.test(text) && windows.length >= 4) {
|
|
608
|
+
const positions = ["top-left", "top-right", "bottom-left", "bottom-right"];
|
|
609
|
+
const actions = windows.slice(0, 4).map((w, index) => ({
|
|
610
|
+
intent: "tile_window",
|
|
611
|
+
slots: { wid: w.wid ?? 0, position: positions[index] },
|
|
612
|
+
}));
|
|
613
|
+
return plan(actions, "Putting four windows in quadrants.", "local-rule");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (/\bthirds\b/.test(text) && windows.length >= 3) {
|
|
617
|
+
const positions = ["left-third", "center-third", "right-third"];
|
|
618
|
+
const actions = windows.slice(0, 3).map((w, index) => ({
|
|
619
|
+
intent: "tile_window",
|
|
620
|
+
slots: { wid: w.wid ?? 0, position: positions[index] },
|
|
621
|
+
}));
|
|
622
|
+
return plan(actions, "Arranging three windows in thirds.", "local-rule");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (/(split screen|side by side)/.test(text) && windows.length >= 2) {
|
|
626
|
+
return plan([
|
|
627
|
+
{ intent: "tile_window", slots: { wid: windows[0].wid ?? 0, position: "left" } },
|
|
628
|
+
{ intent: "tile_window", slots: { wid: windows[1].wid ?? 0, position: "right" } },
|
|
629
|
+
], "Splitting the front two windows.", "local-rule");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function parseDistributePlan(text: string): AssistantPlan | null {
|
|
636
|
+
if (!/(grid|mosaic|distribute|spread|organize|organise|arrange|tidy|clean up)/.test(text)) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const slots: Record<string, AssistantSlotValue> = {};
|
|
641
|
+
const app = appFromText(text);
|
|
642
|
+
const region = regionFromText(text);
|
|
643
|
+
if (app) slots.app = app;
|
|
644
|
+
if (region) slots.region = region;
|
|
645
|
+
|
|
646
|
+
const scope = app ? `${app} windows` : "your windows";
|
|
647
|
+
const where = region ? ` on the ${region}` : "";
|
|
648
|
+
return plan([{ intent: "distribute", slots }], `Gridding ${scope}${where}.`, "local-rule");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function parseSearchQuery(text: string): string | null {
|
|
652
|
+
const query = parsePrefixedEntity(text, [
|
|
653
|
+
"find all",
|
|
654
|
+
"find",
|
|
655
|
+
"search for",
|
|
656
|
+
"search",
|
|
657
|
+
"look for",
|
|
658
|
+
"locate",
|
|
659
|
+
"where is",
|
|
660
|
+
"where does it say",
|
|
661
|
+
"which window has",
|
|
662
|
+
]);
|
|
663
|
+
if (!query) return null;
|
|
664
|
+
return cleanQuery(query);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function parseLayerSwitch(text: string): string | null {
|
|
668
|
+
if (text === "next layer" || text === "previous layer") return text;
|
|
669
|
+
const literal: Record<string, string> = {
|
|
670
|
+
"layer one": "1",
|
|
671
|
+
"layer two": "2",
|
|
672
|
+
"layer three": "3",
|
|
673
|
+
"first layer": "1",
|
|
674
|
+
"second layer": "2",
|
|
675
|
+
"third layer": "3",
|
|
676
|
+
};
|
|
677
|
+
if (literal[text]) return literal[text];
|
|
678
|
+
const entity = parsePrefixedEntity(text, [
|
|
679
|
+
"switch to layer",
|
|
680
|
+
"switch to the",
|
|
681
|
+
"switch to",
|
|
682
|
+
"go to layer",
|
|
683
|
+
"go to the",
|
|
684
|
+
"activate layer",
|
|
685
|
+
"layer",
|
|
686
|
+
]);
|
|
687
|
+
return entity ? cleanEntity(entity).replace(/\s+layer$/, "") : null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function findPosition(text: string): { position: string; phrase: string } | null {
|
|
691
|
+
const grid = text.match(/(?:grid:)?\d+x\d+:\d+,\d+(?:-\d+,\d+)?/);
|
|
692
|
+
if (grid) {
|
|
693
|
+
const position = canonicalGridPosition(grid[0]);
|
|
694
|
+
if (position) return { position, phrase: grid[0] };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
for (const entry of positionAliases) {
|
|
698
|
+
for (const phrase of entry.phrases) {
|
|
699
|
+
if (text.includes(phrase)) {
|
|
700
|
+
return { position: entry.position, phrase };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function canonicalGridPosition(raw: string): string | null {
|
|
708
|
+
const match = raw.toLowerCase().match(/^(grid:)?(\d+)x(\d+):(\d+),(\d+)(?:-(\d+),(\d+))?$/);
|
|
709
|
+
if (!match) return null;
|
|
710
|
+
|
|
711
|
+
const oneBased = !match[1];
|
|
712
|
+
const columns = Number(match[2]);
|
|
713
|
+
const rows = Number(match[3]);
|
|
714
|
+
let c0 = Number(match[4]);
|
|
715
|
+
let r0 = Number(match[5]);
|
|
716
|
+
let c1 = match[6] === undefined ? c0 : Number(match[6]);
|
|
717
|
+
let r1 = match[7] === undefined ? r0 : Number(match[7]);
|
|
718
|
+
if (oneBased) {
|
|
719
|
+
c0 -= 1;
|
|
720
|
+
r0 -= 1;
|
|
721
|
+
c1 -= 1;
|
|
722
|
+
r1 -= 1;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const left = Math.min(c0, c1);
|
|
726
|
+
const right = Math.max(c0, c1);
|
|
727
|
+
const top = Math.min(r0, r1);
|
|
728
|
+
const bottom = Math.max(r0, r1);
|
|
729
|
+
if (
|
|
730
|
+
columns <= 0 || rows <= 0 ||
|
|
731
|
+
left < 0 || top < 0 ||
|
|
732
|
+
right >= columns || bottom >= rows
|
|
733
|
+
) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (left === right && top === bottom) return `grid:${columns}x${rows}:${left},${top}`;
|
|
738
|
+
return `grid:${columns}x${rows}:${left},${top}-${right},${bottom}`;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function regionFromText(text: string): string | null {
|
|
742
|
+
const hit = findPosition(text);
|
|
743
|
+
if (!hit) return null;
|
|
744
|
+
return ["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right", "left-third", "center-third", "right-third"].includes(hit.position)
|
|
745
|
+
? hit.position
|
|
746
|
+
: null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function appFromText(text: string): string | null {
|
|
750
|
+
for (const [app, aliases] of Object.entries(appAliases)) {
|
|
751
|
+
if (aliases.some((alias) => text.includes(alias))) return app;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const entity = parsePrefixedEntity(text, ["grid", "organize", "organise", "arrange", "distribute"]);
|
|
755
|
+
return entity ? titleCase(cleanEntity(entity).replace(/\bon (left|right|top|bottom).*$/, "")) : null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function parsePrefixedEntity(text: string, prefixes: string[]): string | null {
|
|
759
|
+
const sorted = [...prefixes].sort((a, b) => b.length - a.length);
|
|
760
|
+
for (const prefix of sorted) {
|
|
761
|
+
if (text === prefix) return "";
|
|
762
|
+
if (text.startsWith(`${prefix} `)) {
|
|
763
|
+
return text.slice(prefix.length + 1).trim();
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function normalizeTranscript(input: string): string {
|
|
770
|
+
let text = String(input ?? "")
|
|
771
|
+
.toLowerCase()
|
|
772
|
+
.replace(/[^a-z0-9\s:-]/g, " ")
|
|
773
|
+
.split(/\s+/)
|
|
774
|
+
.join(" ")
|
|
775
|
+
.trim();
|
|
776
|
+
|
|
777
|
+
let changed = true;
|
|
778
|
+
while (changed) {
|
|
779
|
+
changed = false;
|
|
780
|
+
for (const prefix of noisePrefixes) {
|
|
781
|
+
if (text.startsWith(`${prefix} `)) {
|
|
782
|
+
text = text.slice(prefix.length + 1).trim();
|
|
783
|
+
changed = true;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return text;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function cleanEntity(input: string): string {
|
|
792
|
+
const words = input
|
|
793
|
+
.toLowerCase()
|
|
794
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
795
|
+
.split(/\s+/)
|
|
796
|
+
.filter((word) => word && !fillerWords.has(word));
|
|
797
|
+
return words.join(" ").trim();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function cleanQuery(input: string): string {
|
|
801
|
+
return cleanEntity(input)
|
|
802
|
+
.replace(/\ball\b/g, " ")
|
|
803
|
+
.replace(/\bwith\b/g, " ")
|
|
804
|
+
.split(/\s+/)
|
|
805
|
+
.filter(Boolean)
|
|
806
|
+
.join(" ");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function listWindows(snapshot: DesktopSnapshot): DesktopWindowSnapshot[] {
|
|
810
|
+
const raw = snapshot.windows ?? snapshot.activeStage ?? [];
|
|
811
|
+
return raw
|
|
812
|
+
.filter((w) => w && (w.app || w.title || w.wid))
|
|
813
|
+
.sort((a, b) => (a.zIndex ?? 999) - (b.zIndex ?? 999));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function resolveWindowTarget(raw: string, snapshot: DesktopSnapshot): { wid?: number; app?: string; label: string } {
|
|
817
|
+
const cleaned = cleanEntity(raw);
|
|
818
|
+
const windows = listWindows(snapshot).filter((w) => w.onScreen !== false);
|
|
819
|
+
|
|
820
|
+
if (!cleaned || ["this", "that", "it"].includes(cleaned)) {
|
|
821
|
+
const front = windows.find((w) => w.zIndex === 0) ?? windows[0];
|
|
822
|
+
if (front?.wid) return { wid: front.wid, app: front.app, label: front.app ?? "this window" };
|
|
823
|
+
return { label: "this window" };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
for (const [app, aliases] of Object.entries(appAliases)) {
|
|
827
|
+
if (aliases.includes(cleaned)) {
|
|
828
|
+
const win = windows.find((w) => w.app?.toLowerCase().includes(app.toLowerCase()));
|
|
829
|
+
if (win?.wid) return { wid: win.wid, app: win.app, label: win.app ?? app };
|
|
830
|
+
return { app, label: app };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const win = windows.find((w) => {
|
|
835
|
+
const app = w.app?.toLowerCase() ?? "";
|
|
836
|
+
const title = w.title?.toLowerCase() ?? "";
|
|
837
|
+
return app.includes(cleaned) || cleaned.includes(app) || title.includes(cleaned);
|
|
838
|
+
});
|
|
839
|
+
if (win?.wid) return { wid: win.wid, app: win.app, label: win.app ?? cleaned };
|
|
840
|
+
|
|
841
|
+
return { app: titleCase(cleaned), label: titleCase(cleaned) };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function targetToSlots(target: { wid?: number; app?: string }, fallback: string): Record<string, AssistantSlotValue> {
|
|
845
|
+
if (target.wid) return { wid: target.wid };
|
|
846
|
+
if (target.app) return { app: target.app };
|
|
847
|
+
const cleaned = cleanEntity(fallback);
|
|
848
|
+
return cleaned ? { app: titleCase(cleaned) } : {};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function tileSpoken(label: string, position: string): string {
|
|
852
|
+
if (position === "maximize") return `Maximizing ${label}.`;
|
|
853
|
+
if (position === "center") return `Centering ${label}.`;
|
|
854
|
+
return `Tiling ${label} to the ${position}.`;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function summarizeWindows(snapshot: DesktopSnapshot): string {
|
|
858
|
+
const windows = listWindows(snapshot).filter((w) => w.onScreen !== false);
|
|
859
|
+
if (!windows.length) return "I do not see any windows in the snapshot.";
|
|
860
|
+
const counts = new Map<string, number>();
|
|
861
|
+
for (const w of windows) {
|
|
862
|
+
const app = w.app ?? "Unknown";
|
|
863
|
+
counts.set(app, (counts.get(app) ?? 0) + 1);
|
|
864
|
+
}
|
|
865
|
+
const summary = [...counts.entries()]
|
|
866
|
+
.slice(0, 5)
|
|
867
|
+
.map(([app, count]) => count === 1 ? app : `${count} ${app}`)
|
|
868
|
+
.join(", ");
|
|
869
|
+
return `You've got ${windows.length} windows: ${summary}.`;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function fallbackSpoken(actions: AssistantAction[], transcript: string): string {
|
|
873
|
+
if (actions.length === 0) return transcript ? `I heard ${transcript}, but I do not have an action for it.` : "No action planned.";
|
|
874
|
+
if (actions.length === 1) {
|
|
875
|
+
const action = actions[0];
|
|
876
|
+
if (action.intent === "tile_window") return tileSpoken(String(action.slots?.app ?? "this window"), String(action.slots?.position ?? "there"));
|
|
877
|
+
if (action.intent === "focus") return `Focusing ${String(action.slots?.app ?? "that window")}.`;
|
|
878
|
+
if (action.intent === "distribute") return "Arranging the windows.";
|
|
879
|
+
}
|
|
880
|
+
return "I'll handle that.";
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function plan(actions: AssistantAction[], spoken: string, source: string): AssistantPlan {
|
|
884
|
+
return { actions, spoken, _meta: { source } };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function titleCase(input: string): string {
|
|
888
|
+
return input
|
|
889
|
+
.split(/\s+/)
|
|
890
|
+
.filter(Boolean)
|
|
891
|
+
.map((word) => word.slice(0, 1).toUpperCase() + word.slice(1))
|
|
892
|
+
.join(" ");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function looksLikeAppCommand(input: string): boolean {
|
|
896
|
+
return /(chrome|safari|slack|finder|iterm|terminal|code|cursor|xcode)/.test(input);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
900
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (import.meta.main) {
|
|
904
|
+
const args = process.argv.slice(2);
|
|
905
|
+
const json = args.includes("--json");
|
|
906
|
+
const text = args.filter((arg) => arg !== "--json").join(" ");
|
|
907
|
+
const stdin = await Bun.stdin.text();
|
|
908
|
+
const snapshot = stdin.trim() ? JSON.parse(stdin) as DesktopSnapshot : {};
|
|
909
|
+
const local = tryLocalAssistantPlan(text, snapshot);
|
|
910
|
+
const result = local ?? { actions: [], spoken: "No local plan matched.", _meta: { source: "local-rule", matched: false } };
|
|
911
|
+
console.log(json ? JSON.stringify(result, null, 2) : result.spoken);
|
|
912
|
+
}
|