@lattices/cli 0.4.2 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/AppShell/App.swift +20 -0
- package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
- package/app/Sources/AppShell/AppUpdater.swift +92 -0
- package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
- package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
- package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
- package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
- package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
- package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
- package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
- package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
- package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
- package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
- package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
- package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
- package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
- package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
- package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
- package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
- package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
- package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
- package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
- package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
- package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
- package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
- package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
- package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
- package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
- package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
- package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
- package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
- package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
- package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
- package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
- package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
- package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
- package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
- package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
- package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
- package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
- package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
- package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
- package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
- package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
- package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
- package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
- package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
- package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
- package/app/Sources/{WorkspaceManager.swift → Core/Workspace/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/component-extraction-roadmap.md +392 -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 +4 -1
- package/app/Sources/App.swift +0 -10
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/MouseFinder.swift +0 -222
- /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
- /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
- /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
- /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
- /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
- /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
- /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
- /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
- /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
- /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
- /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
- /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
- /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
- /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
- /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
- /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
- /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
- /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
- /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
- /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
- /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
- /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
- /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
- /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
- /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
- /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
- /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
- /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
- /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
- /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
- /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
- /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
- /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
- /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
- /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
- /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
- /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
- /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
- /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
- /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
- /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
- /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
- /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
- /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
- /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
- /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
package/bin/lattices-app.ts
CHANGED
|
@@ -22,6 +22,7 @@ const REPO = "arach/lattices";
|
|
|
22
22
|
const RELEASE_APP_ASSET_NAMES = ["Lattices.dmg"];
|
|
23
23
|
const RELEASE_BINARY_ASSET_NAMES = ["Lattices-macos-arm64", "LatticeApp-macos-arm64"];
|
|
24
24
|
type ReleaseAsset = { name: string; browser_download_url: string };
|
|
25
|
+
const selfScriptPath = resolve(__dirname, "lattices-app.ts");
|
|
25
26
|
|
|
26
27
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
27
28
|
|
|
@@ -79,6 +80,14 @@ function launch(extraArgs: string[] = []): void {
|
|
|
79
80
|
console.log("lattices app launched.");
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
function relaunchIfNeeded(shouldLaunch: boolean, extraArgs: string[] = []): void {
|
|
84
|
+
if (!shouldLaunch) {
|
|
85
|
+
console.log("App updated. Launch with: lattices app");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
launch(extraArgs);
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
function resolveSigningIdentity(): string | null {
|
|
83
92
|
try {
|
|
84
93
|
const identities = execSync("security find-identity -v -p codesigning", { stdio: "pipe" }).toString();
|
|
@@ -93,6 +102,11 @@ function resolveSigningIdentity(): string | null {
|
|
|
93
102
|
function signBundle(): void {
|
|
94
103
|
const identity = resolveSigningIdentity();
|
|
95
104
|
const entFlag = existsSync(entitlementsPath) ? ` --entitlements '${entitlementsPath}'` : "";
|
|
105
|
+
const tempBinaryPath = `${binaryPath}.cstemp`;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
if (existsSync(tempBinaryPath)) rmSync(tempBinaryPath, { force: true });
|
|
109
|
+
} catch {}
|
|
96
110
|
|
|
97
111
|
if (identity) {
|
|
98
112
|
console.log(`Signing with: ${identity}`);
|
|
@@ -113,6 +127,10 @@ function signBundle(): void {
|
|
|
113
127
|
`codesign --force --sign -${entFlag} --identifier com.arach.lattices '${bundlePath}'`,
|
|
114
128
|
{ stdio: "pipe" }
|
|
115
129
|
);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
if (existsSync(tempBinaryPath)) rmSync(tempBinaryPath, { force: true });
|
|
133
|
+
} catch {}
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
function writeInfoPlist(): void {
|
|
@@ -304,11 +322,48 @@ async function ensureBinary(): Promise<void> {
|
|
|
304
322
|
process.exit(1);
|
|
305
323
|
}
|
|
306
324
|
|
|
325
|
+
function spawnDetachedUpdateWorker(extraArgs: string[] = [], shouldLaunch = false): void {
|
|
326
|
+
const workerArgs = [
|
|
327
|
+
selfScriptPath,
|
|
328
|
+
"update",
|
|
329
|
+
"--worker",
|
|
330
|
+
...(shouldLaunch ? ["--launch"] : []),
|
|
331
|
+
...extraArgs,
|
|
332
|
+
];
|
|
333
|
+
const child = spawn(process.execPath, workerArgs, {
|
|
334
|
+
cwd: cliRoot,
|
|
335
|
+
detached: true,
|
|
336
|
+
stdio: "ignore",
|
|
337
|
+
});
|
|
338
|
+
child.unref();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function updateApp(extraArgs: string[] = [], shouldLaunch = false): Promise<void> {
|
|
342
|
+
const wasRunning = isRunning();
|
|
343
|
+
if (wasRunning) {
|
|
344
|
+
quit();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const downloaded = await download();
|
|
348
|
+
if (!downloaded) {
|
|
349
|
+
console.error("Update failed.");
|
|
350
|
+
if (wasRunning || shouldLaunch || extraArgs.length > 0) {
|
|
351
|
+
launch(extraArgs);
|
|
352
|
+
}
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
relaunchIfNeeded(shouldLaunch || wasRunning || extraArgs.length > 0, extraArgs);
|
|
357
|
+
}
|
|
358
|
+
|
|
307
359
|
const cmd = process.argv[2];
|
|
308
360
|
const flags = process.argv.slice(3);
|
|
309
361
|
const launchFlags: string[] = [];
|
|
310
362
|
if (flags.includes("--diagnostics") || flags.includes("-d")) launchFlags.push("--diagnostics");
|
|
311
363
|
if (flags.includes("--screen-map") || flags.includes("-m")) launchFlags.push("--screen-map");
|
|
364
|
+
const shouldLaunchAfterUpdate = flags.includes("--launch") || launchFlags.length > 0;
|
|
365
|
+
const shouldDetachUpdate = flags.includes("--detach");
|
|
366
|
+
const isUpdateWorker = flags.includes("--worker");
|
|
312
367
|
|
|
313
368
|
if (cmd === "build") {
|
|
314
369
|
if (!hasSwift()) {
|
|
@@ -334,6 +389,13 @@ if (cmd === "build") {
|
|
|
334
389
|
process.exit(1);
|
|
335
390
|
}
|
|
336
391
|
launch(launchFlags);
|
|
392
|
+
} else if (cmd === "update") {
|
|
393
|
+
if (shouldDetachUpdate && !isUpdateWorker) {
|
|
394
|
+
spawnDetachedUpdateWorker(launchFlags, shouldLaunchAfterUpdate);
|
|
395
|
+
console.log("lattices app update started.");
|
|
396
|
+
} else {
|
|
397
|
+
await updateApp(launchFlags, shouldLaunchAfterUpdate);
|
|
398
|
+
}
|
|
337
399
|
} else {
|
|
338
400
|
await ensureBinary();
|
|
339
401
|
launch(launchFlags);
|
package/bin/lattices-dev
CHANGED
|
@@ -34,6 +34,8 @@ sign_bundle() {
|
|
|
34
34
|
local identity sign_status=0
|
|
35
35
|
local -a ent_flags=()
|
|
36
36
|
|
|
37
|
+
rm -f "$BUNDLE_BIN".cstemp
|
|
38
|
+
|
|
37
39
|
if [ -f "$ENTITLEMENTS" ]; then
|
|
38
40
|
ent_flags=(--entitlements "$ENTITLEMENTS")
|
|
39
41
|
fi
|
|
@@ -53,6 +55,8 @@ sign_bundle() {
|
|
|
53
55
|
dim "No usable signing identity found. Using ad-hoc signature."
|
|
54
56
|
codesign --force --sign - "${ent_flags[@]}" --identifier com.arach.lattices "$BUNDLE"
|
|
55
57
|
fi
|
|
58
|
+
|
|
59
|
+
rm -f "$BUNDLE_BIN".cstemp
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
write_info_plist() {
|
package/bin/lattices.ts
CHANGED
|
@@ -47,7 +47,7 @@ function hasTmux(): boolean {
|
|
|
47
47
|
const tmuxRequiredCommands = new Set([
|
|
48
48
|
"init", "ls", "list", "kill", "rm", "sync", "reconcile",
|
|
49
49
|
"restart", "respawn", "group", "groups", "tab", "status",
|
|
50
|
-
"inventory", "
|
|
50
|
+
"inventory", "sessions",
|
|
51
51
|
]);
|
|
52
52
|
|
|
53
53
|
function requireTmux(command: string | undefined): void {
|
|
@@ -56,7 +56,8 @@ function requireTmux(command: string | undefined): void {
|
|
|
56
56
|
const isImplicitCreate = command && !tmuxRequiredCommands.has(command)
|
|
57
57
|
&& !["search", "s", "focus", "place", "tile", "t", "windows", "window",
|
|
58
58
|
"voice", "call", "layer", "layers", "diag", "diagnostics", "scan",
|
|
59
|
-
"ocr", "daemon", "dev", "app", "mouse", "
|
|
59
|
+
"ocr", "daemon", "dev", "app", "mouse", "assistant",
|
|
60
|
+
"help", "-h", "--help"].includes(command);
|
|
60
61
|
|
|
61
62
|
if (command && !tmuxRequiredCommands.has(command) && !isImplicitCreate) return;
|
|
62
63
|
|
|
@@ -1182,6 +1183,34 @@ async function voiceCommand(subcommand?: string, ...rest: string[]): Promise<voi
|
|
|
1182
1183
|
}
|
|
1183
1184
|
}
|
|
1184
1185
|
|
|
1186
|
+
async function assistantCommand(subcommand?: string, ...rest: string[]): Promise<void> {
|
|
1187
|
+
if (subcommand !== "plan") {
|
|
1188
|
+
console.log("Usage: lattices assistant plan <text> [--json]");
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const jsonOut = rest.includes("--json");
|
|
1193
|
+
const text = rest.filter((arg) => arg !== "--json").join(" ").trim();
|
|
1194
|
+
if (!text) {
|
|
1195
|
+
console.log("Usage: lattices assistant plan <text> [--json]");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const { tryLocalAssistantPlan } = await import("./assistant-intelligence.ts");
|
|
1200
|
+
const result = tryLocalAssistantPlan(text) ?? {
|
|
1201
|
+
actions: [],
|
|
1202
|
+
spoken: "No local TS plan matched.",
|
|
1203
|
+
_meta: { source: "local-rule", matched: false },
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
if (jsonOut) {
|
|
1207
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
console.log(result.spoken);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1185
1214
|
async function callCommand(method?: string, ...rest: string[]): Promise<void> {
|
|
1186
1215
|
if (!method) {
|
|
1187
1216
|
console.log("Usage: lattices call <method> [params-json]");
|
|
@@ -1401,14 +1430,14 @@ async function diagCommand(limit?: string): Promise<void> {
|
|
|
1401
1430
|
}
|
|
1402
1431
|
}
|
|
1403
1432
|
|
|
1404
|
-
async function distributeCommand(): Promise<void> {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1433
|
+
async function distributeCommand(rawArgs: string[] = []): Promise<void> {
|
|
1434
|
+
const request = parseSpaceOptimizeArgs(rawArgs, "visible");
|
|
1435
|
+
await optimizeWindowsCommand(request, "Distributed");
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
async function tileFamilyCommand(rawArgs: string[]): Promise<void> {
|
|
1439
|
+
const request = parseSpaceOptimizeArgs(rawArgs, "active-app");
|
|
1440
|
+
await optimizeWindowsCommand(request, "Smart-tiled");
|
|
1412
1441
|
}
|
|
1413
1442
|
|
|
1414
1443
|
async function daemonLsCommand(): Promise<boolean> {
|
|
@@ -1711,7 +1740,8 @@ Usage:
|
|
|
1711
1740
|
lattices windows [--json] List all desktop windows (daemon required)
|
|
1712
1741
|
lattices sessions [--json] List active tmux sessions via daemon
|
|
1713
1742
|
lattices tile <position> Tile the frontmost window (left, right, top, etc.)
|
|
1714
|
-
lattices
|
|
1743
|
+
lattices tile family [app] [region] Smart-grid the frontmost app family, or a named app
|
|
1744
|
+
lattices distribute [app] [region] Smart-grid visible windows or just one app (daemon required)
|
|
1715
1745
|
lattices layer [name|index] List layers or switch by name/index (daemon required)
|
|
1716
1746
|
lattices layer create <name> [wid:N ...] [--json '<specs>'] Create a session layer
|
|
1717
1747
|
lattices layer snap [name] Snapshot visible windows into a session layer
|
|
@@ -1721,6 +1751,7 @@ Usage:
|
|
|
1721
1751
|
lattices voice status Voice provider status
|
|
1722
1752
|
lattices voice simulate <t> Parse and execute a voice command
|
|
1723
1753
|
lattices voice intents List all available intents
|
|
1754
|
+
lattices assistant plan <t> Preview the TS assistant planner
|
|
1724
1755
|
lattices call <method> [p] Raw daemon API call (params as JSON)
|
|
1725
1756
|
lattices scan Show text from all visible windows
|
|
1726
1757
|
lattices scan --full Full text dump
|
|
@@ -1737,6 +1768,7 @@ Usage:
|
|
|
1737
1768
|
lattices daemon status Show daemon status
|
|
1738
1769
|
lattices diag [limit] Show diagnostic log entries
|
|
1739
1770
|
lattices app Launch the menu bar companion app
|
|
1771
|
+
lattices app update Download the latest menu bar app and relaunch
|
|
1740
1772
|
lattices app build Rebuild the menu bar app
|
|
1741
1773
|
lattices app restart Rebuild and relaunch the menu bar app
|
|
1742
1774
|
lattices app quit Stop the menu bar app
|
|
@@ -1901,6 +1933,69 @@ const tilePresets: Record<string, (s: ScreenBounds) => number[]> = {
|
|
|
1901
1933
|
"right-third": (s) => [s.x + Math.round(s.w * 0.667), s.y, s.x + s.w, s.y + s.h],
|
|
1902
1934
|
};
|
|
1903
1935
|
|
|
1936
|
+
type SpaceOptimizeScope = "visible" | "active-app" | "app";
|
|
1937
|
+
|
|
1938
|
+
interface SpaceOptimizeRequest {
|
|
1939
|
+
scope: SpaceOptimizeScope;
|
|
1940
|
+
app?: string;
|
|
1941
|
+
region?: string;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function isPlacementToken(value?: string): boolean {
|
|
1945
|
+
if (!value) return false;
|
|
1946
|
+
const normalized = value.toLowerCase();
|
|
1947
|
+
return normalized in tilePresets || /^grid:\d+x\d+:\d+,\d+$/i.test(normalized);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function parseSpaceOptimizeArgs(rawArgs: string[], defaultScope: SpaceOptimizeScope): SpaceOptimizeRequest {
|
|
1951
|
+
const parts = rawArgs.filter(Boolean);
|
|
1952
|
+
if (!parts.length) return { scope: defaultScope };
|
|
1953
|
+
|
|
1954
|
+
const last = parts[parts.length - 1];
|
|
1955
|
+
const region = isPlacementToken(last) ? last : undefined;
|
|
1956
|
+
const appParts = region ? parts.slice(0, -1) : parts;
|
|
1957
|
+
const app = appParts.length ? appParts.join(" ") : undefined;
|
|
1958
|
+
|
|
1959
|
+
if (app) return { scope: "app", app, region };
|
|
1960
|
+
return { scope: defaultScope, region };
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function formatOptimizeTarget(request: SpaceOptimizeRequest): string {
|
|
1964
|
+
if (request.app) return `"${request.app}"`;
|
|
1965
|
+
return request.scope === "active-app" ? "the frontmost app" : "all visible windows";
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
async function optimizeWindowsCommand(
|
|
1969
|
+
request: SpaceOptimizeRequest,
|
|
1970
|
+
successVerb: string
|
|
1971
|
+
): Promise<void> {
|
|
1972
|
+
try {
|
|
1973
|
+
const { daemonCall } = await getDaemonClient();
|
|
1974
|
+
const params: Record<string, unknown> = {
|
|
1975
|
+
scope: request.scope,
|
|
1976
|
+
strategy: "balanced",
|
|
1977
|
+
};
|
|
1978
|
+
if (request.app) params.app = request.app;
|
|
1979
|
+
if (request.region) params.region = request.region;
|
|
1980
|
+
|
|
1981
|
+
const result = await daemonCall("space.optimize", params) as any;
|
|
1982
|
+
const count = result?.windowCount ?? 0;
|
|
1983
|
+
const target = formatOptimizeTarget(request);
|
|
1984
|
+
const regionSuffix = request.region ? ` in the ${request.region} region` : "";
|
|
1985
|
+
|
|
1986
|
+
if (count === 0) {
|
|
1987
|
+
console.log(`No eligible windows found for ${target}${regionSuffix}.`);
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
console.log(
|
|
1992
|
+
`${successVerb} ${count} window${count === 1 ? "" : "s"} for ${target}${regionSuffix}.`
|
|
1993
|
+
);
|
|
1994
|
+
} catch {
|
|
1995
|
+
console.log("Daemon not running. Start with: lattices app");
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1904
1999
|
function tileWindow(position: string): void {
|
|
1905
2000
|
const preset = tilePresets[position];
|
|
1906
2001
|
if (!preset) {
|
|
@@ -2098,14 +2193,27 @@ switch (command) {
|
|
|
2098
2193
|
}
|
|
2099
2194
|
break;
|
|
2100
2195
|
case "distribute":
|
|
2101
|
-
await distributeCommand();
|
|
2196
|
+
await distributeCommand(args.slice(1));
|
|
2102
2197
|
break;
|
|
2103
2198
|
case "tile":
|
|
2104
2199
|
case "t":
|
|
2105
|
-
if (args[1]) {
|
|
2200
|
+
if (args[1] === "family" || args[1] === "app") {
|
|
2201
|
+
await tileFamilyCommand(args.slice(2));
|
|
2202
|
+
} else if (args[1] === "all") {
|
|
2203
|
+
await distributeCommand(args.slice(2));
|
|
2204
|
+
} else if (args[1]) {
|
|
2106
2205
|
tileWindow(args[1]);
|
|
2107
2206
|
} else {
|
|
2108
|
-
console.log("Usage:
|
|
2207
|
+
console.log("Usage:");
|
|
2208
|
+
console.log(" lattices tile <position>");
|
|
2209
|
+
console.log(" lattices tile family [app-name] [region]");
|
|
2210
|
+
console.log(" lattices tile all [app-name] [region]\n");
|
|
2211
|
+
console.log("Examples:");
|
|
2212
|
+
console.log(" lattices tile left");
|
|
2213
|
+
console.log(" lattices tile family");
|
|
2214
|
+
console.log(" lattices tile family right");
|
|
2215
|
+
console.log(" lattices tile family iTerm2");
|
|
2216
|
+
console.log(" lattices tile all Google Chrome left\n");
|
|
2109
2217
|
console.log("Positions: left, right, top, bottom, top-left, top-right,");
|
|
2110
2218
|
console.log(" bottom-left, bottom-right, maximize, center,");
|
|
2111
2219
|
console.log(" left-third, center-third, right-third");
|
|
@@ -2141,6 +2249,9 @@ switch (command) {
|
|
|
2141
2249
|
case "voice":
|
|
2142
2250
|
await voiceCommand(args[1], ...args.slice(2));
|
|
2143
2251
|
break;
|
|
2252
|
+
case "assistant":
|
|
2253
|
+
await assistantCommand(args[1], ...args.slice(2));
|
|
2254
|
+
break;
|
|
2144
2255
|
case "call":
|
|
2145
2256
|
await callCommand(args[1], ...args.slice(2));
|
|
2146
2257
|
break;
|
package/docs/agents.md
CHANGED
|
@@ -140,3 +140,17 @@ same canonical actions:
|
|
|
140
140
|
|
|
141
141
|
That keeps the interaction layer flexible while the executor stays
|
|
142
142
|
predictable.
|
|
143
|
+
|
|
144
|
+
## Assistant intelligence boundary
|
|
145
|
+
|
|
146
|
+
Assistant planning lives in TypeScript where possible:
|
|
147
|
+
|
|
148
|
+
- `bin/assistant-intelligence.ts` owns the intent catalog, prompt assembly,
|
|
149
|
+
local rule planner, desktop snapshot formatting, and plan normalization.
|
|
150
|
+
- `bin/handsoff-worker.ts` and `bin/handsoff-infer.ts` call that module before
|
|
151
|
+
falling back to model inference.
|
|
152
|
+
- Swift should remain the macOS execution layer: hotkeys, windows, AX/CG,
|
|
153
|
+
SkyLight, panels, and visual feedback.
|
|
154
|
+
|
|
155
|
+
Use `lattices assistant plan <text> --json` to inspect the TS planner without
|
|
156
|
+
launching the app or mutating the desktop.
|
package/docs/api.md
CHANGED
|
@@ -164,10 +164,65 @@ try {
|
|
|
164
164
|
|
|
165
165
|
| Method | Type | Description |
|
|
166
166
|
|--------|------|-------------|
|
|
167
|
+
| `deck.manifest` | read | Shared companion deck manifest |
|
|
168
|
+
| `deck.snapshot` | read | Current companion deck runtime snapshot |
|
|
169
|
+
| `deck.perform` | write | Perform a companion deck action |
|
|
167
170
|
| `daemon.status` | read | Health check and stats |
|
|
168
171
|
| `api.schema` | read | Full API schema for self-discovery |
|
|
169
172
|
| `diagnostics.list` | read | Recent diagnostic entries |
|
|
170
173
|
|
|
174
|
+
#### `deck.manifest`
|
|
175
|
+
|
|
176
|
+
Return the shared `DeckKit` manifest exposed by the macOS app. This is
|
|
177
|
+
the contract a future Lattices companion can consume to discover pages,
|
|
178
|
+
capabilities, and security mode.
|
|
179
|
+
|
|
180
|
+
**Params**: none
|
|
181
|
+
|
|
182
|
+
#### `deck.snapshot`
|
|
183
|
+
|
|
184
|
+
Return the current `DeckKit` runtime snapshot for the macOS host.
|
|
185
|
+
|
|
186
|
+
**Params**: none
|
|
187
|
+
|
|
188
|
+
**Returns**: a `DeckRuntimeSnapshot` object containing:
|
|
189
|
+
|
|
190
|
+
- `voice`
|
|
191
|
+
- `desktop`
|
|
192
|
+
- `switcher`
|
|
193
|
+
- `history`
|
|
194
|
+
- `questions`
|
|
195
|
+
|
|
196
|
+
#### `deck.perform`
|
|
197
|
+
|
|
198
|
+
Perform a `DeckKit` action against the running macOS host and return a
|
|
199
|
+
`DeckActionResult`.
|
|
200
|
+
|
|
201
|
+
**Params**:
|
|
202
|
+
|
|
203
|
+
| Field | Type | Required | Description |
|
|
204
|
+
|-------|------|----------|-------------|
|
|
205
|
+
| `pageID` | string | no | Deck page ID |
|
|
206
|
+
| `actionID` | string | yes | Deck action identifier |
|
|
207
|
+
| `payload` | object | no | Deck action payload |
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"id": "1",
|
|
214
|
+
"method": "deck.perform",
|
|
215
|
+
"params": {
|
|
216
|
+
"pageID": "layout",
|
|
217
|
+
"actionID": "layout.optimize",
|
|
218
|
+
"payload": {
|
|
219
|
+
"strategy": "balanced",
|
|
220
|
+
"region": "right"
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
171
226
|
#### `daemon.status`
|
|
172
227
|
|
|
173
228
|
Health check and basic stats.
|
package/docs/app.md
CHANGED
|
@@ -11,6 +11,7 @@ workspace from there.
|
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
lattices app # Build (or download) and launch
|
|
14
|
+
lattices app update # Download the latest release and relaunch
|
|
14
15
|
lattices app build # Rebuild from source
|
|
15
16
|
lattices app restart # Quit, rebuild, relaunch
|
|
16
17
|
lattices app quit # Stop the app
|
|
@@ -73,6 +74,7 @@ Available when `layers` are configured in `~/.lattices/workspace.json`
|
|
|
73
74
|
| Command | Description |
|
|
74
75
|
|-------------------|------------------------------------------|
|
|
75
76
|
| Settings | Open preferences (terminal, scan root) |
|
|
77
|
+
| Update Lattices | Download the latest release and relaunch |
|
|
76
78
|
| Diagnostics | View logs and debug info |
|
|
77
79
|
| Refresh Projects | Re-scan for .lattices.json configs |
|
|
78
80
|
| Quit Lattices | Exit the menu bar app |
|
|
@@ -166,6 +168,7 @@ The settings window has four tabs:
|
|
|
166
168
|
| Terminal | Which terminal to use (auto-detected from installed) |
|
|
167
169
|
| Mode | `learning` or `auto` (see below) |
|
|
168
170
|
| Scan Root | Directory to scan for .lattices.json configs (type a path or click Browse) |
|
|
171
|
+
| Updates | Download the latest release and relaunch the app |
|
|
169
172
|
|
|
170
173
|
**Mode** controls how the app handles session interaction:
|
|
171
174
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Companion Deck
|
|
2
|
+
|
|
3
|
+
This document defines the first extraction boundary for the Lattices
|
|
4
|
+
companion deck work.
|
|
5
|
+
|
|
6
|
+
## Goals
|
|
7
|
+
|
|
8
|
+
- Build the deck architecture in `lattices` first, without modifying
|
|
9
|
+
`talkie`.
|
|
10
|
+
- Treat `talkie` as the donor and reference implementation, not the
|
|
11
|
+
place where the first shared abstraction is born.
|
|
12
|
+
- Let `lattices` own Mac functionality.
|
|
13
|
+
- Let `talkie` continue to own Talkie-specific flows.
|
|
14
|
+
- Keep the transport and UI shell generic enough that both products can
|
|
15
|
+
embed the same deck later.
|
|
16
|
+
|
|
17
|
+
## Product Ownership
|
|
18
|
+
|
|
19
|
+
### Lattices owns
|
|
20
|
+
|
|
21
|
+
- Voice agent control
|
|
22
|
+
- Layout and screen state
|
|
23
|
+
- Application, window, tab, and task switching
|
|
24
|
+
- Session and layer switching
|
|
25
|
+
- Desktop questions and agent follow-up
|
|
26
|
+
- Action history and undo-oriented playback
|
|
27
|
+
|
|
28
|
+
### Talkie owns
|
|
29
|
+
|
|
30
|
+
- Dictation
|
|
31
|
+
- Memo recording
|
|
32
|
+
- Scratchpad and compose flows
|
|
33
|
+
- Capture-specific flows
|
|
34
|
+
- Talkie-branded deck pages and follow-up actions
|
|
35
|
+
|
|
36
|
+
### Shared deck kit owns
|
|
37
|
+
|
|
38
|
+
- Page model
|
|
39
|
+
- Action model
|
|
40
|
+
- Runtime snapshot model
|
|
41
|
+
- Security mode model
|
|
42
|
+
- App and task switcher model
|
|
43
|
+
- History feed model
|
|
44
|
+
- Generic host protocol
|
|
45
|
+
|
|
46
|
+
## Security Modes
|
|
47
|
+
|
|
48
|
+
The deck must support two security modes.
|
|
49
|
+
|
|
50
|
+
### Standalone
|
|
51
|
+
|
|
52
|
+
Used by a future standalone `Lattices Companion`.
|
|
53
|
+
|
|
54
|
+
- Bonjour discovery
|
|
55
|
+
- Local network only
|
|
56
|
+
- No Tailscale or external relay dependency
|
|
57
|
+
- QR or code-based pairing on top of the local network path
|
|
58
|
+
- Per-device keypairs
|
|
59
|
+
- Signed requests with nonce and timestamp protection
|
|
60
|
+
- Local companion gateway with a reduced action surface
|
|
61
|
+
|
|
62
|
+
### Embedded
|
|
63
|
+
|
|
64
|
+
Used when the deck is embedded inside `talkie`.
|
|
65
|
+
|
|
66
|
+
- Pairing, trust, transport, and signing are delegated to Talkie
|
|
67
|
+
- Lattices focuses only on local functionality and state
|
|
68
|
+
- The Lattices deck host does not need to own remote security in this mode
|
|
69
|
+
|
|
70
|
+
## First Lattices Companion Scope
|
|
71
|
+
|
|
72
|
+
The first iPad/iPhone companion for Lattices should focus on these pages:
|
|
73
|
+
|
|
74
|
+
1. Voice
|
|
75
|
+
2. Layout
|
|
76
|
+
3. Switch
|
|
77
|
+
4. History
|
|
78
|
+
|
|
79
|
+
These pages cover the highest-value mobile control loops without pulling
|
|
80
|
+
Talkie-specific concepts into the new product.
|
|
81
|
+
|
|
82
|
+
## Module Plan
|
|
83
|
+
|
|
84
|
+
### `swift/DeckKit`
|
|
85
|
+
|
|
86
|
+
Cross-product contract incubated in the Lattices repo first.
|
|
87
|
+
|
|
88
|
+
- Shared deck schema
|
|
89
|
+
- Security mode model
|
|
90
|
+
- Runtime snapshot model
|
|
91
|
+
- Host protocol
|
|
92
|
+
|
|
93
|
+
### `LatticesDeckHost`
|
|
94
|
+
|
|
95
|
+
Mac-side adapter owned by Lattices.
|
|
96
|
+
|
|
97
|
+
- Publishes deck pages and runtime state
|
|
98
|
+
- Maps deck actions to the existing Lattices desktop APIs
|
|
99
|
+
- Uses the existing desktop model, layout engine, switcher logic, and
|
|
100
|
+
voice agent surfaces
|
|
101
|
+
|
|
102
|
+
### `Lattices Companion`
|
|
103
|
+
|
|
104
|
+
Future iOS or iPadOS app that consumes `DeckKit` and the Lattices
|
|
105
|
+
companion gateway.
|
|
106
|
+
|
|
107
|
+
### `TalkieDeckHost`
|
|
108
|
+
|
|
109
|
+
Future Talkie-side adapter that adds Talkie-only pages on top of the
|
|
110
|
+
shared deck shell.
|
|
111
|
+
|
|
112
|
+
## Current Lattices Milestone
|
|
113
|
+
|
|
114
|
+
The first host-side integration now lives in the Lattices macOS app.
|
|
115
|
+
|
|
116
|
+
- `swift/DeckKit` continues to own the shared manifest, snapshot,
|
|
117
|
+
action, and security contract.
|
|
118
|
+
- `app/Sources/LatticesDeckHost.swift` is the first concrete Mac host.
|
|
119
|
+
- The menu bar app daemon now exposes:
|
|
120
|
+
- `deck.manifest`
|
|
121
|
+
- `deck.snapshot`
|
|
122
|
+
- `deck.perform`
|
|
123
|
+
|
|
124
|
+
That gives the future iPhone/iPad companion a stable local contract
|
|
125
|
+
before transport and pairing are finalized.
|
|
126
|
+
|
|
127
|
+
The current transport now runs as a local network bridge in the macOS
|
|
128
|
+
app with Bonjour discovery on port `5287` (`LATS` on a phone keypad).
|
|
129
|
+
Standalone mode now uses:
|
|
130
|
+
|
|
131
|
+
- local Mac approval for first-time device pairing
|
|
132
|
+
- per-device key agreement
|
|
133
|
+
- signed requests with nonce and timestamp checks
|
|
134
|
+
- encrypted deck payloads for snapshots, actions, and trackpad events
|
|
135
|
+
|
|
136
|
+
The bridge still keeps `/health`, `/deck/manifest`, and pairing
|
|
137
|
+
bootstrap lightweight so a new companion can connect and establish trust
|
|
138
|
+
without an external relay or Tailscale dependency.
|
|
139
|
+
|
|
140
|
+
## Initial Action Surface
|
|
141
|
+
|
|
142
|
+
The first deck action IDs are intentionally small and map to existing
|
|
143
|
+
desktop behavior:
|
|
144
|
+
|
|
145
|
+
- `voice.toggle`
|
|
146
|
+
- `voice.cancel`
|
|
147
|
+
- `layout.activateLayer`
|
|
148
|
+
- `layout.optimize`
|
|
149
|
+
- `layout.placeFrontmost`
|
|
150
|
+
- `switch.focusItem`
|
|
151
|
+
- `history.undoLast`
|
|
152
|
+
|
|
153
|
+
This keeps the first bridge focused on real Mac control loops instead
|
|
154
|
+
of inventing a second execution stack.
|
|
155
|
+
|
|
156
|
+
## Rollout Sequence
|
|
157
|
+
|
|
158
|
+
1. Leave Talkie untouched and use it as the donor reference.
|
|
159
|
+
2. Incubate `DeckKit` in `lattices`.
|
|
160
|
+
3. Build a clean Lattices companion around `Voice`, `Layout`,
|
|
161
|
+
`Switch`, and `History`.
|
|
162
|
+
4. Prove standalone local pairing and strong security for Lattices.
|
|
163
|
+
5. Harden the deck contract.
|
|
164
|
+
6. Retrofit the stabilized deck kit back into Talkie.
|
|
165
|
+
|
|
166
|
+
Embedded mode remains a first-class constraint throughout the rollout.
|
|
167
|
+
The standalone bridge must not leak into the shared deck contract in a
|
|
168
|
+
way that would make Talkie embedding awkward later.
|
|
169
|
+
|
|
170
|
+
## Vox
|
|
171
|
+
|
|
172
|
+
Vox is the preferred voice dependency for the Lattices companion path.
|
|
173
|
+
|
|
174
|
+
- Prefer direct embedding through `VoxCore` and `VoxEngine` for
|
|
175
|
+
in-process ASR and TTS inside Apple apps.
|
|
176
|
+
- Keep `VoxBridge` available as an optional daemon-style path when a
|
|
177
|
+
shared runtime is more appropriate than direct embedding.
|
|
178
|
+
- Keep the first contract in `DeckKit` voice-agnostic.
|
|
179
|
+
- Let `LatticesDeckHost` decide whether voice is served by embedded Vox
|
|
180
|
+
or another local service surface.
|