vicem-cli 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/vicem/SKILL.md +57 -0
- package/AGENTS.snippet.md +7 -0
- package/LICENSE +21 -0
- package/README.md +185 -2
- package/SECURITY.md +35 -0
- package/SHA256SUMS +4 -0
- package/index.html +1410 -0
- package/logo-dark-transparent.png +0 -0
- package/logo-light-transparent.png +0 -0
- package/package.json +51 -4
- package/setup.bat +13 -0
- package/setup.command +12 -0
- package/vicem-gui.mjs +3452 -0
- package/vicem.mjs +5857 -0
package/vicem-gui.mjs
ADDED
|
@@ -0,0 +1,3452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Vicem Desktop Command Center (v9 — Real Native App Launcher + Step-by-Step Verification Receipt)
|
|
4
|
+
* - Default Auto-Open ON: clicking "Hand off ->" actually launches the target agent on macOS:
|
|
5
|
+
* - Claude Code / OpenAI Codex: generates an executable `.vicem/launch-<agent>.command` with full
|
|
6
|
+
* binary path (`~/.local/bin/claude` or `~/.local/bin/codex`), `cd`s into the workspace, passes
|
|
7
|
+
* the initial Baton prompt, and opens it natively in Terminal.app (`open -a Terminal ...`).
|
|
8
|
+
* - Antigravity / VS Code: opens the project directory directly in `/Applications/Antigravity.app`
|
|
9
|
+
* or `/Applications/Visual Studio Code.app`.
|
|
10
|
+
* - Web Chat: copies Baton to clipboard and opens `https://chatgpt.com/?q=...`.
|
|
11
|
+
* - Live 5-Step Handoff Verification Panel: proves every background step (file size on disk,
|
|
12
|
+
* mounted rule file verification, real `pbpaste` clipboard byte check, and process launch status)
|
|
13
|
+
* with a live collapsible preview of the actual `.vicem/BATON.md` content.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import http from 'node:http';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
DoctorDiagnostics,
|
|
25
|
+
SmartInferenceEngine,
|
|
26
|
+
GitCollector,
|
|
27
|
+
DeterministicTaskExtractor,
|
|
28
|
+
BatonIaBuilder,
|
|
29
|
+
RuleMountAdapter,
|
|
30
|
+
ClipboardAdapter,
|
|
31
|
+
VicemApp,
|
|
32
|
+
} from './vicem.mjs';
|
|
33
|
+
|
|
34
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
+
const __dirname = path.dirname(__filename);
|
|
36
|
+
const DEFAULT_PROJECT_ROOT =
|
|
37
|
+
process.env.VICEM_PROJECT_ROOT && fs.existsSync(process.env.VICEM_PROJECT_ROOT)
|
|
38
|
+
? path.resolve(process.env.VICEM_PROJECT_ROOT)
|
|
39
|
+
: fs.existsSync(path.join(process.cwd(), '.git'))
|
|
40
|
+
? process.cwd()
|
|
41
|
+
: path.basename(__dirname) === 'prototype'
|
|
42
|
+
? path.resolve(__dirname, '..')
|
|
43
|
+
: __dirname;
|
|
44
|
+
|
|
45
|
+
const LOGO_DARK_PATH =
|
|
46
|
+
[
|
|
47
|
+
path.resolve(__dirname, 'logo-dark-transparent.png'),
|
|
48
|
+
path.resolve(__dirname, 'release', 'logo-dark-transparent.png'),
|
|
49
|
+
path.resolve(__dirname, '..', 'logo-dark-transparent.png'),
|
|
50
|
+
path.resolve(__dirname, '..', 'release', 'logo-dark-transparent.png'),
|
|
51
|
+
].find((p) => fs.existsSync(p)) || path.resolve(__dirname, 'logo-dark-transparent.png');
|
|
52
|
+
|
|
53
|
+
const LOGO_LIGHT_PATH =
|
|
54
|
+
[
|
|
55
|
+
path.resolve(__dirname, 'logo-light-transparent.png'),
|
|
56
|
+
path.resolve(__dirname, 'release', 'logo-light-transparent.png'),
|
|
57
|
+
path.resolve(__dirname, '..', 'logo-light-transparent.png'),
|
|
58
|
+
path.resolve(__dirname, '..', 'release', 'logo-light-transparent.png'),
|
|
59
|
+
].find((p) => fs.existsSync(p)) || path.resolve(__dirname, 'logo-light-transparent.png');
|
|
60
|
+
|
|
61
|
+
function shortenHome(fullPath, home) {
|
|
62
|
+
if (!fullPath) return '—';
|
|
63
|
+
if (fullPath.startsWith(home)) {
|
|
64
|
+
return '~' + fullPath.slice(home.length);
|
|
65
|
+
}
|
|
66
|
+
return fullPath;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatAgo(mtimeMs) {
|
|
70
|
+
if (!mtimeMs) return '—';
|
|
71
|
+
const diffSec = Math.max(1, Math.floor((Date.now() - mtimeMs) / 1000));
|
|
72
|
+
if (diffSec < 60) return 'Just now';
|
|
73
|
+
if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`;
|
|
74
|
+
if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`;
|
|
75
|
+
return `${Math.floor(diffSec / 86400)}d ago`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatResetShort(resetEpochSec) {
|
|
79
|
+
if (!resetEpochSec) return '';
|
|
80
|
+
const diffSec = Math.max(0, Math.floor(resetEpochSec - Date.now() / 1000));
|
|
81
|
+
if (diffSec <= 0) return 'Resets now';
|
|
82
|
+
const d = Math.floor(diffSec / 86400);
|
|
83
|
+
const h = Math.floor((diffSec % 86400) / 3600);
|
|
84
|
+
const m = Math.floor((diffSec % 3600) / 60);
|
|
85
|
+
if (d > 0) return `Resets in ${d}d ${h}h`;
|
|
86
|
+
if (h > 0) return `Resets in ${h}h ${m}m`;
|
|
87
|
+
return `Resets in ${m}m`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function shellQuote(value) {
|
|
91
|
+
return "'" + String(value).replaceAll("'", "'\"'\"'") + "'";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getIdeUserDir(ideName, home) {
|
|
95
|
+
if (process.platform === 'darwin') {
|
|
96
|
+
return path.join(home, 'Library', 'Application Support', ideName, 'User');
|
|
97
|
+
}
|
|
98
|
+
if (process.platform === 'win32') {
|
|
99
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
100
|
+
return path.join(appData, ideName, 'User');
|
|
101
|
+
}
|
|
102
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
103
|
+
return path.join(configHome, ideName, 'User');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readClipboardCrossPlatform() {
|
|
107
|
+
try {
|
|
108
|
+
if (process.platform === 'darwin') {
|
|
109
|
+
return execSync('pbpaste', { encoding: 'utf8', timeout: 1200 });
|
|
110
|
+
}
|
|
111
|
+
if (process.platform === 'win32') {
|
|
112
|
+
return execSync('powershell -NoProfile -Command Get-Clipboard', {
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
timeout: 1500,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
for (const cmd of ['wl-paste -n', 'xclip -o -selection clipboard', 'xsel -ob']) {
|
|
118
|
+
try {
|
|
119
|
+
return execSync(cmd, { encoding: 'utf8', timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
} catch {}
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function openExternalCrossPlatform(urlOrPath, macAppPath = null) {
|
|
127
|
+
if (process.platform === 'darwin') {
|
|
128
|
+
const args = macAppPath ? ['-a', macAppPath, urlOrPath] : [urlOrPath];
|
|
129
|
+
return spawnSync('open', args, { encoding: 'utf8' });
|
|
130
|
+
}
|
|
131
|
+
if (process.platform === 'win32') {
|
|
132
|
+
return spawnSync('cmd', ['/c', 'start', '""', urlOrPath], {
|
|
133
|
+
encoding: 'utf8',
|
|
134
|
+
windowsHide: true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return spawnSync('xdg-open', [urlOrPath], { encoding: 'utf8' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveAgentBinary(agentId, home) {
|
|
141
|
+
const isWin = process.platform === 'win32';
|
|
142
|
+
const exts = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
143
|
+
|
|
144
|
+
for (const ext of exts) {
|
|
145
|
+
const localBin = path.join(home, '.local', 'bin', agentId + ext);
|
|
146
|
+
if (fs.existsSync(localBin)) return localBin;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (process.platform === 'darwin') {
|
|
150
|
+
if (agentId === 'claude') {
|
|
151
|
+
const appSupport = path.join(home, 'Library', 'Application Support', 'Claude', 'claude-code');
|
|
152
|
+
if (fs.existsSync(appSupport)) {
|
|
153
|
+
for (const ver of fs.readdirSync(appSupport)) {
|
|
154
|
+
const p = path.join(appSupport, ver);
|
|
155
|
+
if (fs.statSync(p).isDirectory()) {
|
|
156
|
+
for (const sub of fs.readdirSync(p)) {
|
|
157
|
+
const bin = path.join(p, sub, 'claude.app', 'Contents', 'MacOS', 'claude');
|
|
158
|
+
if (fs.existsSync(bin)) return bin;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (agentId === 'codex') {
|
|
165
|
+
const codexAppBin = '/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex';
|
|
166
|
+
if (fs.existsSync(codexAppBin)) return codexAppBin;
|
|
167
|
+
}
|
|
168
|
+
if (agentId === 'cursor') {
|
|
169
|
+
const p = '/Applications/Cursor.app/Contents/Resources/app/bin/cursor';
|
|
170
|
+
if (fs.existsSync(p)) return p;
|
|
171
|
+
}
|
|
172
|
+
if (agentId === 'windsurf') {
|
|
173
|
+
const p = '/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf';
|
|
174
|
+
if (fs.existsSync(p)) return p;
|
|
175
|
+
}
|
|
176
|
+
if (agentId === 'vscode') {
|
|
177
|
+
const p = '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code';
|
|
178
|
+
if (fs.existsSync(p)) return p;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (isWin) {
|
|
183
|
+
const binName = agentId === 'vscode' ? 'code' : agentId;
|
|
184
|
+
try {
|
|
185
|
+
const found = execSync(`where ${binName}`, {
|
|
186
|
+
encoding: 'utf8',
|
|
187
|
+
timeout: 2500,
|
|
188
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
189
|
+
})
|
|
190
|
+
.trim()
|
|
191
|
+
.split(/\r?\n/)[0];
|
|
192
|
+
if (found && fs.existsSync(found)) return found;
|
|
193
|
+
} catch {}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Unix/macOS login shell lookup
|
|
198
|
+
const binName = agentId === 'vscode' ? 'code' : agentId === 'antigravity' ? 'agy' : agentId;
|
|
199
|
+
try {
|
|
200
|
+
const shellBin = process.platform === 'darwin' ? '/bin/zsh' : '/bin/sh';
|
|
201
|
+
const found = execSync(`${shellBin} -lic 'command -v ${binName}'`, {
|
|
202
|
+
encoding: 'utf8',
|
|
203
|
+
timeout: 3000,
|
|
204
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
205
|
+
})
|
|
206
|
+
.trim()
|
|
207
|
+
.split('\n')
|
|
208
|
+
.pop();
|
|
209
|
+
if (found && found.startsWith('/') && fs.existsSync(found)) return found;
|
|
210
|
+
} catch {}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isPidAlive(pid) {
|
|
215
|
+
try {
|
|
216
|
+
process.kill(pid, 0);
|
|
217
|
+
return true;
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return e.code === 'EPERM';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Polls the status file written by the launcher script until the agent reports in or we time out.
|
|
225
|
+
*/
|
|
226
|
+
async function waitForLaunchStatus(statusPath, timeoutMs = 8000) {
|
|
227
|
+
const deadline = Date.now() + timeoutMs;
|
|
228
|
+
while (Date.now() < deadline) {
|
|
229
|
+
if (fs.existsSync(statusPath)) {
|
|
230
|
+
try {
|
|
231
|
+
const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
232
|
+
if (status.state === 'started') {
|
|
233
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
234
|
+
return { ...status, alive: isPidAlive(status.pid) };
|
|
235
|
+
}
|
|
236
|
+
return status;
|
|
237
|
+
} catch {}
|
|
238
|
+
}
|
|
239
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
240
|
+
}
|
|
241
|
+
return { state: 'timeout' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function detectRunningProcesses() {
|
|
245
|
+
const result = {
|
|
246
|
+
antigravity: false,
|
|
247
|
+
claude: false,
|
|
248
|
+
codex: false,
|
|
249
|
+
cursor: false,
|
|
250
|
+
vscode: false,
|
|
251
|
+
gemini: false,
|
|
252
|
+
windsurf: false,
|
|
253
|
+
cline: false,
|
|
254
|
+
opencode: false,
|
|
255
|
+
aider: false,
|
|
256
|
+
goose: false,
|
|
257
|
+
};
|
|
258
|
+
try {
|
|
259
|
+
if (process.platform === 'win32') {
|
|
260
|
+
const out = execSync('tasklist /FO CSV /NH', { encoding: 'utf8', timeout: 1500 });
|
|
261
|
+
if (/Antigravity/i.test(out)) result.antigravity = true;
|
|
262
|
+
if (/claude\.exe/i.test(out)) result.claude = true;
|
|
263
|
+
if (/codex\.exe/i.test(out)) result.codex = true;
|
|
264
|
+
if (/Cursor\.exe/i.test(out)) result.cursor = true;
|
|
265
|
+
if (/Windsurf\.exe/i.test(out)) result.windsurf = true;
|
|
266
|
+
if (/opencode/i.test(out)) result.opencode = true;
|
|
267
|
+
if (/aider/i.test(out)) result.aider = true;
|
|
268
|
+
if (/goose/i.test(out)) result.goose = true;
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
const out = execSync('ps -Ao pcpu,comm,args', { encoding: 'utf8', timeout: 1500 });
|
|
272
|
+
for (const line of out.split('\n')) {
|
|
273
|
+
if (!line || line.includes('vicem-gui')) continue;
|
|
274
|
+
if (/language_server_macos|Antigravity\.app|antigravity/i.test(line)) {
|
|
275
|
+
const cpu = parseFloat(line.trim().split(/\s+/)[0] || '0');
|
|
276
|
+
if (cpu > 0.5) result.antigravity = true;
|
|
277
|
+
}
|
|
278
|
+
if (/(?:^|\s|\/)claude(?:\s|$)/i.test(line) && !/Claude\.app\/Contents\/MacOS\/Claude/i.test(line)) {
|
|
279
|
+
result.claude = true;
|
|
280
|
+
}
|
|
281
|
+
if (/(?:^|\s|\/)codex(?:\s|$)/i.test(line) && !/app-server/i.test(line)) {
|
|
282
|
+
result.codex = true;
|
|
283
|
+
}
|
|
284
|
+
if (/(?:^|\s|\/)gemini(?:\s|$)/i.test(line)) result.gemini = true;
|
|
285
|
+
if (/(?:^|\s|\/)opencode(?:\s|$)/i.test(line)) result.opencode = true;
|
|
286
|
+
if (/(?:^|\s|\/)aider(?:\s|$)/i.test(line)) result.aider = true;
|
|
287
|
+
if (/(?:^|\s|\/)goose(?:\s|$)/i.test(line)) result.goose = true;
|
|
288
|
+
}
|
|
289
|
+
} catch {}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function collectRealAgentTelemetry(home) {
|
|
294
|
+
const now = Date.now();
|
|
295
|
+
const fiveHoursAgo = now - 5 * 3600 * 1000;
|
|
296
|
+
const sevenDaysAgo = now - 7 * 86400 * 1000;
|
|
297
|
+
const discoveredWorkspaces = new Map();
|
|
298
|
+
|
|
299
|
+
const addWorkspace = (dirPath, agentName, mtimeMs, lastPrompt = '') => {
|
|
300
|
+
if (!dirPath || typeof dirPath !== 'string') return;
|
|
301
|
+
const cleaned = dirPath.replace(/^file:\/\//, '').trim();
|
|
302
|
+
if (!cleaned.startsWith('/') || !fs.existsSync(cleaned)) return;
|
|
303
|
+
const prev = discoveredWorkspaces.get(cleaned);
|
|
304
|
+
if (!prev || mtimeMs > prev.mtimeMs) {
|
|
305
|
+
discoveredWorkspaces.set(cleaned, {
|
|
306
|
+
path: cleaned,
|
|
307
|
+
name: path.basename(cleaned),
|
|
308
|
+
shortPath: shortenHome(cleaned, home),
|
|
309
|
+
lastAgent: agentName,
|
|
310
|
+
mtimeMs,
|
|
311
|
+
lastPrompt: lastPrompt || prev?.lastPrompt || '',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
addWorkspace(DEFAULT_PROJECT_ROOT, 'Antigravity', now, '');
|
|
317
|
+
|
|
318
|
+
// 1. Antigravity IDE
|
|
319
|
+
let agyLatestMtime = 0;
|
|
320
|
+
let agyLatestPrompt = '';
|
|
321
|
+
let agyLatestWorkspace = DEFAULT_PROJECT_ROOT;
|
|
322
|
+
let agyActiveBytes5h = 0;
|
|
323
|
+
let agyTotalSessions = 0;
|
|
324
|
+
const brainDir = path.join(home, '.gemini', 'antigravity', 'brain');
|
|
325
|
+
try {
|
|
326
|
+
if (fs.existsSync(brainDir)) {
|
|
327
|
+
const entries = fs.readdirSync(brainDir);
|
|
328
|
+
agyTotalSessions = entries.length;
|
|
329
|
+
const recentFiles = [];
|
|
330
|
+
for (const d of entries) {
|
|
331
|
+
const tp = path.join(brainDir, d, '.system_generated', 'logs', 'transcript.jsonl');
|
|
332
|
+
try {
|
|
333
|
+
if (fs.existsSync(tp)) {
|
|
334
|
+
const st = fs.statSync(tp);
|
|
335
|
+
if (st.mtimeMs >= fiveHoursAgo) agyActiveBytes5h += st.size;
|
|
336
|
+
recentFiles.push({ path: tp, mtimeMs: st.mtimeMs, size: st.size });
|
|
337
|
+
}
|
|
338
|
+
} catch {}
|
|
339
|
+
}
|
|
340
|
+
recentFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
341
|
+
if (recentFiles[0]) {
|
|
342
|
+
agyLatestMtime = recentFiles[0].mtimeMs;
|
|
343
|
+
const raw = fs.readFileSync(recentFiles[0].path, 'utf8');
|
|
344
|
+
const wsMatch = raw.match(/Command Working Directory:\s*(\/[^\n\\<"]+)/);
|
|
345
|
+
if (wsMatch && wsMatch[1] && fs.existsSync(wsMatch[1].trim())) {
|
|
346
|
+
agyLatestWorkspace = wsMatch[1].trim();
|
|
347
|
+
}
|
|
348
|
+
const lines = raw.trim().split('\n');
|
|
349
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
350
|
+
if (!lines[i].includes('"USER_INPUT"')) continue;
|
|
351
|
+
try {
|
|
352
|
+
const step = JSON.parse(lines[i]);
|
|
353
|
+
if (step.type === 'USER_INPUT' && typeof step.content === 'string') {
|
|
354
|
+
const m = step.content.match(/<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/);
|
|
355
|
+
const text = (m ? m[1] : step.content).replace(/\s+/g, ' ').trim();
|
|
356
|
+
if (
|
|
357
|
+
text &&
|
|
358
|
+
text.length >= 6 &&
|
|
359
|
+
!/^(ok|okay|yes|yep|sure|go|k|y|thx|thanks)$/i.test(text)
|
|
360
|
+
) {
|
|
361
|
+
agyLatestPrompt = text.slice(0, 120);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} catch {}
|
|
366
|
+
}
|
|
367
|
+
addWorkspace(agyLatestWorkspace, 'Antigravity', agyLatestMtime, agyLatestPrompt);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} catch {}
|
|
371
|
+
|
|
372
|
+
const agyEstTokens5h = Math.round(agyActiveBytes5h / 4);
|
|
373
|
+
const agyUsedPct = Math.min(95, Math.round((agyActiveBytes5h / (12 * 1024 * 1024)) * 100));
|
|
374
|
+
const agyRemainPct = Math.max(5, 100 - agyUsedPct);
|
|
375
|
+
|
|
376
|
+
// 2. Claude Code
|
|
377
|
+
let claudeCost5h = 0;
|
|
378
|
+
let claudeCost7d = 0;
|
|
379
|
+
let claudeOldestIn5h = 0;
|
|
380
|
+
let claudeLatestMtime = 0;
|
|
381
|
+
let claudeLatestWorkspace = '';
|
|
382
|
+
let claudeLatestModel = 'claude-opus-4-8';
|
|
383
|
+
let claudeLatestPrompt = '';
|
|
384
|
+
let claudeSessionCount = 0;
|
|
385
|
+
const claudeDir = path.join(home, '.claude', 'projects');
|
|
386
|
+
try {
|
|
387
|
+
if (fs.existsSync(claudeDir)) {
|
|
388
|
+
const allFiles = [];
|
|
389
|
+
for (const d of fs.readdirSync(claudeDir)) {
|
|
390
|
+
const p = path.join(claudeDir, d);
|
|
391
|
+
if (!fs.statSync(p).isDirectory()) continue;
|
|
392
|
+
for (const f of fs.readdirSync(p)) {
|
|
393
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
394
|
+
claudeSessionCount++;
|
|
395
|
+
const full = path.join(p, f);
|
|
396
|
+
const st = fs.statSync(full);
|
|
397
|
+
allFiles.push({ path: full, mtimeMs: st.mtimeMs });
|
|
398
|
+
if (st.mtimeMs < sevenDaysAgo) continue;
|
|
399
|
+
const lines = fs.readFileSync(full, 'utf8').split('\n');
|
|
400
|
+
for (const l of lines) {
|
|
401
|
+
if (!l.includes('"usage"')) continue;
|
|
402
|
+
try {
|
|
403
|
+
const obj = JSON.parse(l);
|
|
404
|
+
const ts = obj.timestamp ? Date.parse(obj.timestamp) : st.mtimeMs;
|
|
405
|
+
const u = obj.message?.usage;
|
|
406
|
+
if (!u) continue;
|
|
407
|
+
const inp = Number(u.input_tokens || 0);
|
|
408
|
+
const cw = Number(u.cache_creation_input_tokens || 0);
|
|
409
|
+
const cr = Number(u.cache_read_input_tokens || 0);
|
|
410
|
+
const out = Number(u.output_tokens || 0);
|
|
411
|
+
const isOpus = (obj.message?.model || '').includes('opus');
|
|
412
|
+
const cost = isOpus
|
|
413
|
+
? (inp * 15 + cw * 18.75 + cr * 1.5 + out * 75) / 1e6
|
|
414
|
+
: (inp * 3 + cw * 3.75 + cr * 0.3 + out * 15) / 1e6;
|
|
415
|
+
if (ts >= sevenDaysAgo) claudeCost7d += cost;
|
|
416
|
+
if (ts >= fiveHoursAgo) {
|
|
417
|
+
claudeCost5h += cost;
|
|
418
|
+
if (!claudeOldestIn5h || ts < claudeOldestIn5h) claudeOldestIn5h = ts;
|
|
419
|
+
}
|
|
420
|
+
} catch {}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
allFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
425
|
+
for (const item of allFiles.slice(0, 5)) {
|
|
426
|
+
try {
|
|
427
|
+
const lines = fs.readFileSync(item.path, 'utf8').trim().split('\n');
|
|
428
|
+
let cwd = '';
|
|
429
|
+
let model = '';
|
|
430
|
+
let prompt = '';
|
|
431
|
+
for (const l of lines) {
|
|
432
|
+
try {
|
|
433
|
+
const o = JSON.parse(l);
|
|
434
|
+
if (o.cwd) cwd = o.cwd;
|
|
435
|
+
if (o.message?.model) model = o.message.model;
|
|
436
|
+
if (o.type === 'user' && typeof o.message?.content === 'string') {
|
|
437
|
+
prompt = o.message.content.trim().split('\n')[0].slice(0, 120);
|
|
438
|
+
}
|
|
439
|
+
} catch {}
|
|
440
|
+
}
|
|
441
|
+
if (cwd) addWorkspace(cwd, 'Claude Code', item.mtimeMs, prompt);
|
|
442
|
+
if (item === allFiles[0]) {
|
|
443
|
+
claudeLatestMtime = item.mtimeMs;
|
|
444
|
+
claudeLatestWorkspace = cwd;
|
|
445
|
+
if (model) claudeLatestModel = model;
|
|
446
|
+
claudeLatestPrompt = prompt;
|
|
447
|
+
}
|
|
448
|
+
} catch {}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} catch {}
|
|
452
|
+
|
|
453
|
+
const claudeUsedPct5h = Math.min(100, Math.round((claudeCost5h / 45) * 100));
|
|
454
|
+
const claudeRemainPct = Math.max(0, 100 - claudeUsedPct5h);
|
|
455
|
+
const claudeResetNote =
|
|
456
|
+
claudeOldestIn5h > 0
|
|
457
|
+
? `${formatResetShort(Math.floor((claudeOldestIn5h + 5 * 3600 * 1000) / 1000))} · $${claudeCost5h.toFixed(1)}/5h`
|
|
458
|
+
: `5h window · $${claudeCost7d.toFixed(0)} / 7d`;
|
|
459
|
+
|
|
460
|
+
// 3. OpenAI Codex
|
|
461
|
+
let codexRemainPct = 100;
|
|
462
|
+
let codexPlan = 'Pro';
|
|
463
|
+
let codexResetNote = 'Weekly window';
|
|
464
|
+
let codexLatestMtime = 0;
|
|
465
|
+
let codexLatestWorkspace = '';
|
|
466
|
+
let codexLatestModel = 'gpt-6-astra';
|
|
467
|
+
let codexLatestPrompt = '';
|
|
468
|
+
let codexSessionCount = 0;
|
|
469
|
+
const codexDir = path.join(home, '.codex', 'sessions');
|
|
470
|
+
try {
|
|
471
|
+
const codexFiles = [];
|
|
472
|
+
const scan = (dir) => {
|
|
473
|
+
if (!fs.existsSync(dir)) return;
|
|
474
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
475
|
+
const full = path.join(dir, e.name);
|
|
476
|
+
if (e.isDirectory()) scan(full);
|
|
477
|
+
else if (e.name.endsWith('.jsonl')) {
|
|
478
|
+
codexSessionCount++;
|
|
479
|
+
const st = fs.statSync(full);
|
|
480
|
+
codexFiles.push({ path: full, mtimeMs: st.mtimeMs });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
scan(codexDir);
|
|
485
|
+
codexFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
486
|
+
for (const item of codexFiles.slice(0, 5)) {
|
|
487
|
+
try {
|
|
488
|
+
const lines = fs.readFileSync(item.path, 'utf8').trim().split('\n');
|
|
489
|
+
let cwd = '';
|
|
490
|
+
let model = '';
|
|
491
|
+
let prompt = '';
|
|
492
|
+
for (const l of lines) {
|
|
493
|
+
try {
|
|
494
|
+
const obj = JSON.parse(l);
|
|
495
|
+
if (obj.type === 'session_meta' && obj.payload?.cwd) cwd = obj.payload.cwd;
|
|
496
|
+
if (obj.type === 'turn_context' && obj.payload?.model) model = obj.payload.model;
|
|
497
|
+
if (obj.payload?.role === 'user' && Array.isArray(obj.payload?.content)) {
|
|
498
|
+
const txt = obj.payload.content.find((c) => c.text)?.text || '';
|
|
499
|
+
if (txt && !txt.startsWith('<')) prompt = txt.trim().split('\n')[0].slice(0, 120);
|
|
500
|
+
}
|
|
501
|
+
} catch {}
|
|
502
|
+
}
|
|
503
|
+
if (cwd) addWorkspace(cwd, 'OpenAI Codex', item.mtimeMs, prompt);
|
|
504
|
+
if (item === codexFiles[0]) {
|
|
505
|
+
codexLatestMtime = item.mtimeMs;
|
|
506
|
+
codexLatestWorkspace = cwd;
|
|
507
|
+
if (model) codexLatestModel = model;
|
|
508
|
+
codexLatestPrompt = prompt;
|
|
509
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
510
|
+
try {
|
|
511
|
+
const obj = JSON.parse(lines[i]);
|
|
512
|
+
if (obj.payload && obj.payload.rate_limits) {
|
|
513
|
+
const rl = obj.payload.rate_limits;
|
|
514
|
+
const used = Number(rl.primary?.used_percent || 0);
|
|
515
|
+
codexRemainPct = Math.max(0, Math.min(100, 100 - used));
|
|
516
|
+
codexPlan = rl.plan_type
|
|
517
|
+
? rl.plan_type[0].toUpperCase() + rl.plan_type.slice(1)
|
|
518
|
+
: 'Pro';
|
|
519
|
+
if (rl.primary?.resets_at) {
|
|
520
|
+
codexResetNote = formatResetShort(rl.primary.resets_at);
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
} catch {}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} catch {}
|
|
528
|
+
}
|
|
529
|
+
} catch {}
|
|
530
|
+
|
|
531
|
+
// Helper for VS Code / Cursor / Windsurf workspaceStorage scanning across macOS / Windows / Linux
|
|
532
|
+
const scanIdeWorkspaces = (ideName, label) => {
|
|
533
|
+
let latestMtime = 0;
|
|
534
|
+
let latestWorkspace = '';
|
|
535
|
+
let count = 0;
|
|
536
|
+
const dir = path.join(getIdeUserDir(ideName, home), 'workspaceStorage');
|
|
537
|
+
try {
|
|
538
|
+
if (fs.existsSync(dir)) {
|
|
539
|
+
const entries = fs.readdirSync(dir);
|
|
540
|
+
count = entries.length;
|
|
541
|
+
for (const d of entries) {
|
|
542
|
+
const wj = path.join(dir, d, 'workspace.json');
|
|
543
|
+
if (!fs.existsSync(wj)) continue;
|
|
544
|
+
try {
|
|
545
|
+
const st = fs.statSync(wj);
|
|
546
|
+
const data = JSON.parse(fs.readFileSync(wj, 'utf8'));
|
|
547
|
+
const folder = decodeURIComponent(String(data.folder || '').replace(/^file:\/\//, ''));
|
|
548
|
+
if (folder) addWorkspace(folder, label, st.mtimeMs, '');
|
|
549
|
+
if (st.mtimeMs > latestMtime && folder) {
|
|
550
|
+
latestMtime = st.mtimeMs;
|
|
551
|
+
latestWorkspace = folder;
|
|
552
|
+
}
|
|
553
|
+
} catch {}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
} catch {}
|
|
557
|
+
return { latestMtime, latestWorkspace, count };
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// 4. VS Code Copilot, 5. Cursor, 6. Windsurf
|
|
561
|
+
const vscInfo = scanIdeWorkspaces('Code', 'VS Code Copilot');
|
|
562
|
+
const curInfo = scanIdeWorkspaces('Cursor', 'Cursor');
|
|
563
|
+
const wsInfo = scanIdeWorkspaces('Windsurf', 'Windsurf');
|
|
564
|
+
|
|
565
|
+
// 7. Google Gemini CLI (~/.gemini/tmp/<hash>/chats)
|
|
566
|
+
let geminiLatestMtime = 0;
|
|
567
|
+
let geminiChatCount = 0;
|
|
568
|
+
const geminiTmpDir = path.join(home, '.gemini', 'tmp');
|
|
569
|
+
try {
|
|
570
|
+
if (fs.existsSync(geminiTmpDir)) {
|
|
571
|
+
for (const h of fs.readdirSync(geminiTmpDir)) {
|
|
572
|
+
const chatsDir = path.join(geminiTmpDir, h, 'chats');
|
|
573
|
+
if (!fs.existsSync(chatsDir)) continue;
|
|
574
|
+
for (const f of fs.readdirSync(chatsDir)) {
|
|
575
|
+
if (!f.endsWith('.json') && !f.endsWith('.jsonl')) continue;
|
|
576
|
+
geminiChatCount++;
|
|
577
|
+
const st = fs.statSync(path.join(chatsDir, f));
|
|
578
|
+
if (st.mtimeMs > geminiLatestMtime) geminiLatestMtime = st.mtimeMs;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} catch {}
|
|
583
|
+
|
|
584
|
+
// 8. Cline / Roo Code (globalStorage/saoudrizwan.claude-dev/tasks)
|
|
585
|
+
let clineLatestMtime = 0;
|
|
586
|
+
let clineTaskCount = 0;
|
|
587
|
+
for (const ide of ['Code', 'Cursor', 'Windsurf']) {
|
|
588
|
+
for (const extId of ['saoudrizwan.claude-dev', 'rooveterinaryinc.roo-cline']) {
|
|
589
|
+
const tasksDir = path.join(getIdeUserDir(ide, home), 'globalStorage', extId, 'tasks');
|
|
590
|
+
try {
|
|
591
|
+
if (fs.existsSync(tasksDir)) {
|
|
592
|
+
for (const t of fs.readdirSync(tasksDir)) {
|
|
593
|
+
const uiMsg = path.join(tasksDir, t, 'ui_messages.json');
|
|
594
|
+
if (fs.existsSync(uiMsg)) {
|
|
595
|
+
clineTaskCount++;
|
|
596
|
+
const st = fs.statSync(uiMsg);
|
|
597
|
+
if (st.mtimeMs > clineLatestMtime) clineLatestMtime = st.mtimeMs;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} catch {}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 9. OpenCode (~/.local/share/opencode/opencode.db)
|
|
606
|
+
let opencodeLatestMtime = 0;
|
|
607
|
+
const opencodeDb = path.join(
|
|
608
|
+
process.env.OPENCODE_DATA_DIR || path.join(home, '.local', 'share', 'opencode'),
|
|
609
|
+
'opencode.db'
|
|
610
|
+
);
|
|
611
|
+
try {
|
|
612
|
+
if (fs.existsSync(opencodeDb)) {
|
|
613
|
+
opencodeLatestMtime = fs.statSync(opencodeDb).mtimeMs;
|
|
614
|
+
}
|
|
615
|
+
} catch {}
|
|
616
|
+
|
|
617
|
+
// 10. Aider (.aider.chat.history.md in active workspace or ~/.aider*)
|
|
618
|
+
let aiderLatestMtime = 0;
|
|
619
|
+
try {
|
|
620
|
+
const aiderHist = path.join(DEFAULT_PROJECT_ROOT, '.aider.chat.history.md');
|
|
621
|
+
if (fs.existsSync(aiderHist)) aiderLatestMtime = fs.statSync(aiderHist).mtimeMs;
|
|
622
|
+
} catch {}
|
|
623
|
+
|
|
624
|
+
// 11. Block Goose (~/.local/share/goose/sessions)
|
|
625
|
+
let gooseLatestMtime = 0;
|
|
626
|
+
const gooseDir = path.join(home, '.local', 'share', 'goose', 'sessions');
|
|
627
|
+
try {
|
|
628
|
+
if (fs.existsSync(gooseDir)) {
|
|
629
|
+
gooseLatestMtime = fs.statSync(gooseDir).mtimeMs;
|
|
630
|
+
}
|
|
631
|
+
} catch {}
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
workspaces: [...discoveredWorkspaces.values()].sort((a, b) => b.mtimeMs - a.mtimeMs),
|
|
635
|
+
agents: {
|
|
636
|
+
antigravity: {
|
|
637
|
+
mtimeMs: agyLatestMtime,
|
|
638
|
+
workspace: shortenHome(agyLatestWorkspace, home),
|
|
639
|
+
rawWorkspace: agyLatestWorkspace,
|
|
640
|
+
model: 'Antigravity IDE',
|
|
641
|
+
lastPrompt: agyLatestPrompt,
|
|
642
|
+
quota: {
|
|
643
|
+
hasBar: true,
|
|
644
|
+
remainPct: agyRemainPct,
|
|
645
|
+
label: `${agyRemainPct}%`,
|
|
646
|
+
detail: `~${(agyEstTokens5h / 1000).toFixed(0)}k tok / 5h · ${agyTotalSessions} sessions`,
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
claude: {
|
|
650
|
+
mtimeMs: claudeLatestMtime,
|
|
651
|
+
workspace: shortenHome(claudeLatestWorkspace, home),
|
|
652
|
+
rawWorkspace: claudeLatestWorkspace,
|
|
653
|
+
model: claudeLatestModel,
|
|
654
|
+
lastPrompt: claudeLatestPrompt,
|
|
655
|
+
quota: {
|
|
656
|
+
hasBar: true,
|
|
657
|
+
remainPct: claudeRemainPct,
|
|
658
|
+
label: `${claudeRemainPct}%`,
|
|
659
|
+
detail: claudeResetNote,
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
codex: {
|
|
663
|
+
mtimeMs: codexLatestMtime,
|
|
664
|
+
workspace: shortenHome(codexLatestWorkspace, home),
|
|
665
|
+
rawWorkspace: codexLatestWorkspace,
|
|
666
|
+
model: codexLatestModel,
|
|
667
|
+
lastPrompt: codexLatestPrompt,
|
|
668
|
+
quota: {
|
|
669
|
+
hasBar: true,
|
|
670
|
+
remainPct: codexRemainPct,
|
|
671
|
+
label: `${codexRemainPct}%`,
|
|
672
|
+
detail: `${codexPlan} · ${codexResetNote}`,
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
cursor: {
|
|
676
|
+
mtimeMs: curInfo.latestMtime,
|
|
677
|
+
workspace: shortenHome(curInfo.latestWorkspace, home),
|
|
678
|
+
rawWorkspace: curInfo.latestWorkspace,
|
|
679
|
+
model: 'Cursor Agent',
|
|
680
|
+
lastPrompt: '',
|
|
681
|
+
quota: {
|
|
682
|
+
hasBar: false,
|
|
683
|
+
remainPct: null,
|
|
684
|
+
label: curInfo.count > 0 ? 'Local IDE' : 'Rule Mount (.cursorrules)',
|
|
685
|
+
detail: curInfo.count > 0 ? `${curInfo.count} workspaces indexed` : 'Ready on install',
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
vscode: {
|
|
689
|
+
mtimeMs: vscInfo.latestMtime,
|
|
690
|
+
workspace: shortenHome(vscInfo.latestWorkspace, home),
|
|
691
|
+
rawWorkspace: vscInfo.latestWorkspace,
|
|
692
|
+
model: 'Copilot Chat',
|
|
693
|
+
lastPrompt: '',
|
|
694
|
+
quota: {
|
|
695
|
+
hasBar: false,
|
|
696
|
+
remainPct: null,
|
|
697
|
+
label: 'Local IDE',
|
|
698
|
+
detail: `${vscInfo.count} workspaces indexed`,
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
gemini: {
|
|
702
|
+
mtimeMs: geminiLatestMtime,
|
|
703
|
+
workspace: geminiChatCount > 0 ? '~/.gemini/tmp' : 'GEMINI.md mount',
|
|
704
|
+
rawWorkspace: '',
|
|
705
|
+
model: 'gemini-2.5-pro',
|
|
706
|
+
lastPrompt: '',
|
|
707
|
+
quota: {
|
|
708
|
+
hasBar: false,
|
|
709
|
+
remainPct: null,
|
|
710
|
+
label: 'CLI / GEMINI.md',
|
|
711
|
+
detail: geminiChatCount > 0 ? `${geminiChatCount} chat sessions` : 'Ready on install',
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
windsurf: {
|
|
715
|
+
mtimeMs: wsInfo.latestMtime,
|
|
716
|
+
workspace: shortenHome(wsInfo.latestWorkspace, home),
|
|
717
|
+
rawWorkspace: wsInfo.latestWorkspace,
|
|
718
|
+
model: 'Cascade',
|
|
719
|
+
lastPrompt: '',
|
|
720
|
+
quota: {
|
|
721
|
+
hasBar: false,
|
|
722
|
+
remainPct: null,
|
|
723
|
+
label: wsInfo.count > 0 ? 'Local IDE' : 'Rule Mount (.windsurfrules)',
|
|
724
|
+
detail: wsInfo.count > 0 ? `${wsInfo.count} workspaces indexed` : 'Ready on install',
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
cline: {
|
|
728
|
+
mtimeMs: clineLatestMtime,
|
|
729
|
+
workspace: clineTaskCount > 0 ? 'VS Code / Cursor' : '.clinerules mount',
|
|
730
|
+
rawWorkspace: '',
|
|
731
|
+
model: 'Autonomous Ext',
|
|
732
|
+
lastPrompt: '',
|
|
733
|
+
quota: {
|
|
734
|
+
hasBar: false,
|
|
735
|
+
remainPct: null,
|
|
736
|
+
label: 'Extension Bridge',
|
|
737
|
+
detail: clineTaskCount > 0 ? `${clineTaskCount} tasks indexed` : 'Mounts .clinerules',
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
opencode: {
|
|
741
|
+
mtimeMs: opencodeLatestMtime,
|
|
742
|
+
workspace: opencodeLatestMtime ? '~/.local/share/opencode' : 'AGENTS.md mount',
|
|
743
|
+
rawWorkspace: '',
|
|
744
|
+
model: 'opencode v2',
|
|
745
|
+
lastPrompt: '',
|
|
746
|
+
quota: {
|
|
747
|
+
hasBar: false,
|
|
748
|
+
remainPct: null,
|
|
749
|
+
label: 'SQLite / CLI',
|
|
750
|
+
detail: opencodeLatestMtime ? 'opencode.db detected' : 'Ready on install',
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
aider: {
|
|
754
|
+
mtimeMs: aiderLatestMtime,
|
|
755
|
+
workspace: '.aider.chat.history.md',
|
|
756
|
+
rawWorkspace: '',
|
|
757
|
+
model: 'Architect / Diff',
|
|
758
|
+
lastPrompt: '',
|
|
759
|
+
quota: {
|
|
760
|
+
hasBar: false,
|
|
761
|
+
remainPct: null,
|
|
762
|
+
label: 'CLI (--read)',
|
|
763
|
+
detail: 'aider --read .vicem/BATON.md',
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
goose: {
|
|
767
|
+
mtimeMs: gooseLatestMtime,
|
|
768
|
+
workspace: '~/.local/share/goose',
|
|
769
|
+
rawWorkspace: '',
|
|
770
|
+
model: 'MCP CLI',
|
|
771
|
+
lastPrompt: '',
|
|
772
|
+
quota: {
|
|
773
|
+
hasBar: false,
|
|
774
|
+
remainPct: null,
|
|
775
|
+
label: 'CLI / MCP',
|
|
776
|
+
detail: 'goose session + AGENTS.md',
|
|
777
|
+
},
|
|
778
|
+
},
|
|
779
|
+
web: {
|
|
780
|
+
mtimeMs: 0,
|
|
781
|
+
workspace: 'Clipboard / Browser',
|
|
782
|
+
rawWorkspace: '',
|
|
783
|
+
model: 'ChatGPT · Claude · Gemini',
|
|
784
|
+
lastPrompt: '',
|
|
785
|
+
quota: {
|
|
786
|
+
hasBar: false,
|
|
787
|
+
remainPct: null,
|
|
788
|
+
label: 'Zero-net bridge',
|
|
789
|
+
detail: 'Clipboard + URL prefill',
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
let selectedProjectRoot = DEFAULT_PROJECT_ROOT;
|
|
797
|
+
let customGoalOverride = '';
|
|
798
|
+
let manualActiveAgent = null;
|
|
799
|
+
let lastHandedOffFrom = null;
|
|
800
|
+
let lastHandoffReceipt = null;
|
|
801
|
+
let cachedTelemetry = null;
|
|
802
|
+
let cachedTelemetryAt = 0;
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Harvests rich, multi-turn conversation history, tool actions, and active files
|
|
806
|
+
* from the real session logs of Antigravity, Claude Code, or OpenAI Codex for `projectRoot`.
|
|
807
|
+
*/
|
|
808
|
+
function harvestRichProjectContext(projectRoot, home, git) {
|
|
809
|
+
const userTimeline = [];
|
|
810
|
+
const assistantDigest = [];
|
|
811
|
+
const touchedFiles = new Set();
|
|
812
|
+
let rawSessionBytes = 0;
|
|
813
|
+
let sessionId = 'live-session';
|
|
814
|
+
|
|
815
|
+
// 1. Scan Antigravity brain transcripts sorted by mtimeMs (descending)
|
|
816
|
+
const brainDir = path.join(home, '.gemini', 'antigravity', 'brain');
|
|
817
|
+
try {
|
|
818
|
+
if (fs.existsSync(brainDir)) {
|
|
819
|
+
const candidates = [];
|
|
820
|
+
for (const d of fs.readdirSync(brainDir)) {
|
|
821
|
+
const tp = path.join(brainDir, d, '.system_generated', 'logs', 'transcript.jsonl');
|
|
822
|
+
try {
|
|
823
|
+
if (fs.existsSync(tp)) {
|
|
824
|
+
const st = fs.statSync(tp);
|
|
825
|
+
candidates.push({ id: d, path: tp, mtimeMs: st.mtimeMs, size: st.size });
|
|
826
|
+
}
|
|
827
|
+
} catch {}
|
|
828
|
+
}
|
|
829
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
830
|
+
for (const c of candidates.slice(0, 10)) {
|
|
831
|
+
const raw = fs.readFileSync(c.path, 'utf8');
|
|
832
|
+
if (!raw.includes(projectRoot)) continue;
|
|
833
|
+
rawSessionBytes = c.size;
|
|
834
|
+
sessionId = c.id;
|
|
835
|
+
const lines = raw.trim().split('\n');
|
|
836
|
+
const pathRegex = new RegExp(
|
|
837
|
+
projectRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\/([A-Za-z0-9_./-]+\\.[A-Za-z0-9]+)',
|
|
838
|
+
'g'
|
|
839
|
+
);
|
|
840
|
+
for (const line of lines.slice(-160)) {
|
|
841
|
+
for (const pm of line.matchAll(pathRegex)) {
|
|
842
|
+
const rel = pm[1];
|
|
843
|
+
if (rel && !rel.startsWith('.vicem') && !['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].includes(rel)) {
|
|
844
|
+
touchedFiles.add(rel);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const step = JSON.parse(line);
|
|
849
|
+
if (step.type === 'USER_INPUT' && typeof step.content === 'string') {
|
|
850
|
+
const m = step.content.match(/<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/);
|
|
851
|
+
const text = (m ? m[1] : step.content).replace(/\s+/g, ' ').trim();
|
|
852
|
+
if (text && text.length > 1 && !text.startsWith('<')) {
|
|
853
|
+
userTimeline.push(text.slice(0, 180));
|
|
854
|
+
}
|
|
855
|
+
} else if (step.type === 'PLANNER_RESPONSE') {
|
|
856
|
+
if (typeof step.content === 'string' && step.content.trim()) {
|
|
857
|
+
const firstPara = step.content
|
|
858
|
+
.trim()
|
|
859
|
+
.split(/\n\n+/)[0]
|
|
860
|
+
.replace(/\s+/g, ' ')
|
|
861
|
+
.slice(0, 220);
|
|
862
|
+
if (firstPara) assistantDigest.push(firstPara);
|
|
863
|
+
}
|
|
864
|
+
if (Array.isArray(step.tool_calls)) {
|
|
865
|
+
for (const tc of step.tool_calls) {
|
|
866
|
+
const args = tc.arguments || tc.input || {};
|
|
867
|
+
const p = args.Path || args.path || args.file_path || args.TargetFile;
|
|
868
|
+
if (typeof p === 'string' && p.startsWith(projectRoot)) {
|
|
869
|
+
const rel = path.relative(projectRoot, p).replace(/\\/g, '/');
|
|
870
|
+
if (rel && !rel.startsWith('..') && !['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].includes(rel)) {
|
|
871
|
+
touchedFiles.add(rel);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
} catch {}
|
|
878
|
+
}
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
} catch {}
|
|
883
|
+
|
|
884
|
+
// 2. Fallback: Check Claude Code project sessions if no Antigravity timeline found
|
|
885
|
+
if (userTimeline.length === 0) {
|
|
886
|
+
const encoded = projectRoot.replace(/[^a-zA-Z0-9]/g, '-');
|
|
887
|
+
const claudeProjDir = path.join(home, '.claude', 'projects', encoded);
|
|
888
|
+
try {
|
|
889
|
+
if (fs.existsSync(claudeProjDir)) {
|
|
890
|
+
const files = fs
|
|
891
|
+
.readdirSync(claudeProjDir)
|
|
892
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
893
|
+
.map((f) => {
|
|
894
|
+
const full = path.join(claudeProjDir, f);
|
|
895
|
+
const st = fs.statSync(full);
|
|
896
|
+
return { full, id: path.basename(f, '.jsonl'), mtimeMs: st.mtimeMs, size: st.size };
|
|
897
|
+
})
|
|
898
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
899
|
+
if (files[0]) {
|
|
900
|
+
rawSessionBytes = files[0].size;
|
|
901
|
+
sessionId = files[0].id;
|
|
902
|
+
const lines = fs.readFileSync(files[0].full, 'utf8').trim().split('\n');
|
|
903
|
+
for (const l of lines.slice(-120)) {
|
|
904
|
+
try {
|
|
905
|
+
const o = JSON.parse(l);
|
|
906
|
+
if (o.type === 'user' && typeof o.message?.content === 'string') {
|
|
907
|
+
const txt = o.message.content.replace(/\s+/g, ' ').trim();
|
|
908
|
+
if (txt) userTimeline.push(txt.slice(0, 180));
|
|
909
|
+
}
|
|
910
|
+
} catch {}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
} catch {}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// 3. Combine session-touched files FIRST, then Git modified + untracked files sorted by code priority & mtime
|
|
918
|
+
const candidateMap = new Map();
|
|
919
|
+
const skipDocsAndMeta = new Set([
|
|
920
|
+
'AGENTS.md',
|
|
921
|
+
'CLAUDE.md',
|
|
922
|
+
'GEMINI.md',
|
|
923
|
+
'LICENSE',
|
|
924
|
+
'SHA256SUMS',
|
|
925
|
+
'.gitignore',
|
|
926
|
+
'AGENTS.snippet.md',
|
|
927
|
+
'README.md',
|
|
928
|
+
'REQUIREMENTS.md',
|
|
929
|
+
'SECURITY.md',
|
|
930
|
+
]);
|
|
931
|
+
const addCandidate = (rel, isTouched) => {
|
|
932
|
+
if (!rel || skipDocsAndMeta.has(rel) || rel.startsWith('.vicem') || rel.startsWith('docs/')) return;
|
|
933
|
+
if (/\.(png|jpg|jpeg|gif|webp|svg|ico|pdf|zip|db|sqlite)$/i.test(rel)) return;
|
|
934
|
+
const full = path.join(projectRoot, rel);
|
|
935
|
+
try {
|
|
936
|
+
const st = fs.statSync(full);
|
|
937
|
+
if (st.isFile()) {
|
|
938
|
+
const isCode = /\.(mjs|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|css|html)$/i.test(rel);
|
|
939
|
+
const score = (isTouched ? 1e13 : 0) + (isCode ? 5e12 : 0) + st.mtimeMs;
|
|
940
|
+
if (!candidateMap.has(rel) || candidateMap.get(rel) < score) {
|
|
941
|
+
candidateMap.set(rel, score);
|
|
942
|
+
}
|
|
943
|
+
} else if (st.isDirectory()) {
|
|
944
|
+
for (const sub of fs.readdirSync(full)) {
|
|
945
|
+
addCandidate(path.join(rel, sub).replace(/\\/g, '/'), isTouched);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
} catch {}
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
for (const f of touchedFiles) addCandidate(f, true);
|
|
952
|
+
for (const f of [...(git.modifiedFiles || []), ...(git.untrackedFiles || [])]) addCandidate(f, false);
|
|
953
|
+
|
|
954
|
+
const activeFileList = [...candidateMap.entries()]
|
|
955
|
+
.sort((a, b) => b[1] - a[1])
|
|
956
|
+
.slice(0, 6)
|
|
957
|
+
.map(([rel]) => rel);
|
|
958
|
+
const recentUsers = userTimeline.slice(-6);
|
|
959
|
+
const recentAssistant = assistantDigest.slice(-3);
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
sessionId,
|
|
963
|
+
rawSessionBytes,
|
|
964
|
+
recentUsers,
|
|
965
|
+
recentAssistant,
|
|
966
|
+
activeFileList,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function collectState() {
|
|
971
|
+
const home = os.homedir();
|
|
972
|
+
if (!cachedTelemetry || Date.now() - cachedTelemetryAt > 2500) {
|
|
973
|
+
cachedTelemetry = collectRealAgentTelemetry(home);
|
|
974
|
+
cachedTelemetryAt = Date.now();
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const projectRoot = fs.existsSync(selectedProjectRoot)
|
|
978
|
+
? selectedProjectRoot
|
|
979
|
+
: DEFAULT_PROJECT_ROOT;
|
|
980
|
+
|
|
981
|
+
const runningProcs = detectRunningProcesses();
|
|
982
|
+
const diag = DoctorDiagnostics.runDiagnostics(projectRoot, process.env);
|
|
983
|
+
const detected = await SmartInferenceEngine.autoDetectSource(projectRoot, process.env);
|
|
984
|
+
const git = GitCollector.collect(projectRoot);
|
|
985
|
+
const rich = harvestRichProjectContext(projectRoot, home, git);
|
|
986
|
+
|
|
987
|
+
const effectiveGit = {
|
|
988
|
+
...git,
|
|
989
|
+
modifiedFiles: rich.activeFileList.length > 0 ? rich.activeFileList : git.modifiedFiles,
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const tel = cachedTelemetry.agents;
|
|
993
|
+
const substantiveUsers = rich.recentUsers.filter(
|
|
994
|
+
(u) => u.length >= 6 && !/^(ok|okay|yes|yep|sure|go|k|y|thx|thanks)$/i.test(u.trim())
|
|
995
|
+
);
|
|
996
|
+
const livePromptForProject =
|
|
997
|
+
customGoalOverride ||
|
|
998
|
+
substantiveUsers[substantiveUsers.length - 1] ||
|
|
999
|
+
rich.recentUsers[rich.recentUsers.length - 1] ||
|
|
1000
|
+
(projectRoot === DEFAULT_PROJECT_ROOT && tel.antigravity.lastPrompt
|
|
1001
|
+
? tel.antigravity.lastPrompt
|
|
1002
|
+
: '') ||
|
|
1003
|
+
tel.claude.lastPrompt ||
|
|
1004
|
+
tel.codex.lastPrompt ||
|
|
1005
|
+
`Active branch '${git.branch || 'main'}' (${git.headShort || 'HEAD'})`;
|
|
1006
|
+
|
|
1007
|
+
const doneList = [];
|
|
1008
|
+
if (rich.recentUsers.length > 1) {
|
|
1009
|
+
for (const u of rich.recentUsers.slice(0, -1)) {
|
|
1010
|
+
doneList.push(`User request handled: "${u}"`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
for (const a of rich.recentAssistant) {
|
|
1014
|
+
doneList.push(`Agent summary: ${a}`);
|
|
1015
|
+
}
|
|
1016
|
+
for (const f of rich.activeFileList) {
|
|
1017
|
+
doneList.push(`Active file modified/created in session: \`${f}\``);
|
|
1018
|
+
}
|
|
1019
|
+
for (const c of (git.recentCommits || []).slice(0, 3)) {
|
|
1020
|
+
doneList.push(`Git commit: ${c}`);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const effectiveTask = {
|
|
1024
|
+
goal: livePromptForProject,
|
|
1025
|
+
immediateNext: [
|
|
1026
|
+
`Fulfill latest request in ${path.basename(projectRoot)} (${git.branch || 'master'}): "${livePromptForProject}"`,
|
|
1027
|
+
...(rich.activeFileList.length > 0
|
|
1028
|
+
? [`Inspect and continue working on active session files: ${rich.activeFileList.map((f) => `\`${f}\``).join(', ')}`]
|
|
1029
|
+
: []),
|
|
1030
|
+
],
|
|
1031
|
+
done: doneList,
|
|
1032
|
+
userPrompts: rich.recentUsers,
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
const baton = BatonIaBuilder.build({
|
|
1036
|
+
projectRoot,
|
|
1037
|
+
sourceAgent: detected.agent || 'antigravity',
|
|
1038
|
+
targetAgent: 'codex',
|
|
1039
|
+
sessionId: rich.sessionId,
|
|
1040
|
+
persist: false,
|
|
1041
|
+
taskContext: effectiveTask,
|
|
1042
|
+
gitContext: effectiveGit,
|
|
1043
|
+
budgetTokens: 4000,
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
const trueRawTokens = Math.max(
|
|
1047
|
+
baton.rawTokens,
|
|
1048
|
+
Math.round(rich.rawSessionBytes / 4)
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
const ALL_PLATFORMS = [
|
|
1052
|
+
{ id: 'antigravity', name: 'Antigravity', primary: true },
|
|
1053
|
+
{ id: 'claude', name: 'Claude Code', primary: true },
|
|
1054
|
+
{ id: 'codex', name: 'OpenAI Codex', primary: true },
|
|
1055
|
+
{ id: 'cursor', name: 'Cursor', primary: false },
|
|
1056
|
+
{ id: 'vscode', name: 'VS Code Copilot', primary: true },
|
|
1057
|
+
{ id: 'gemini', name: 'Gemini CLI', primary: false },
|
|
1058
|
+
{ id: 'windsurf', name: 'Windsurf', primary: false },
|
|
1059
|
+
{ id: 'cline', name: 'Cline / Roo', primary: false },
|
|
1060
|
+
{ id: 'opencode', name: 'OpenCode', primary: false },
|
|
1061
|
+
{ id: 'aider', name: 'Aider', primary: false },
|
|
1062
|
+
{ id: 'goose', name: 'Block Goose', primary: false },
|
|
1063
|
+
{ id: 'web', name: 'Web Chat', primary: true },
|
|
1064
|
+
];
|
|
1065
|
+
|
|
1066
|
+
let autoWorkingId = manualActiveAgent;
|
|
1067
|
+
if (!autoWorkingId) {
|
|
1068
|
+
for (const a of ALL_PLATFORMS.map((p) => p.id)) {
|
|
1069
|
+
if (a === 'web') continue;
|
|
1070
|
+
const ageSec = (Date.now() - (tel[a]?.mtimeMs || 0)) / 1000;
|
|
1071
|
+
if (runningProcs[a] || ageSec < 120) {
|
|
1072
|
+
autoWorkingId = a;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (!autoWorkingId) autoWorkingId = 'antigravity';
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const nodes = ALL_PLATFORMS.map((item) => {
|
|
1080
|
+
const t = tel[item.id];
|
|
1081
|
+
const check = diag.agentChecks.find((c) => c.id === item.id);
|
|
1082
|
+
const binFound = Boolean(check?.binFound || resolveAgentBinary(item.id, home));
|
|
1083
|
+
const hasStorage = Boolean(check?.hasStorage || (t?.mtimeMs || 0) > 0);
|
|
1084
|
+
const installed = item.id === 'web' ? true : binFound || hasStorage;
|
|
1085
|
+
const mtimeMs = t?.mtimeMs || 0;
|
|
1086
|
+
const ageSec = mtimeMs ? (Date.now() - mtimeMs) / 1000 : Infinity;
|
|
1087
|
+
|
|
1088
|
+
let status = 'Idle';
|
|
1089
|
+
if (item.id === autoWorkingId && runningProcs[item.id]) {
|
|
1090
|
+
status = 'Working';
|
|
1091
|
+
} else if (item.id === lastHandedOffFrom || ageSec < 3600 * 12) {
|
|
1092
|
+
status = 'Done';
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
id: item.id,
|
|
1097
|
+
name: item.name,
|
|
1098
|
+
primary: item.primary || installed,
|
|
1099
|
+
model: t?.model || '',
|
|
1100
|
+
workspace: t?.workspace || '—',
|
|
1101
|
+
lastPrompt: t?.lastPrompt || '',
|
|
1102
|
+
installed,
|
|
1103
|
+
status,
|
|
1104
|
+
lastSeen: item.id === 'web' ? 'On demand' : formatAgo(mtimeMs),
|
|
1105
|
+
quota: t?.quota || { hasBar: false, label: 'Ready', detail: '' },
|
|
1106
|
+
};
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
const mountedFiles = [];
|
|
1110
|
+
for (const rel of [
|
|
1111
|
+
'AGENTS.md',
|
|
1112
|
+
'CLAUDE.md',
|
|
1113
|
+
'GEMINI.md',
|
|
1114
|
+
'.cursorrules',
|
|
1115
|
+
'.windsurfrules',
|
|
1116
|
+
'.clinerules',
|
|
1117
|
+
'.github/copilot-instructions.md',
|
|
1118
|
+
]) {
|
|
1119
|
+
const fp = path.join(projectRoot, rel);
|
|
1120
|
+
try {
|
|
1121
|
+
if (fs.existsSync(fp) && fs.readFileSync(fp, 'utf8').includes('<!-- VICEM-BATON-START -->')) {
|
|
1122
|
+
mountedFiles.push(rel);
|
|
1123
|
+
}
|
|
1124
|
+
} catch {}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const savedPct =
|
|
1128
|
+
trueRawTokens > baton.dietTokens
|
|
1129
|
+
? Math.round((1 - baton.dietTokens / trueRawTokens) * 1000) / 10
|
|
1130
|
+
: 0;
|
|
1131
|
+
|
|
1132
|
+
return {
|
|
1133
|
+
guiVersion: 'v16',
|
|
1134
|
+
osPlatform: process.platform,
|
|
1135
|
+
projectRoot,
|
|
1136
|
+
project: path.basename(projectRoot),
|
|
1137
|
+
shortProject: shortenHome(projectRoot, home),
|
|
1138
|
+
branch: git.branch || 'main',
|
|
1139
|
+
headShort: git.headShort || 'HEAD',
|
|
1140
|
+
modifiedFiles: effectiveGit.modifiedFiles,
|
|
1141
|
+
mountedFiles,
|
|
1142
|
+
workspaces: cachedTelemetry.workspaces,
|
|
1143
|
+
workingAgentId: autoWorkingId,
|
|
1144
|
+
lastHandedOffFrom,
|
|
1145
|
+
taskTitle: livePromptForProject,
|
|
1146
|
+
customGoalOverride,
|
|
1147
|
+
stats: {
|
|
1148
|
+
rawTokens: trueRawTokens,
|
|
1149
|
+
dietTokens: baton.dietTokens,
|
|
1150
|
+
savedPct,
|
|
1151
|
+
secretsMasked: baton.redactedCount,
|
|
1152
|
+
},
|
|
1153
|
+
lastHandoffReceipt,
|
|
1154
|
+
batonMarkdown: baton.markdown,
|
|
1155
|
+
nodes,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Cross-OS (macOS / Windows / Linux) Target Agent Launcher supporting all 12 platforms:
|
|
1161
|
+
* - launchMode === 'app' (default): Opens the native Desktop GUI App / Deep Link (`claude://`, `codex://`, `cursor://`, `windsurf://`, `vscode://`, or IDE app).
|
|
1162
|
+
* - launchMode === 'cli': Opens the OS native terminal (`Terminal.app` on macOS, `wt.exe`/`cmd.exe` on Windows, `x-terminal-emulator`/`gnome-terminal` on Linux) running the CLI binary.
|
|
1163
|
+
*/
|
|
1164
|
+
async function launchTargetAgentCrossPlatform(targetId, projectRoot, batonMarkdown, launchMode = 'app') {
|
|
1165
|
+
const home = os.homedir();
|
|
1166
|
+
const isMac = process.platform === 'darwin';
|
|
1167
|
+
const isWin = process.platform === 'win32';
|
|
1168
|
+
const vicemDir = path.join(projectRoot, '.vicem');
|
|
1169
|
+
fs.mkdirSync(vicemDir, { recursive: true });
|
|
1170
|
+
const initialPrompt = `Read .vicem/BATON.md in ${projectRoot} and continue immediately from Section 2 (Immediate NEXT #1).`;
|
|
1171
|
+
|
|
1172
|
+
// 1. Desktop GUI Deep Links for Claude Code & OpenAI Codex (`launchMode === 'app'`)
|
|
1173
|
+
if (targetId === 'claude' && launchMode === 'app') {
|
|
1174
|
+
const hasDesktop =
|
|
1175
|
+
(isMac && fs.existsSync('/Applications/Claude.app')) ||
|
|
1176
|
+
(isWin && fs.existsSync(path.join(process.env.LOCALAPPDATA || '', 'AnthropicClaude')));
|
|
1177
|
+
if (hasDesktop || !isMac) {
|
|
1178
|
+
const deepLink = `claude://code/new?folder=${encodeURIComponent(projectRoot)}&q=${encodeURIComponent(initialPrompt)}&source=vicem`;
|
|
1179
|
+
const res = openExternalCrossPlatform(deepLink);
|
|
1180
|
+
if (res.status === 0) {
|
|
1181
|
+
return {
|
|
1182
|
+
ok: true,
|
|
1183
|
+
method: `claude://code/new?folder=${shortenHome(projectRoot, home)}&q=...`,
|
|
1184
|
+
detail: `Opened Claude Desktop GUI in ${path.basename(projectRoot)} with Baton prompt prefilled`,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (targetId === 'codex' && launchMode === 'app') {
|
|
1191
|
+
const hasDesktop = !isMac || fs.existsSync('/Applications/ChatGPT.app');
|
|
1192
|
+
if (hasDesktop) {
|
|
1193
|
+
const deepLink = `codex://threads/new?prompt=${encodeURIComponent(initialPrompt)}`;
|
|
1194
|
+
const res = openExternalCrossPlatform(deepLink);
|
|
1195
|
+
if (res.status === 0) {
|
|
1196
|
+
return {
|
|
1197
|
+
ok: true,
|
|
1198
|
+
method: `codex://threads/new?prompt=...`,
|
|
1199
|
+
detail: `Opened Codex Desktop GUI with Baton prompt prefilled`,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// 2. IDE Targets (`antigravity`, `cursor`, `windsurf`, `vscode`, `cline`)
|
|
1206
|
+
const ideSpecs = {
|
|
1207
|
+
antigravity: {
|
|
1208
|
+
macApps: ['/Applications/Antigravity.app', '/Applications/Antigravity IDE.app'],
|
|
1209
|
+
cli: 'agy',
|
|
1210
|
+
scheme: `antigravity://file${projectRoot}`,
|
|
1211
|
+
rule: 'AGENTS.md',
|
|
1212
|
+
},
|
|
1213
|
+
cursor: {
|
|
1214
|
+
macApps: ['/Applications/Cursor.app'],
|
|
1215
|
+
cli: 'cursor',
|
|
1216
|
+
scheme: `cursor://file${projectRoot}`,
|
|
1217
|
+
rule: '.cursorrules',
|
|
1218
|
+
},
|
|
1219
|
+
windsurf: {
|
|
1220
|
+
macApps: ['/Applications/Windsurf.app'],
|
|
1221
|
+
cli: 'windsurf',
|
|
1222
|
+
scheme: `windsurf://file${projectRoot}`,
|
|
1223
|
+
rule: '.windsurfrules',
|
|
1224
|
+
},
|
|
1225
|
+
vscode: {
|
|
1226
|
+
macApps: ['/Applications/Visual Studio Code.app'],
|
|
1227
|
+
cli: 'code',
|
|
1228
|
+
scheme: `vscode://file${projectRoot}`,
|
|
1229
|
+
rule: '.github/copilot-instructions.md',
|
|
1230
|
+
},
|
|
1231
|
+
cline: {
|
|
1232
|
+
macApps: ['/Applications/Cursor.app', '/Applications/Visual Studio Code.app'],
|
|
1233
|
+
cli: 'code',
|
|
1234
|
+
scheme: `vscode://file${projectRoot}`,
|
|
1235
|
+
rule: '.clinerules',
|
|
1236
|
+
},
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
if (ideSpecs[targetId]) {
|
|
1240
|
+
const spec = ideSpecs[targetId];
|
|
1241
|
+
if (isMac) {
|
|
1242
|
+
const foundApp = spec.macApps.find((p) => fs.existsSync(p));
|
|
1243
|
+
if (foundApp) {
|
|
1244
|
+
const res = openExternalCrossPlatform(projectRoot, foundApp);
|
|
1245
|
+
return {
|
|
1246
|
+
ok: res.status === 0,
|
|
1247
|
+
method: `open -a "${path.basename(foundApp)}" ${shortenHome(projectRoot, home)}`,
|
|
1248
|
+
detail: `Opened ${path.basename(foundApp)} at ${path.basename(projectRoot)} (${spec.rule} mounted)`,
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const bin = resolveAgentBinary(targetId, home);
|
|
1253
|
+
if (bin) {
|
|
1254
|
+
const res = spawnSync(bin, [projectRoot], { encoding: 'utf8', windowsHide: true });
|
|
1255
|
+
return {
|
|
1256
|
+
ok: res.status === 0,
|
|
1257
|
+
method: `${path.basename(bin)} ${shortenHome(projectRoot, home)}`,
|
|
1258
|
+
detail: `Opened ${targetId} at ${path.basename(projectRoot)} (${spec.rule} mounted)`,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const res = openExternalCrossPlatform(spec.scheme);
|
|
1262
|
+
return {
|
|
1263
|
+
ok: res.status === 0,
|
|
1264
|
+
method: spec.scheme,
|
|
1265
|
+
detail: `Triggered ${targetId} URI scheme (${spec.rule} mounted + clipboard ready)`,
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// 3. Web Chat (`web`)
|
|
1270
|
+
if (targetId === 'web') {
|
|
1271
|
+
const shortQuery = encodeURIComponent(batonMarkdown.slice(0, 1800));
|
|
1272
|
+
const webUrl = `https://chatgpt.com/?q=${shortQuery}`;
|
|
1273
|
+
const res = openExternalCrossPlatform(webUrl);
|
|
1274
|
+
return {
|
|
1275
|
+
ok: res.status === 0,
|
|
1276
|
+
method: 'https://chatgpt.com/?q=...',
|
|
1277
|
+
detail: 'Opened browser tab with prefilled Baton + full Baton on clipboard',
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// 4. CLI Agents (`claude`, `codex`, `gemini`, `opencode`, `aider`, `goose`) across macOS / Windows / Linux
|
|
1282
|
+
const binPath = resolveAgentBinary(targetId, home);
|
|
1283
|
+
if (!binPath) {
|
|
1284
|
+
return {
|
|
1285
|
+
ok: true,
|
|
1286
|
+
method: 'Rule Mount + Clipboard (CLI binary not installed)',
|
|
1287
|
+
detail: `${targetId} CLI not installed locally; Baton mounted & copied to clipboard`,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const buildCliCommandLine = (bin) => {
|
|
1292
|
+
const qBin = isWin ? `"${bin}"` : shellQuote(bin);
|
|
1293
|
+
if (targetId === 'gemini') return `${qBin} -i ${isWin ? `"${initialPrompt}"` : shellQuote(initialPrompt)}`;
|
|
1294
|
+
if (targetId === 'opencode') return `${qBin} --prompt ${isWin ? `"${initialPrompt}"` : shellQuote(initialPrompt)}`;
|
|
1295
|
+
if (targetId === 'aider') return `${qBin} --read .vicem/BATON.md`;
|
|
1296
|
+
if (targetId === 'goose') return `${qBin} session`;
|
|
1297
|
+
return `${qBin} ${isWin ? `"${initialPrompt}"` : shellQuote(initialPrompt)}`;
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
if (isWin) {
|
|
1301
|
+
const cmdPath = path.join(vicemDir, `launch-${targetId}.cmd`);
|
|
1302
|
+
const cmdContent = [
|
|
1303
|
+
'@echo off',
|
|
1304
|
+
`cd /d "${projectRoot}"`,
|
|
1305
|
+
`echo [Vicem Relay] Loaded .vicem/BATON.md in ${path.basename(projectRoot)}`,
|
|
1306
|
+
buildCliCommandLine(binPath),
|
|
1307
|
+
].join('\r\n');
|
|
1308
|
+
fs.writeFileSync(cmdPath, cmdContent);
|
|
1309
|
+
const res = spawnSync('cmd', ['/c', 'start', `"Vicem - ${targetId}"`, 'cmd', '/k', cmdPath], {
|
|
1310
|
+
encoding: 'utf8',
|
|
1311
|
+
});
|
|
1312
|
+
return {
|
|
1313
|
+
ok: res.status === 0,
|
|
1314
|
+
method: `start cmd /k ${path.basename(cmdPath)}`,
|
|
1315
|
+
detail: `Launched ${targetId} in Windows Terminal / Command Prompt`,
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const launcherScriptPath = path.join(vicemDir, isMac ? `launch-${targetId}.command` : `launch-${targetId}.sh`);
|
|
1320
|
+
const statusPath = path.join(vicemDir, `launch-${targetId}.status.json`);
|
|
1321
|
+
fs.rmSync(statusPath, { force: true });
|
|
1322
|
+
const scriptContent = [
|
|
1323
|
+
isMac ? '#!/bin/zsh' : '#!/bin/sh',
|
|
1324
|
+
'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"',
|
|
1325
|
+
`STATUS=${shellQuote(statusPath)}`,
|
|
1326
|
+
`cd ${shellQuote(projectRoot)} || { echo '{"state":"cd_failed"}' > "$STATUS"; exit 1; }`,
|
|
1327
|
+
'clear',
|
|
1328
|
+
`echo "=================================================================="`,
|
|
1329
|
+
`printf '%s\\n' ${shellQuote(` ✓ [Vicem Relay] Loaded .vicem/BATON.md in ${path.basename(projectRoot)}`)}`,
|
|
1330
|
+
`printf '%s\\n' ${shellQuote(` ✓ Target Agent: ${targetId} (${binPath})`)}`,
|
|
1331
|
+
`echo "=================================================================="`,
|
|
1332
|
+
`echo ""`,
|
|
1333
|
+
`echo "{\\"state\\":\\"started\\",\\"pid\\":$$,\\"at\\":\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\"}" > "$STATUS"`,
|
|
1334
|
+
`exec ${buildCliCommandLine(binPath)}`,
|
|
1335
|
+
'',
|
|
1336
|
+
].join('\n');
|
|
1337
|
+
|
|
1338
|
+
fs.writeFileSync(launcherScriptPath, scriptContent, { mode: 0o755 });
|
|
1339
|
+
fs.chmodSync(launcherScriptPath, 0o755);
|
|
1340
|
+
|
|
1341
|
+
if (isMac) {
|
|
1342
|
+
const res = spawnSync('open', ['-a', 'Terminal', launcherScriptPath], { encoding: 'utf8' });
|
|
1343
|
+
if (res.status !== 0) {
|
|
1344
|
+
return { ok: false, method: 'open -a Terminal', detail: `Terminal failed to open` };
|
|
1345
|
+
}
|
|
1346
|
+
} else {
|
|
1347
|
+
// Linux terminal emulators
|
|
1348
|
+
const terms = [
|
|
1349
|
+
['x-terminal-emulator', ['-e', launcherScriptPath]],
|
|
1350
|
+
['gnome-terminal', ['--', launcherScriptPath]],
|
|
1351
|
+
['konsole', ['-e', launcherScriptPath]],
|
|
1352
|
+
['xfce4-terminal', ['-e', launcherScriptPath]],
|
|
1353
|
+
['kitty', [launcherScriptPath]],
|
|
1354
|
+
['alacritty', ['-e', launcherScriptPath]],
|
|
1355
|
+
];
|
|
1356
|
+
let launched = false;
|
|
1357
|
+
for (const [tBin, tArgs] of terms) {
|
|
1358
|
+
const r = spawnSync(tBin, tArgs, { encoding: 'utf8', stdio: 'ignore' });
|
|
1359
|
+
if (r.status === 0) {
|
|
1360
|
+
launched = true;
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (!launched) {
|
|
1365
|
+
return {
|
|
1366
|
+
ok: true,
|
|
1367
|
+
method: 'Clipboard + Rule Mount',
|
|
1368
|
+
detail: `Prepared ${shortenHome(launcherScriptPath, home)} (run in terminal)`,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const status = await waitForLaunchStatus(statusPath);
|
|
1374
|
+
const name = path.basename(binPath);
|
|
1375
|
+
if (status.state === 'started' && status.alive) {
|
|
1376
|
+
return { ok: true, method: `Terminal (${name})`, detail: `${name} running in Terminal (pid ${status.pid})` };
|
|
1377
|
+
}
|
|
1378
|
+
return { ok: true, method: `Terminal (${name})`, detail: `Opened Terminal launcher for ${name}` };
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Performs a REAL handoff and records a 5-step verified receipt so the user can see concrete proof.
|
|
1383
|
+
*/
|
|
1384
|
+
async function executeRealHandoff(targetId, launchMode = 'app') {
|
|
1385
|
+
const home = os.homedir();
|
|
1386
|
+
const state = await collectState();
|
|
1387
|
+
const projectRoot = state.projectRoot;
|
|
1388
|
+
const sourceAgent = state.workingAgentId || 'antigravity';
|
|
1389
|
+
|
|
1390
|
+
const git = GitCollector.collect(projectRoot);
|
|
1391
|
+
const rich = harvestRichProjectContext(projectRoot, home, git);
|
|
1392
|
+
const effectiveGit = {
|
|
1393
|
+
...git,
|
|
1394
|
+
modifiedFiles: rich.activeFileList.length > 0 ? rich.activeFileList : git.modifiedFiles,
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
const doneList = [];
|
|
1398
|
+
if (rich.recentUsers.length > 1) {
|
|
1399
|
+
for (const u of rich.recentUsers.slice(0, -1)) {
|
|
1400
|
+
doneList.push(`User request handled: "${u}"`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
for (const a of rich.recentAssistant) {
|
|
1404
|
+
doneList.push(`Agent summary: ${a}`);
|
|
1405
|
+
}
|
|
1406
|
+
for (const f of rich.activeFileList) {
|
|
1407
|
+
doneList.push(`Active file modified/created in session: \`${f}\``);
|
|
1408
|
+
}
|
|
1409
|
+
for (const c of (git.recentCommits || []).slice(0, 3)) {
|
|
1410
|
+
doneList.push(`Git commit: ${c}`);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const task = {
|
|
1414
|
+
goal: state.taskTitle,
|
|
1415
|
+
immediateNext: [
|
|
1416
|
+
`Fulfill latest request in ${state.project} (${state.branch}): "${state.taskTitle}"`,
|
|
1417
|
+
...(rich.activeFileList.length > 0
|
|
1418
|
+
? [`Inspect and continue working on active session files: ${rich.activeFileList.map((f) => `\`${f}\``).join(', ')}`]
|
|
1419
|
+
: []),
|
|
1420
|
+
],
|
|
1421
|
+
done: doneList,
|
|
1422
|
+
userPrompts: rich.recentUsers,
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
// Step 1 & 2 & 3: Build Baton & write `.vicem/BATON.md` + `.vicem/ledger.json`
|
|
1426
|
+
const baton = BatonIaBuilder.build({
|
|
1427
|
+
projectRoot,
|
|
1428
|
+
sourceAgent,
|
|
1429
|
+
targetAgent: targetId,
|
|
1430
|
+
sessionId: rich.sessionId,
|
|
1431
|
+
taskContext: task,
|
|
1432
|
+
gitContext: effectiveGit,
|
|
1433
|
+
budgetTokens: 4000,
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
const batonFilePath = path.join(projectRoot, '.vicem', 'BATON.md');
|
|
1437
|
+
const batonStat = fs.existsSync(batonFilePath) ? fs.statSync(batonFilePath) : null;
|
|
1438
|
+
|
|
1439
|
+
// Step 4A: Mount target rule file (`CLAUDE.md` / `AGENTS.md` / `.cursorrules` / `.windsurfrules` / `.clinerules` / `GEMINI.md` / `.github/copilot-instructions.md`)
|
|
1440
|
+
let mountedRel = '.vicem/BATON.md';
|
|
1441
|
+
let mountVerified = false;
|
|
1442
|
+
try {
|
|
1443
|
+
const mountRes = RuleMountAdapter.mount(projectRoot, targetId, baton.markdown);
|
|
1444
|
+
mountedRel = mountRes.relPath;
|
|
1445
|
+
if (targetId === 'claude') {
|
|
1446
|
+
const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
1447
|
+
const block = `<!-- VICEM-BATON-START -->\n${baton.markdown.trim()}\n<!-- VICEM-BATON-END -->\n`;
|
|
1448
|
+
fs.writeFileSync(claudeMdPath, block, 'utf8');
|
|
1449
|
+
mountedRel = 'CLAUDE.md + AGENTS.md';
|
|
1450
|
+
}
|
|
1451
|
+
const checkPath = path.join(projectRoot, mountRes.relPath);
|
|
1452
|
+
mountVerified =
|
|
1453
|
+
fs.existsSync(checkPath) &&
|
|
1454
|
+
fs.readFileSync(checkPath, 'utf8').includes('<!-- VICEM-BATON-START -->');
|
|
1455
|
+
} catch {}
|
|
1456
|
+
|
|
1457
|
+
// Step 4B: Copy to OS Clipboard (`pbcopy` / `Set-Clipboard` / `wl-copy` / `xclip`) and verify
|
|
1458
|
+
ClipboardAdapter.copyNative(baton.markdown);
|
|
1459
|
+
let clipboardChars = 0;
|
|
1460
|
+
try {
|
|
1461
|
+
const pasted = readClipboardCrossPlatform();
|
|
1462
|
+
if (pasted.includes('VICEM-BATON') || pasted.includes('# BATON:')) {
|
|
1463
|
+
clipboardChars = pasted.length;
|
|
1464
|
+
}
|
|
1465
|
+
} catch {}
|
|
1466
|
+
|
|
1467
|
+
// Step 5: Launch target application across macOS / Windows / Linux according to launchMode ('app' | 'cli' | 'off')
|
|
1468
|
+
let launchResult = {
|
|
1469
|
+
ok: true,
|
|
1470
|
+
method: 'Skipped (Launch: Off)',
|
|
1471
|
+
detail: 'Clipboard + Rule mount only',
|
|
1472
|
+
};
|
|
1473
|
+
if (launchMode && launchMode !== 'off') {
|
|
1474
|
+
launchResult = await launchTargetAgentCrossPlatform(targetId, projectRoot, baton.markdown, launchMode);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (launchResult.ok && targetId && targetId !== sourceAgent) {
|
|
1478
|
+
lastHandedOffFrom = sourceAgent;
|
|
1479
|
+
manualActiveAgent = targetId;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const nowStr = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
1483
|
+
lastHandoffReceipt = {
|
|
1484
|
+
timestamp: nowStr,
|
|
1485
|
+
sourceAgent,
|
|
1486
|
+
targetId,
|
|
1487
|
+
project: state.project,
|
|
1488
|
+
steps: [
|
|
1489
|
+
{
|
|
1490
|
+
ok: true,
|
|
1491
|
+
title: '1. Session & Git Harvest',
|
|
1492
|
+
detail: `Extracted context from ${sourceAgent} in ${state.project} (${state.branch})`,
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
ok: true,
|
|
1496
|
+
title: '2. Context Diet & Redaction',
|
|
1497
|
+
detail: `${state.stats.rawTokens.toLocaleString()} → ${baton.dietTokens.toLocaleString()} tokens (-${state.stats.savedPct}%) · ${baton.redactedCount} secrets masked`,
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
ok: Boolean(batonStat && batonStat.size > 0),
|
|
1501
|
+
title: '3. Baton File Written on Disk',
|
|
1502
|
+
detail: `${shortenHome(batonFilePath, home)} (${batonStat ? batonStat.size.toLocaleString() : 0} bytes verified)`,
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
ok: mountVerified && clipboardChars > 0,
|
|
1506
|
+
title: '4. Rule Mounted & Clipboard Verified',
|
|
1507
|
+
detail: `Injected into ${mountedRel} · macOS clipboard verified (${clipboardChars.toLocaleString()} chars on pbpaste)`,
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
ok: launchResult.ok,
|
|
1511
|
+
title: '5. Target Agent Launch',
|
|
1512
|
+
detail: `${launchResult.detail} [${launchResult.method}]`,
|
|
1513
|
+
},
|
|
1514
|
+
],
|
|
1515
|
+
batonPreview: baton.markdown,
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
cachedTelemetryAt = 0;
|
|
1519
|
+
const updated = await collectState();
|
|
1520
|
+
return {
|
|
1521
|
+
...updated,
|
|
1522
|
+
actionReceipt: `${nowStr} · ${launchMode && launchMode !== 'off' ? (launchResult.ok ? 'Launch verified for' : 'Launch failed for') : 'Baton prepared for'} ${targetId} (${launchResult.detail})`,
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const HTML_PAGE = `<!DOCTYPE html>
|
|
1527
|
+
<html lang="en" data-theme="dark">
|
|
1528
|
+
<head>
|
|
1529
|
+
<meta charset="UTF-8" />
|
|
1530
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1531
|
+
<title>Vicem — Agent Context Relay & Capacity Monitor</title>
|
|
1532
|
+
<link rel="icon" type="image/png" href="/logo-dark.png" />
|
|
1533
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1534
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1535
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
1536
|
+
<style>
|
|
1537
|
+
:root[data-theme="dark"] {
|
|
1538
|
+
color-scheme: dark;
|
|
1539
|
+
--canvas: #08080A;
|
|
1540
|
+
--canvas-glow: radial-gradient(900px circle at 50% 0%, #14141A 0%, #08080A 70%);
|
|
1541
|
+
--win-bg: #101013;
|
|
1542
|
+
--win-header: #0C0C0E;
|
|
1543
|
+
--win-elevated: #151519;
|
|
1544
|
+
--row-hover: #17171C;
|
|
1545
|
+
--row-working: #111814;
|
|
1546
|
+
--border-subtle: #1E1E24;
|
|
1547
|
+
--border-mid: #282830;
|
|
1548
|
+
--border-strong: #383844;
|
|
1549
|
+
--text-primary: #F4F4F6;
|
|
1550
|
+
--text-secondary: #A0A0AB;
|
|
1551
|
+
--text-muted: #63636E;
|
|
1552
|
+
--icon-box-bg: #16161B;
|
|
1553
|
+
--icon-box-border: #26262F;
|
|
1554
|
+
--seg-empty: #1D1D24;
|
|
1555
|
+
--seg-fill: #E4E4E9;
|
|
1556
|
+
--seg-working: #22C55E;
|
|
1557
|
+
--seg-warn: #F59E0B;
|
|
1558
|
+
--status-working: #22C55E;
|
|
1559
|
+
--status-done: #A1A1AA;
|
|
1560
|
+
--status-idle: #52525C;
|
|
1561
|
+
--btn-bg: #16161B;
|
|
1562
|
+
--btn-bg-hover: #F4F4F6;
|
|
1563
|
+
--btn-text-hover: #09090B;
|
|
1564
|
+
--shadow-win: 0 28px 70px rgba(0, 0, 0, 0.72), 0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
1565
|
+
--relay-track: #22222A;
|
|
1566
|
+
--relay-pulse: #F4F4F6;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
:root[data-theme="light"] {
|
|
1570
|
+
color-scheme: light;
|
|
1571
|
+
--canvas: #E8E8E5;
|
|
1572
|
+
--canvas-glow: radial-gradient(900px circle at 50% 0%, #F3F3F0 0%, #E6E6E2 75%);
|
|
1573
|
+
--win-bg: #FAFAF8;
|
|
1574
|
+
--win-header: #F2F2EE;
|
|
1575
|
+
--win-elevated: #F0F0EC;
|
|
1576
|
+
--row-hover: #F3F3F0;
|
|
1577
|
+
--row-working: #EDF6F0;
|
|
1578
|
+
--border-subtle: #E4E4DF;
|
|
1579
|
+
--border-mid: #D4D4CE;
|
|
1580
|
+
--border-strong: #B8B8B0;
|
|
1581
|
+
--text-primary: #111113;
|
|
1582
|
+
--text-secondary: #4E4E56;
|
|
1583
|
+
--text-muted: #82828C;
|
|
1584
|
+
--icon-box-bg: #F2F2EE;
|
|
1585
|
+
--icon-box-border: #DADAD4;
|
|
1586
|
+
--seg-empty: #E2E2DC;
|
|
1587
|
+
--seg-fill: #18181B;
|
|
1588
|
+
--seg-working: #059669;
|
|
1589
|
+
--seg-warn: #D97706;
|
|
1590
|
+
--status-working: #059669;
|
|
1591
|
+
--status-done: #52525B;
|
|
1592
|
+
--status-idle: #A1A1AA;
|
|
1593
|
+
--btn-bg: #FCFBF9;
|
|
1594
|
+
--btn-bg-hover: #111113;
|
|
1595
|
+
--btn-text-hover: #FAFAF8;
|
|
1596
|
+
--shadow-win: 0 24px 60px rgba(15, 15, 20, 0.10), 0 2px 10px rgba(15, 15, 20, 0.05);
|
|
1597
|
+
--relay-track: #D4D4CE;
|
|
1598
|
+
--relay-pulse: #111113;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
* {
|
|
1602
|
+
box-sizing: border-box;
|
|
1603
|
+
margin: 0;
|
|
1604
|
+
padding: 0;
|
|
1605
|
+
-webkit-font-smoothing: antialiased;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
html.no-anim *,
|
|
1609
|
+
html.no-anim *::before,
|
|
1610
|
+
html.no-anim *::after {
|
|
1611
|
+
animation-duration: 0s !important;
|
|
1612
|
+
animation-delay: 0s !important;
|
|
1613
|
+
transition-duration: 0s !important;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
body {
|
|
1617
|
+
background: var(--canvas);
|
|
1618
|
+
background-image: var(--canvas-glow);
|
|
1619
|
+
color: var(--text-primary);
|
|
1620
|
+
font-family: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
1621
|
+
font-size: 13px;
|
|
1622
|
+
line-height: 1.45;
|
|
1623
|
+
min-height: 100vh;
|
|
1624
|
+
display: flex;
|
|
1625
|
+
align-items: center;
|
|
1626
|
+
justify-content: center;
|
|
1627
|
+
padding: clamp(0px, 2vw, 24px);
|
|
1628
|
+
transition: background-color 0.2s ease, color 0.2s ease;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
.app-window {
|
|
1632
|
+
width: min(1160px, 100%);
|
|
1633
|
+
max-width: 100%;
|
|
1634
|
+
max-height: calc(100vh - clamp(0px, 4vw, 48px));
|
|
1635
|
+
display: flex;
|
|
1636
|
+
flex-direction: column;
|
|
1637
|
+
background: var(--win-bg);
|
|
1638
|
+
border: 1px solid var(--border-mid);
|
|
1639
|
+
box-shadow: var(--shadow-win);
|
|
1640
|
+
overflow: hidden;
|
|
1641
|
+
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/* Top Brand & Workspace Bar */
|
|
1645
|
+
.win-topbar {
|
|
1646
|
+
display: flex;
|
|
1647
|
+
align-items: center;
|
|
1648
|
+
justify-content: space-between;
|
|
1649
|
+
gap: 12px;
|
|
1650
|
+
min-height: 52px;
|
|
1651
|
+
padding: 8px 22px;
|
|
1652
|
+
background: var(--win-header);
|
|
1653
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
1654
|
+
flex-shrink: 0;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
.brand-cluster {
|
|
1658
|
+
display: flex;
|
|
1659
|
+
align-items: center;
|
|
1660
|
+
gap: 12px;
|
|
1661
|
+
min-width: 0;
|
|
1662
|
+
flex: 1 1 auto;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
.official-logo-box {
|
|
1666
|
+
width: 30px;
|
|
1667
|
+
height: 30px;
|
|
1668
|
+
flex-shrink: 0;
|
|
1669
|
+
background: var(--icon-box-bg);
|
|
1670
|
+
border: 1px solid var(--border-mid);
|
|
1671
|
+
display: flex;
|
|
1672
|
+
align-items: center;
|
|
1673
|
+
justify-content: center;
|
|
1674
|
+
padding: 2px;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
.official-logo-box img {
|
|
1678
|
+
width: 100%;
|
|
1679
|
+
height: 100%;
|
|
1680
|
+
object-fit: contain;
|
|
1681
|
+
display: block;
|
|
1682
|
+
filter: contrast(1.35) brightness(1.15);
|
|
1683
|
+
}
|
|
1684
|
+
:root[data-theme="light"] .official-logo-box img {
|
|
1685
|
+
filter: contrast(1.4) brightness(0.85);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.brand-name-wrap {
|
|
1689
|
+
display: flex;
|
|
1690
|
+
align-items: baseline;
|
|
1691
|
+
gap: 7px;
|
|
1692
|
+
flex-shrink: 0;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
.brand-name {
|
|
1696
|
+
font-weight: 700;
|
|
1697
|
+
font-size: 14.5px;
|
|
1698
|
+
letter-spacing: -0.02em;
|
|
1699
|
+
color: var(--text-primary);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
.brand-ver {
|
|
1703
|
+
font-family: "IBM Plex Mono", monospace;
|
|
1704
|
+
font-size: 10px;
|
|
1705
|
+
color: var(--text-muted);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
.ws-tabs {
|
|
1709
|
+
display: flex;
|
|
1710
|
+
align-items: center;
|
|
1711
|
+
gap: 4px;
|
|
1712
|
+
margin-left: 8px;
|
|
1713
|
+
padding-left: 14px;
|
|
1714
|
+
border-left: 1px solid var(--border-subtle);
|
|
1715
|
+
min-width: 0;
|
|
1716
|
+
overflow-x: auto;
|
|
1717
|
+
scrollbar-width: none;
|
|
1718
|
+
}
|
|
1719
|
+
.ws-tabs::-webkit-scrollbar {
|
|
1720
|
+
display: none;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
.ws-tab {
|
|
1724
|
+
font-family: "IBM Plex Mono", monospace;
|
|
1725
|
+
font-size: 11px;
|
|
1726
|
+
color: var(--text-secondary);
|
|
1727
|
+
background: transparent;
|
|
1728
|
+
border: 1px solid transparent;
|
|
1729
|
+
height: 26px;
|
|
1730
|
+
padding: 0 10px;
|
|
1731
|
+
cursor: pointer;
|
|
1732
|
+
display: inline-flex;
|
|
1733
|
+
align-items: center;
|
|
1734
|
+
gap: 6px;
|
|
1735
|
+
transition: all 0.14s ease;
|
|
1736
|
+
max-width: 160px;
|
|
1737
|
+
flex-shrink: 0;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
.ws-tab span {
|
|
1741
|
+
overflow: hidden;
|
|
1742
|
+
text-overflow: ellipsis;
|
|
1743
|
+
white-space: nowrap;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
.ws-tab:hover {
|
|
1747
|
+
color: var(--text-primary);
|
|
1748
|
+
background: var(--win-elevated);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
.ws-tab.active {
|
|
1752
|
+
color: var(--text-primary);
|
|
1753
|
+
background: var(--win-bg);
|
|
1754
|
+
border-color: var(--border-mid);
|
|
1755
|
+
font-weight: 500;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
.topbar-right {
|
|
1759
|
+
display: flex;
|
|
1760
|
+
align-items: center;
|
|
1761
|
+
gap: 10px;
|
|
1762
|
+
flex-shrink: 0;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
.telemetry-badge {
|
|
1766
|
+
display: inline-flex;
|
|
1767
|
+
align-items: center;
|
|
1768
|
+
gap: 7px;
|
|
1769
|
+
font-family: "IBM Plex Mono", monospace;
|
|
1770
|
+
font-size: 11px;
|
|
1771
|
+
color: var(--text-secondary);
|
|
1772
|
+
padding: 4px 9px;
|
|
1773
|
+
background: var(--win-bg);
|
|
1774
|
+
border: 1px solid var(--border-subtle);
|
|
1775
|
+
max-width: 240px;
|
|
1776
|
+
white-space: nowrap;
|
|
1777
|
+
overflow: hidden;
|
|
1778
|
+
text-overflow: ellipsis;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
.pulse-dot {
|
|
1782
|
+
width: 6px;
|
|
1783
|
+
height: 6px;
|
|
1784
|
+
flex-shrink: 0;
|
|
1785
|
+
background: var(--status-working);
|
|
1786
|
+
animation: pulseGlow 2.2s infinite ease-in-out;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
@keyframes pulseGlow {
|
|
1790
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
1791
|
+
50% { opacity: 0.45; transform: scale(0.92); }
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
.theme-btn {
|
|
1795
|
+
height: 28px;
|
|
1796
|
+
padding: 0 10px;
|
|
1797
|
+
background: var(--btn-bg);
|
|
1798
|
+
border: 1px solid var(--border-mid);
|
|
1799
|
+
color: var(--text-secondary);
|
|
1800
|
+
font-family: "IBM Plex Mono", monospace;
|
|
1801
|
+
font-size: 11px;
|
|
1802
|
+
cursor: pointer;
|
|
1803
|
+
display: inline-flex;
|
|
1804
|
+
align-items: center;
|
|
1805
|
+
gap: 6px;
|
|
1806
|
+
white-space: nowrap;
|
|
1807
|
+
flex-shrink: 0;
|
|
1808
|
+
transition: all 0.14s ease;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
.theme-btn:hover {
|
|
1812
|
+
color: var(--text-primary);
|
|
1813
|
+
border-color: var(--border-strong);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
/* Live Baton Relay Deck */
|
|
1817
|
+
.relay-deck {
|
|
1818
|
+
display: grid;
|
|
1819
|
+
grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
|
|
1820
|
+
align-items: center;
|
|
1821
|
+
gap: 20px;
|
|
1822
|
+
padding: 16px 22px;
|
|
1823
|
+
background: var(--win-bg);
|
|
1824
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
1825
|
+
flex-shrink: 0;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
.deck-left {
|
|
1829
|
+
display: flex;
|
|
1830
|
+
flex-direction: column;
|
|
1831
|
+
gap: 6px;
|
|
1832
|
+
min-width: 0;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
.deck-eyebrow {
|
|
1836
|
+
display: flex;
|
|
1837
|
+
align-items: center;
|
|
1838
|
+
gap: 8px;
|
|
1839
|
+
font-family: "IBM Plex Mono", monospace;
|
|
1840
|
+
font-size: 11px;
|
|
1841
|
+
color: var(--text-muted);
|
|
1842
|
+
white-space: nowrap;
|
|
1843
|
+
overflow: hidden;
|
|
1844
|
+
text-overflow: ellipsis;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
.branch-tag {
|
|
1848
|
+
color: var(--text-secondary);
|
|
1849
|
+
background: var(--win-elevated);
|
|
1850
|
+
border: 1px solid var(--border-subtle);
|
|
1851
|
+
padding: 1px 6px;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
.prompt-input-wrap {
|
|
1855
|
+
position: relative;
|
|
1856
|
+
display: flex;
|
|
1857
|
+
align-items: center;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
.prompt-input {
|
|
1861
|
+
width: 100%;
|
|
1862
|
+
background: var(--win-header);
|
|
1863
|
+
border: 1px solid var(--border-subtle);
|
|
1864
|
+
color: var(--text-primary);
|
|
1865
|
+
font-family: "Plus Jakarta Sans", sans-serif;
|
|
1866
|
+
font-size: 13.5px;
|
|
1867
|
+
font-weight: 500;
|
|
1868
|
+
height: 36px;
|
|
1869
|
+
padding: 0 72px 0 12px;
|
|
1870
|
+
transition: border-color 0.15s ease, background-color 0.15s ease;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
.prompt-input:hover {
|
|
1874
|
+
border-color: var(--border-mid);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
.prompt-input:focus {
|
|
1878
|
+
outline: none;
|
|
1879
|
+
border-color: var(--border-strong);
|
|
1880
|
+
background: var(--win-bg);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
.prompt-edit-hint {
|
|
1884
|
+
position: absolute;
|
|
1885
|
+
right: 10px;
|
|
1886
|
+
font-family: "IBM Plex Mono", monospace;
|
|
1887
|
+
font-size: 10px;
|
|
1888
|
+
color: var(--text-muted);
|
|
1889
|
+
pointer-events: none;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
.relay-graph {
|
|
1893
|
+
display: flex;
|
|
1894
|
+
flex-direction: column;
|
|
1895
|
+
gap: 7px;
|
|
1896
|
+
padding: 11px 14px;
|
|
1897
|
+
background: var(--win-header);
|
|
1898
|
+
border: 1px solid var(--border-subtle);
|
|
1899
|
+
min-width: 0;
|
|
1900
|
+
position: relative;
|
|
1901
|
+
overflow: hidden;
|
|
1902
|
+
transition: border-color 0.22s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
.relay-graph.route-locked {
|
|
1906
|
+
border-color: rgba(34, 197, 94, 0.45);
|
|
1907
|
+
box-shadow: inset 0 0 24px rgba(34, 197, 94, 0.05);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
.relay-graph.relaying-surge {
|
|
1911
|
+
border-color: var(--status-working);
|
|
1912
|
+
box-shadow: inset 0 0 32px rgba(34, 197, 94, 0.14);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
.relay-nodes {
|
|
1916
|
+
display: flex;
|
|
1917
|
+
align-items: center;
|
|
1918
|
+
justify-content: space-between;
|
|
1919
|
+
gap: 10px;
|
|
1920
|
+
font-family: "IBM Plex Mono", monospace;
|
|
1921
|
+
font-size: 11.5px;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
.relay-endpoint {
|
|
1925
|
+
font-weight: 600;
|
|
1926
|
+
color: var(--text-primary);
|
|
1927
|
+
white-space: nowrap;
|
|
1928
|
+
transition: color 0.18s ease, transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
.relay-endpoint.target-preview {
|
|
1932
|
+
color: var(--text-secondary);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
.relay-endpoint.target-preview.active-route {
|
|
1936
|
+
color: var(--status-working);
|
|
1937
|
+
transform: translateX(-1px);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
.relay-wire {
|
|
1941
|
+
flex: 1;
|
|
1942
|
+
height: 10px;
|
|
1943
|
+
position: relative;
|
|
1944
|
+
display: flex;
|
|
1945
|
+
align-items: center;
|
|
1946
|
+
overflow: hidden;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
.relay-wire::before {
|
|
1950
|
+
content: "";
|
|
1951
|
+
width: 100%;
|
|
1952
|
+
height: 1px;
|
|
1953
|
+
background: repeating-linear-gradient(
|
|
1954
|
+
90deg,
|
|
1955
|
+
var(--relay-track) 0px,
|
|
1956
|
+
var(--relay-track) 4px,
|
|
1957
|
+
transparent 4px,
|
|
1958
|
+
transparent 7px
|
|
1959
|
+
);
|
|
1960
|
+
transition: opacity 0.2s ease;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
.relay-wire::after {
|
|
1964
|
+
content: "";
|
|
1965
|
+
position: absolute;
|
|
1966
|
+
top: 50%;
|
|
1967
|
+
transform: translateY(-50%);
|
|
1968
|
+
left: -40%;
|
|
1969
|
+
width: 40%;
|
|
1970
|
+
height: 2px;
|
|
1971
|
+
background: linear-gradient(90deg, transparent, var(--relay-pulse), var(--status-working));
|
|
1972
|
+
box-shadow: 0 0 8px rgba(34, 197, 94, 0.45);
|
|
1973
|
+
animation: wireSlide 2.2s infinite cubic-bezier(0.4, 0, 0.2, 1);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
.relay-graph.route-locked .relay-wire::after {
|
|
1977
|
+
height: 2px;
|
|
1978
|
+
background: linear-gradient(90deg, transparent, rgba(34, 197, 94, 0.5), var(--status-working));
|
|
1979
|
+
box-shadow: 0 0 12px rgba(34, 197, 94, 0.8);
|
|
1980
|
+
animation-duration: 0.72s;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
.relay-graph.relaying-surge .relay-wire::after {
|
|
1984
|
+
height: 3px;
|
|
1985
|
+
width: 65%;
|
|
1986
|
+
background: linear-gradient(90deg, transparent, #4ade80, #ecfdf5);
|
|
1987
|
+
box-shadow: 0 0 16px rgba(34, 197, 94, 1);
|
|
1988
|
+
animation-duration: 0.36s;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
@keyframes wireSlide {
|
|
1992
|
+
0% { left: -40%; opacity: 0.2; }
|
|
1993
|
+
30% { opacity: 1; }
|
|
1994
|
+
80% { opacity: 1; }
|
|
1995
|
+
100% { left: 100%; opacity: 0.1; }
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
.relay-metrics {
|
|
1999
|
+
display: flex;
|
|
2000
|
+
align-items: center;
|
|
2001
|
+
justify-content: space-between;
|
|
2002
|
+
gap: 8px;
|
|
2003
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2004
|
+
font-size: 10.5px;
|
|
2005
|
+
color: var(--text-muted);
|
|
2006
|
+
font-variant-numeric: tabular-nums;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
.diet-pill {
|
|
2010
|
+
color: var(--text-secondary);
|
|
2011
|
+
font-weight: 500;
|
|
2012
|
+
white-space: nowrap;
|
|
2013
|
+
transition: color 0.2s ease;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/* Scrollable Table Viewport & Responsive Process Table */
|
|
2017
|
+
.table-viewport,
|
|
2018
|
+
.receipt-panel,
|
|
2019
|
+
.baton-preview-box {
|
|
2020
|
+
scrollbar-width: none;
|
|
2021
|
+
-ms-overflow-style: none;
|
|
2022
|
+
}
|
|
2023
|
+
.table-viewport::-webkit-scrollbar,
|
|
2024
|
+
.receipt-panel::-webkit-scrollbar,
|
|
2025
|
+
.baton-preview-box::-webkit-scrollbar {
|
|
2026
|
+
width: 0;
|
|
2027
|
+
height: 0;
|
|
2028
|
+
display: none;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
.table-viewport {
|
|
2032
|
+
flex: 1 1 auto;
|
|
2033
|
+
min-height: 0;
|
|
2034
|
+
overflow-y: auto;
|
|
2035
|
+
overflow-x: hidden;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
.process-table {
|
|
2039
|
+
width: 100%;
|
|
2040
|
+
table-layout: fixed;
|
|
2041
|
+
border-collapse: collapse;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
.col-agent { width: 29%; }
|
|
2045
|
+
.col-state { width: 13%; }
|
|
2046
|
+
.col-quota { width: 33%; }
|
|
2047
|
+
.col-seen { width: 11%; }
|
|
2048
|
+
.col-act { width: 14%; }
|
|
2049
|
+
|
|
2050
|
+
.process-table thead th {
|
|
2051
|
+
position: sticky;
|
|
2052
|
+
top: 0;
|
|
2053
|
+
z-index: 5;
|
|
2054
|
+
height: 34px;
|
|
2055
|
+
padding: 0 12px;
|
|
2056
|
+
background: var(--win-header);
|
|
2057
|
+
box-shadow: inset 0 -1px 0 var(--border-subtle);
|
|
2058
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2059
|
+
font-size: 10.5px;
|
|
2060
|
+
font-weight: 500;
|
|
2061
|
+
color: var(--text-muted);
|
|
2062
|
+
text-align: left;
|
|
2063
|
+
user-select: none;
|
|
2064
|
+
white-space: nowrap;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
.process-table thead th:first-child { padding-left: 22px; }
|
|
2068
|
+
.process-table thead th:last-child { padding-right: 22px; text-align: right; }
|
|
2069
|
+
.process-table thead th.num-col { text-align: right; }
|
|
2070
|
+
|
|
2071
|
+
.process-table tbody tr {
|
|
2072
|
+
height: 64px;
|
|
2073
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
2074
|
+
transition: background-color 0.18s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
.process-table tbody tr.row-enter {
|
|
2078
|
+
animation: rowSlideIn 0.28s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
2079
|
+
animation-delay: calc(var(--row-idx, 0) * 18ms);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
@keyframes rowSlideIn {
|
|
2083
|
+
from {
|
|
2084
|
+
opacity: 0;
|
|
2085
|
+
transform: translateY(6px);
|
|
2086
|
+
}
|
|
2087
|
+
to {
|
|
2088
|
+
opacity: 1;
|
|
2089
|
+
transform: translateY(0);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
.process-table tbody tr:last-child {
|
|
2094
|
+
border-bottom: none;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
.process-table tbody tr:hover {
|
|
2098
|
+
background: var(--row-hover);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
.process-table tbody tr.route-target-hover {
|
|
2102
|
+
background: var(--row-hover);
|
|
2103
|
+
box-shadow: inset 3px 0 0 rgba(34, 197, 94, 0.55);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
.process-table tbody tr.is-working {
|
|
2107
|
+
background: var(--row-working);
|
|
2108
|
+
box-shadow: inset 3px 0 0 var(--status-working);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
.process-table tbody tr.just-relayed {
|
|
2112
|
+
animation: relayFlash 0.9s cubic-bezier(0.16, 1, 0.3, 1);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
@keyframes relayFlash {
|
|
2116
|
+
0% { background: rgba(34, 197, 94, 0.22); }
|
|
2117
|
+
100% { background: var(--row-working); }
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
.process-table td {
|
|
2121
|
+
padding: 0 12px;
|
|
2122
|
+
vertical-align: middle;
|
|
2123
|
+
overflow: hidden;
|
|
2124
|
+
text-overflow: ellipsis;
|
|
2125
|
+
white-space: nowrap;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
.process-table td:first-child { padding-left: 22px; }
|
|
2129
|
+
.process-table td:last-child { padding-right: 22px; text-align: right; }
|
|
2130
|
+
|
|
2131
|
+
.agent-cell {
|
|
2132
|
+
display: flex;
|
|
2133
|
+
align-items: center;
|
|
2134
|
+
min-width: 0;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
.agent-info {
|
|
2138
|
+
display: flex;
|
|
2139
|
+
flex-direction: column;
|
|
2140
|
+
gap: 3px;
|
|
2141
|
+
min-width: 0;
|
|
2142
|
+
width: 100%;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
.agent-title-row {
|
|
2146
|
+
display: flex;
|
|
2147
|
+
align-items: center;
|
|
2148
|
+
gap: 8px;
|
|
2149
|
+
min-width: 0;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
.agent-name {
|
|
2153
|
+
font-weight: 600;
|
|
2154
|
+
font-size: 13.5px;
|
|
2155
|
+
letter-spacing: -0.01em;
|
|
2156
|
+
color: var(--text-primary);
|
|
2157
|
+
overflow: hidden;
|
|
2158
|
+
text-overflow: ellipsis;
|
|
2159
|
+
white-space: nowrap;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
.model-badge {
|
|
2163
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2164
|
+
font-size: 10px;
|
|
2165
|
+
color: var(--text-secondary);
|
|
2166
|
+
background: var(--win-elevated);
|
|
2167
|
+
border: 1px solid var(--border-subtle);
|
|
2168
|
+
padding: 1px 5px;
|
|
2169
|
+
line-height: 1.3;
|
|
2170
|
+
flex-shrink: 0;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
.agent-path {
|
|
2174
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2175
|
+
font-size: 11px;
|
|
2176
|
+
color: var(--text-muted);
|
|
2177
|
+
overflow: hidden;
|
|
2178
|
+
text-overflow: ellipsis;
|
|
2179
|
+
white-space: nowrap;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
.state-badge {
|
|
2183
|
+
display: inline-flex;
|
|
2184
|
+
align-items: center;
|
|
2185
|
+
gap: 7px;
|
|
2186
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2187
|
+
font-size: 11.5px;
|
|
2188
|
+
font-weight: 500;
|
|
2189
|
+
padding: 3px 8px;
|
|
2190
|
+
border: 1px solid transparent;
|
|
2191
|
+
white-space: nowrap;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
.state-badge.Working {
|
|
2195
|
+
color: var(--status-working);
|
|
2196
|
+
background: rgba(34, 197, 94, 0.08);
|
|
2197
|
+
border-color: rgba(34, 197, 94, 0.25);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
.state-badge.Done {
|
|
2201
|
+
color: var(--text-secondary);
|
|
2202
|
+
background: var(--win-elevated);
|
|
2203
|
+
border-color: var(--border-subtle);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
.state-badge.Idle {
|
|
2207
|
+
color: var(--text-muted);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
.state-sq {
|
|
2211
|
+
width: 6px;
|
|
2212
|
+
height: 6px;
|
|
2213
|
+
flex-shrink: 0;
|
|
2214
|
+
}
|
|
2215
|
+
.state-sq.Working { background: var(--status-working); }
|
|
2216
|
+
.state-sq.Done { background: var(--status-done); }
|
|
2217
|
+
.state-sq.Idle { background: var(--status-idle); }
|
|
2218
|
+
|
|
2219
|
+
.quota-cell {
|
|
2220
|
+
display: flex;
|
|
2221
|
+
flex-direction: column;
|
|
2222
|
+
gap: 6px;
|
|
2223
|
+
padding-right: 12px;
|
|
2224
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2225
|
+
font-variant-numeric: tabular-nums;
|
|
2226
|
+
min-width: 0;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
.quota-meta {
|
|
2230
|
+
display: flex;
|
|
2231
|
+
align-items: baseline;
|
|
2232
|
+
justify-content: space-between;
|
|
2233
|
+
gap: 8px;
|
|
2234
|
+
min-width: 0;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
.quota-val {
|
|
2238
|
+
font-size: 12.5px;
|
|
2239
|
+
font-weight: 600;
|
|
2240
|
+
color: var(--text-primary);
|
|
2241
|
+
flex-shrink: 0;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
.quota-val.subtle {
|
|
2245
|
+
font-weight: 500;
|
|
2246
|
+
color: var(--text-secondary);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
.quota-sub {
|
|
2250
|
+
font-size: 11px;
|
|
2251
|
+
color: var(--text-muted);
|
|
2252
|
+
overflow: hidden;
|
|
2253
|
+
text-overflow: ellipsis;
|
|
2254
|
+
white-space: nowrap;
|
|
2255
|
+
min-width: 0;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
.seg-bar {
|
|
2259
|
+
display: grid;
|
|
2260
|
+
grid-template-columns: repeat(20, 1fr);
|
|
2261
|
+
gap: 2px;
|
|
2262
|
+
height: 6px;
|
|
2263
|
+
width: 100%;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
.seg {
|
|
2267
|
+
background: var(--seg-empty);
|
|
2268
|
+
height: 100%;
|
|
2269
|
+
transform-origin: left center;
|
|
2270
|
+
transition: background-color 0.24s cubic-bezier(0.16, 1, 0.3, 1), transform 0.2s ease;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
.process-table tbody tr.row-enter .seg.on {
|
|
2274
|
+
animation: segFillIn 0.32s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
2275
|
+
animation-delay: calc(var(--row-idx, 0) * 16ms + var(--seg-idx, 0) * 11ms);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
@keyframes segFillIn {
|
|
2279
|
+
0% { opacity: 0.15; transform: scaleY(0.35); }
|
|
2280
|
+
100% { opacity: 1; transform: scaleY(1); }
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
.seg.on { background: var(--seg-fill); }
|
|
2284
|
+
.seg.on.working { background: var(--seg-working); }
|
|
2285
|
+
.seg.on.warn { background: var(--seg-warn); }
|
|
2286
|
+
|
|
2287
|
+
.seen-cell {
|
|
2288
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2289
|
+
font-size: 11.5px;
|
|
2290
|
+
color: var(--text-secondary);
|
|
2291
|
+
font-variant-numeric: tabular-nums;
|
|
2292
|
+
text-align: right;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
.handoff-btn {
|
|
2296
|
+
width: 100px;
|
|
2297
|
+
max-width: 100%;
|
|
2298
|
+
height: 30px;
|
|
2299
|
+
background: var(--btn-bg);
|
|
2300
|
+
color: var(--text-primary);
|
|
2301
|
+
border: 1px solid var(--border-mid);
|
|
2302
|
+
font-family: "Plus Jakarta Sans", sans-serif;
|
|
2303
|
+
font-size: 12px;
|
|
2304
|
+
font-weight: 600;
|
|
2305
|
+
cursor: pointer;
|
|
2306
|
+
display: inline-flex;
|
|
2307
|
+
align-items: center;
|
|
2308
|
+
justify-content: center;
|
|
2309
|
+
gap: 6px;
|
|
2310
|
+
white-space: nowrap;
|
|
2311
|
+
position: relative;
|
|
2312
|
+
overflow: hidden;
|
|
2313
|
+
transition: all 0.16s cubic-bezier(0.16, 1, 0.3, 1);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
.handoff-btn .arr {
|
|
2317
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2318
|
+
font-size: 11px;
|
|
2319
|
+
transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
.handoff-btn:hover {
|
|
2323
|
+
background: var(--btn-bg-hover);
|
|
2324
|
+
color: var(--btn-text-hover);
|
|
2325
|
+
border-color: var(--btn-bg-hover);
|
|
2326
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.22);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
.handoff-btn:hover .arr {
|
|
2330
|
+
transform: translateX(3.5px);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
.handoff-btn:active {
|
|
2334
|
+
transform: scale(0.96);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
.handoff-btn.is-relaying {
|
|
2338
|
+
border-color: var(--status-working);
|
|
2339
|
+
color: var(--status-working);
|
|
2340
|
+
background: rgba(34, 197, 94, 0.1);
|
|
2341
|
+
pointer-events: none;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
.holding-pill {
|
|
2345
|
+
display: inline-flex;
|
|
2346
|
+
align-items: center;
|
|
2347
|
+
justify-content: center;
|
|
2348
|
+
gap: 6px;
|
|
2349
|
+
width: 100px;
|
|
2350
|
+
max-width: 100%;
|
|
2351
|
+
height: 30px;
|
|
2352
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2353
|
+
font-size: 11px;
|
|
2354
|
+
font-weight: 500;
|
|
2355
|
+
color: var(--status-working);
|
|
2356
|
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
2357
|
+
background: rgba(34, 197, 94, 0.06);
|
|
2358
|
+
white-space: nowrap;
|
|
2359
|
+
animation: holdingPulse 3s infinite ease-in-out;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
@keyframes holdingPulse {
|
|
2363
|
+
0%, 100% { border-color: rgba(34, 197, 94, 0.3); }
|
|
2364
|
+
50% { border-color: rgba(34, 197, 94, 0.6); }
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
/* ==========================================================================
|
|
2368
|
+
LIVE 5-STEP HANDOFF VERIFICATION RECEIPT PANEL
|
|
2369
|
+
========================================================================== */
|
|
2370
|
+
.receipt-panel {
|
|
2371
|
+
display: none;
|
|
2372
|
+
border-top: 1px solid var(--border-mid);
|
|
2373
|
+
background: var(--win-header);
|
|
2374
|
+
padding: 14px 22px;
|
|
2375
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2376
|
+
font-size: 11.5px;
|
|
2377
|
+
flex-shrink: 0;
|
|
2378
|
+
max-height: 42vh;
|
|
2379
|
+
overflow-y: auto;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
.receipt-panel.visible {
|
|
2383
|
+
display: flex;
|
|
2384
|
+
flex-direction: column;
|
|
2385
|
+
gap: 10px;
|
|
2386
|
+
animation: drawerSlideUp 0.26s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
@keyframes drawerSlideUp {
|
|
2390
|
+
from {
|
|
2391
|
+
opacity: 0;
|
|
2392
|
+
transform: translateY(8px);
|
|
2393
|
+
}
|
|
2394
|
+
to {
|
|
2395
|
+
opacity: 1;
|
|
2396
|
+
transform: translateY(0);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
.receipt-header {
|
|
2401
|
+
display: flex;
|
|
2402
|
+
flex-wrap: wrap;
|
|
2403
|
+
align-items: center;
|
|
2404
|
+
justify-content: space-between;
|
|
2405
|
+
gap: 8px;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
.receipt-title {
|
|
2409
|
+
display: inline-flex;
|
|
2410
|
+
align-items: center;
|
|
2411
|
+
gap: 8px;
|
|
2412
|
+
font-weight: 600;
|
|
2413
|
+
color: var(--status-working);
|
|
2414
|
+
font-size: 12px;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
.receipt-actions {
|
|
2418
|
+
display: flex;
|
|
2419
|
+
align-items: center;
|
|
2420
|
+
gap: 8px;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
.steps-grid {
|
|
2424
|
+
display: grid;
|
|
2425
|
+
grid-template-columns: 1fr;
|
|
2426
|
+
gap: 6px;
|
|
2427
|
+
background: var(--win-bg);
|
|
2428
|
+
border: 1px solid var(--border-subtle);
|
|
2429
|
+
padding: 10px 14px;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
.step-row {
|
|
2433
|
+
display: grid;
|
|
2434
|
+
grid-template-columns: minmax(180px, 260px) minmax(0, 1fr);
|
|
2435
|
+
align-items: baseline;
|
|
2436
|
+
gap: 12px;
|
|
2437
|
+
font-size: 11px;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
.step-row.step-cascade {
|
|
2441
|
+
animation: stepReveal 0.18s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
2442
|
+
animation-delay: calc(var(--step-idx, 0) * 20ms);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
@keyframes stepReveal {
|
|
2446
|
+
from {
|
|
2447
|
+
opacity: 0;
|
|
2448
|
+
transform: translateX(-6px);
|
|
2449
|
+
}
|
|
2450
|
+
to {
|
|
2451
|
+
opacity: 1;
|
|
2452
|
+
transform: translateX(0);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
.step-name {
|
|
2457
|
+
color: var(--text-primary);
|
|
2458
|
+
font-weight: 500;
|
|
2459
|
+
display: inline-flex;
|
|
2460
|
+
align-items: center;
|
|
2461
|
+
gap: 7px;
|
|
2462
|
+
white-space: nowrap;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
.step-ok {
|
|
2466
|
+
color: var(--status-working);
|
|
2467
|
+
font-weight: 600;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
.step-detail {
|
|
2471
|
+
color: var(--text-secondary);
|
|
2472
|
+
overflow: hidden;
|
|
2473
|
+
text-overflow: ellipsis;
|
|
2474
|
+
white-space: nowrap;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
.baton-preview-box {
|
|
2478
|
+
display: none;
|
|
2479
|
+
background: var(--win-bg);
|
|
2480
|
+
border: 1px solid var(--border-subtle);
|
|
2481
|
+
padding: 12px;
|
|
2482
|
+
max-height: 170px;
|
|
2483
|
+
overflow-y: auto;
|
|
2484
|
+
font-size: 11px;
|
|
2485
|
+
color: var(--text-secondary);
|
|
2486
|
+
white-space: pre-wrap;
|
|
2487
|
+
line-height: 1.5;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
.baton-preview-box.open {
|
|
2491
|
+
display: block;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
/* Inspector Drawer */
|
|
2495
|
+
.inspector-drawer {
|
|
2496
|
+
display: none;
|
|
2497
|
+
padding: 12px 22px;
|
|
2498
|
+
border-top: 1px solid var(--border-subtle);
|
|
2499
|
+
background: var(--win-header);
|
|
2500
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2501
|
+
font-size: 11px;
|
|
2502
|
+
flex-shrink: 0;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
.inspector-drawer.open {
|
|
2506
|
+
display: grid;
|
|
2507
|
+
grid-template-columns: 1fr 1fr;
|
|
2508
|
+
gap: 16px;
|
|
2509
|
+
animation: drawerSlideUp 0.22s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
.insp-col {
|
|
2513
|
+
display: flex;
|
|
2514
|
+
flex-direction: column;
|
|
2515
|
+
gap: 6px;
|
|
2516
|
+
min-width: 0;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
.insp-title {
|
|
2520
|
+
color: var(--text-muted);
|
|
2521
|
+
font-size: 10.5px;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
.insp-list {
|
|
2525
|
+
color: var(--text-secondary);
|
|
2526
|
+
display: flex;
|
|
2527
|
+
flex-wrap: wrap;
|
|
2528
|
+
gap: 6px;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
.insp-tag {
|
|
2532
|
+
border: 1px solid var(--border-mid);
|
|
2533
|
+
background: var(--win-bg);
|
|
2534
|
+
padding: 2px 8px;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
/* Command Bar Footer — Adaptive Zero-Break Layout */
|
|
2538
|
+
.win-footer {
|
|
2539
|
+
display: flex;
|
|
2540
|
+
flex-wrap: nowrap;
|
|
2541
|
+
align-items: center;
|
|
2542
|
+
justify-content: space-between;
|
|
2543
|
+
gap: 8px 12px;
|
|
2544
|
+
min-height: 46px;
|
|
2545
|
+
padding: 8px 22px;
|
|
2546
|
+
background: var(--win-header);
|
|
2547
|
+
border-top: 1px solid var(--border-subtle);
|
|
2548
|
+
flex-shrink: 0;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
.footer-cmds {
|
|
2552
|
+
display: flex;
|
|
2553
|
+
flex-wrap: nowrap;
|
|
2554
|
+
align-items: center;
|
|
2555
|
+
gap: 5px;
|
|
2556
|
+
flex: 0 0 auto;
|
|
2557
|
+
min-width: 0;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
.cmd-btn {
|
|
2561
|
+
height: 28px;
|
|
2562
|
+
padding: 0 8px;
|
|
2563
|
+
background: var(--win-bg);
|
|
2564
|
+
border: 1px solid var(--border-subtle);
|
|
2565
|
+
color: var(--text-secondary);
|
|
2566
|
+
font-family: "Plus Jakarta Sans", sans-serif;
|
|
2567
|
+
font-size: 11px;
|
|
2568
|
+
font-weight: 500;
|
|
2569
|
+
cursor: pointer;
|
|
2570
|
+
display: inline-flex;
|
|
2571
|
+
align-items: center;
|
|
2572
|
+
gap: 5px;
|
|
2573
|
+
white-space: nowrap;
|
|
2574
|
+
flex-shrink: 0;
|
|
2575
|
+
transition: all 0.14s ease;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
.cmd-btn:hover {
|
|
2579
|
+
color: var(--text-primary);
|
|
2580
|
+
border-color: var(--border-mid);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
.cmd-btn:active,
|
|
2584
|
+
.cmd-btn.kbd-pressed,
|
|
2585
|
+
.theme-btn:active,
|
|
2586
|
+
.theme-btn.kbd-pressed {
|
|
2587
|
+
transform: scale(0.96);
|
|
2588
|
+
border-color: var(--border-strong);
|
|
2589
|
+
color: var(--text-primary);
|
|
2590
|
+
background: var(--win-elevated);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
.cmd-btn.active-toggle {
|
|
2594
|
+
color: var(--text-primary);
|
|
2595
|
+
border-color: var(--border-strong);
|
|
2596
|
+
background: var(--win-elevated);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
.kbd {
|
|
2600
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2601
|
+
font-size: 10px;
|
|
2602
|
+
color: var(--text-muted);
|
|
2603
|
+
padding-left: 2px;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
.footer-receipt {
|
|
2607
|
+
font-family: "IBM Plex Mono", monospace;
|
|
2608
|
+
font-size: 11px;
|
|
2609
|
+
color: var(--text-muted);
|
|
2610
|
+
white-space: nowrap;
|
|
2611
|
+
overflow: hidden;
|
|
2612
|
+
text-overflow: ellipsis;
|
|
2613
|
+
flex: 1 1 0;
|
|
2614
|
+
min-width: 0;
|
|
2615
|
+
text-align: right;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
/* ==========================================================================
|
|
2619
|
+
RESPONSIVE BREAKPOINTS (ANY WINDOW SIZE: WIDE / SPLIT-SCREEN / COMPACT)
|
|
2620
|
+
========================================================================== */
|
|
2621
|
+
@media (max-width: 880px) {
|
|
2622
|
+
.win-footer, .footer-cmds {
|
|
2623
|
+
flex-wrap: wrap;
|
|
2624
|
+
}
|
|
2625
|
+
.footer-cmds {
|
|
2626
|
+
flex: 1 1 100%;
|
|
2627
|
+
}
|
|
2628
|
+
.footer-receipt {
|
|
2629
|
+
flex: 1 1 100%;
|
|
2630
|
+
text-align: right;
|
|
2631
|
+
}
|
|
2632
|
+
.relay-deck {
|
|
2633
|
+
grid-template-columns: 1fr;
|
|
2634
|
+
gap: 12px;
|
|
2635
|
+
}
|
|
2636
|
+
.col-seen, .th-seen, .seen-cell {
|
|
2637
|
+
display: none;
|
|
2638
|
+
}
|
|
2639
|
+
.col-agent { width: 35%; }
|
|
2640
|
+
.col-state { width: 13%; }
|
|
2641
|
+
.col-quota { width: 35%; }
|
|
2642
|
+
.col-act { width: 17%; }
|
|
2643
|
+
.step-row {
|
|
2644
|
+
grid-template-columns: 1fr;
|
|
2645
|
+
gap: 2px;
|
|
2646
|
+
}
|
|
2647
|
+
.inspector-drawer.open {
|
|
2648
|
+
grid-template-columns: 1fr;
|
|
2649
|
+
gap: 10px;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
@media (max-width: 640px) {
|
|
2654
|
+
body {
|
|
2655
|
+
padding: 0;
|
|
2656
|
+
}
|
|
2657
|
+
.app-window {
|
|
2658
|
+
width: 100%;
|
|
2659
|
+
height: 100vh;
|
|
2660
|
+
max-height: 100vh;
|
|
2661
|
+
border-left: none;
|
|
2662
|
+
border-right: none;
|
|
2663
|
+
}
|
|
2664
|
+
.win-topbar, .relay-deck, .win-footer, .receipt-panel, .inspector-drawer {
|
|
2665
|
+
padding-left: 14px;
|
|
2666
|
+
padding-right: 14px;
|
|
2667
|
+
}
|
|
2668
|
+
.process-table thead th:first-child,
|
|
2669
|
+
.process-table td:first-child {
|
|
2670
|
+
padding-left: 14px;
|
|
2671
|
+
}
|
|
2672
|
+
.process-table thead th:last-child,
|
|
2673
|
+
.process-table td:last-child {
|
|
2674
|
+
padding-right: 14px;
|
|
2675
|
+
}
|
|
2676
|
+
.telemetry-badge {
|
|
2677
|
+
display: none;
|
|
2678
|
+
}
|
|
2679
|
+
.col-state, .th-state, .td-state {
|
|
2680
|
+
display: none;
|
|
2681
|
+
}
|
|
2682
|
+
.col-agent { width: 42%; }
|
|
2683
|
+
.col-quota { width: 36%; }
|
|
2684
|
+
.col-act { width: 22%; }
|
|
2685
|
+
.handoff-btn, .holding-pill {
|
|
2686
|
+
width: 88px;
|
|
2687
|
+
font-size: 11px;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
</style>
|
|
2691
|
+
</head>
|
|
2692
|
+
<body>
|
|
2693
|
+
|
|
2694
|
+
<div class="app-window">
|
|
2695
|
+
|
|
2696
|
+
<!-- 1. Top Brand & One-Click Workspace Bar -->
|
|
2697
|
+
<header class="win-topbar">
|
|
2698
|
+
<div class="brand-cluster">
|
|
2699
|
+
<div class="official-logo-box">
|
|
2700
|
+
<img id="official-logo" src="/logo-dark.png" alt="Vicem Logo" />
|
|
2701
|
+
</div>
|
|
2702
|
+
<div class="brand-name-wrap">
|
|
2703
|
+
<span class="brand-name">Vicem</span>
|
|
2704
|
+
<span class="brand-ver">v1.0</span>
|
|
2705
|
+
</div>
|
|
2706
|
+
<div class="ws-tabs" id="ws-tabs"></div>
|
|
2707
|
+
</div>
|
|
2708
|
+
|
|
2709
|
+
<div class="topbar-right">
|
|
2710
|
+
<div class="telemetry-badge">
|
|
2711
|
+
<span class="pulse-dot"></span>
|
|
2712
|
+
<span id="mounted-status">Live telemetry</span>
|
|
2713
|
+
</div>
|
|
2714
|
+
<button class="theme-btn" onclick="toggleTheme()" title="Toggle Light / Dark Mode (T)">
|
|
2715
|
+
<span id="theme-icon">☀</span>
|
|
2716
|
+
<span id="theme-label">Light</span>
|
|
2717
|
+
<span class="kbd">T</span>
|
|
2718
|
+
</button>
|
|
2719
|
+
</div>
|
|
2720
|
+
</header>
|
|
2721
|
+
|
|
2722
|
+
<!-- 2. Live Baton Relay Deck -->
|
|
2723
|
+
<section class="relay-deck">
|
|
2724
|
+
<div class="deck-left">
|
|
2725
|
+
<div class="deck-eyebrow">
|
|
2726
|
+
<span>Active session baton</span>
|
|
2727
|
+
<span>·</span>
|
|
2728
|
+
<span class="branch-tag" id="branch-badge">workspace (main)</span>
|
|
2729
|
+
<span>·</span>
|
|
2730
|
+
<span>Editable handoff instruction</span>
|
|
2731
|
+
</div>
|
|
2732
|
+
<div class="prompt-input-wrap">
|
|
2733
|
+
<input
|
|
2734
|
+
type="text"
|
|
2735
|
+
class="prompt-input"
|
|
2736
|
+
id="task-input"
|
|
2737
|
+
value="Loading session context..."
|
|
2738
|
+
placeholder="Type handoff goal or instruction before relaying..."
|
|
2739
|
+
onchange="saveCustomGoal(this.value)"
|
|
2740
|
+
/>
|
|
2741
|
+
<span class="prompt-edit-hint">✎ Edit</span>
|
|
2742
|
+
</div>
|
|
2743
|
+
</div>
|
|
2744
|
+
|
|
2745
|
+
<div class="relay-graph">
|
|
2746
|
+
<div class="relay-nodes">
|
|
2747
|
+
<span class="relay-endpoint" id="relay-source">Antigravity</span>
|
|
2748
|
+
<div class="relay-wire"></div>
|
|
2749
|
+
<span class="relay-endpoint target-preview" id="relay-target">Select target</span>
|
|
2750
|
+
</div>
|
|
2751
|
+
<div class="relay-metrics">
|
|
2752
|
+
<span class="diet-pill" id="stat-tokens">—</span>
|
|
2753
|
+
<span id="stat-secrets">0 masked</span>
|
|
2754
|
+
</div>
|
|
2755
|
+
</div>
|
|
2756
|
+
</section>
|
|
2757
|
+
|
|
2758
|
+
<!-- 3. Fluid Responsive Agent Process & Quota Table -->
|
|
2759
|
+
<div class="table-viewport">
|
|
2760
|
+
<table class="process-table">
|
|
2761
|
+
<colgroup>
|
|
2762
|
+
<col class="col-agent" />
|
|
2763
|
+
<col class="col-state" />
|
|
2764
|
+
<col class="col-quota" />
|
|
2765
|
+
<col class="col-seen" />
|
|
2766
|
+
<col class="col-act" />
|
|
2767
|
+
</colgroup>
|
|
2768
|
+
<thead>
|
|
2769
|
+
<tr>
|
|
2770
|
+
<th>Agent & Workspace</th>
|
|
2771
|
+
<th class="th-state">State</th>
|
|
2772
|
+
<th>Remaining Capacity</th>
|
|
2773
|
+
<th class="num-col th-seen">Last Activity</th>
|
|
2774
|
+
<th>Relay Baton</th>
|
|
2775
|
+
</tr>
|
|
2776
|
+
</thead>
|
|
2777
|
+
<tbody id="agent-rows"></tbody>
|
|
2778
|
+
</table>
|
|
2779
|
+
</div>
|
|
2780
|
+
|
|
2781
|
+
<!-- 4. Live 5-Step Handoff Verification Receipt Panel (Appears on Handoff) -->
|
|
2782
|
+
<section class="receipt-panel" id="receipt-panel">
|
|
2783
|
+
<div class="receipt-header">
|
|
2784
|
+
<span class="receipt-title" id="receipt-title">✓ Handoff Execution Verified</span>
|
|
2785
|
+
<div class="receipt-actions">
|
|
2786
|
+
<button class="cmd-btn" onclick="toggleBatonPreview()">
|
|
2787
|
+
<span id="btn-preview-label">View .vicem/BATON.md</span>
|
|
2788
|
+
</button>
|
|
2789
|
+
<button class="cmd-btn" onclick="closeReceiptPanel()">
|
|
2790
|
+
<span>Dismiss</span>
|
|
2791
|
+
</button>
|
|
2792
|
+
</div>
|
|
2793
|
+
</div>
|
|
2794
|
+
<div class="steps-grid" id="receipt-steps"></div>
|
|
2795
|
+
<pre class="baton-preview-box" id="baton-preview-box"></pre>
|
|
2796
|
+
</section>
|
|
2797
|
+
|
|
2798
|
+
<!-- 5. Collapsible Inspector Drawer -->
|
|
2799
|
+
<div class="inspector-drawer" id="inspector-drawer">
|
|
2800
|
+
<div class="insp-col">
|
|
2801
|
+
<span class="insp-title">Tracked modified files in baton</span>
|
|
2802
|
+
<div class="insp-list" id="insp-files"></div>
|
|
2803
|
+
</div>
|
|
2804
|
+
<div class="insp-col">
|
|
2805
|
+
<span class="insp-title">Mounted rule files in workspace</span>
|
|
2806
|
+
<div class="insp-list" id="insp-mounts"></div>
|
|
2807
|
+
</div>
|
|
2808
|
+
</div>
|
|
2809
|
+
|
|
2810
|
+
<!-- 6. Command Bar Footer -->
|
|
2811
|
+
<footer class="win-footer">
|
|
2812
|
+
<div class="footer-cmds">
|
|
2813
|
+
<button class="cmd-btn" onclick="triggerSelfDiet()">
|
|
2814
|
+
<span>Compact current</span>
|
|
2815
|
+
<span class="kbd">R</span>
|
|
2816
|
+
</button>
|
|
2817
|
+
<button class="cmd-btn" onclick="triggerBack()">
|
|
2818
|
+
<span>Return to previous</span>
|
|
2819
|
+
<span class="kbd">Z</span>
|
|
2820
|
+
</button>
|
|
2821
|
+
<button class="cmd-btn" onclick="copyBaton()">
|
|
2822
|
+
<span>Copy baton</span>
|
|
2823
|
+
<span class="kbd">C</span>
|
|
2824
|
+
</button>
|
|
2825
|
+
<button class="cmd-btn" id="btn-inspect" onclick="toggleInspector()">
|
|
2826
|
+
<span>Inspect files</span>
|
|
2827
|
+
<span class="kbd">I</span>
|
|
2828
|
+
</button>
|
|
2829
|
+
<button class="cmd-btn" id="btn-all-platforms" onclick="toggleAllPlatforms()">
|
|
2830
|
+
<span id="platforms-label">All 12 platforms</span>
|
|
2831
|
+
<span class="kbd">A</span>
|
|
2832
|
+
</button>
|
|
2833
|
+
<button class="cmd-btn active-toggle" id="btn-launch-toggle" onclick="toggleLaunchApp()">
|
|
2834
|
+
<span id="launch-label">Launch: Desktop App</span>
|
|
2835
|
+
</button>
|
|
2836
|
+
</div>
|
|
2837
|
+
<div class="footer-receipt" id="status-line">Ready · Desktop App launch enabled</div>
|
|
2838
|
+
</footer>
|
|
2839
|
+
|
|
2840
|
+
</div>
|
|
2841
|
+
|
|
2842
|
+
<script>
|
|
2843
|
+
const CLIENT_GUI_VERSION = 'v16';
|
|
2844
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
2845
|
+
const isStaticMode = Boolean(navigator.webdriver || urlParams.get('static') === '1');
|
|
2846
|
+
const isDemoMode = urlParams.get('demo') === '1';
|
|
2847
|
+
if (isStaticMode) {
|
|
2848
|
+
document.documentElement.classList.add('no-anim');
|
|
2849
|
+
}
|
|
2850
|
+
let state = null;
|
|
2851
|
+
let currentTheme = localStorage.getItem('vicem-theme') || 'dark';
|
|
2852
|
+
let launchMode = 'app'; // 'app' (Desktop GUI App) | 'cli' (Terminal CLI) | 'off' (Clipboard only)
|
|
2853
|
+
let inspectorOpen = false;
|
|
2854
|
+
let showAllPlatforms = new URLSearchParams(window.location.search).get('all') === '1';
|
|
2855
|
+
let batonPreviewOpen = false;
|
|
2856
|
+
let receiptDismissed = false;
|
|
2857
|
+
let isEditingGoal = false;
|
|
2858
|
+
let lastRenderedReceiptTs = null;
|
|
2859
|
+
let lastAnimRaw = 0;
|
|
2860
|
+
let lastAnimDiet = 0;
|
|
2861
|
+
let relayingTargetId = null;
|
|
2862
|
+
|
|
2863
|
+
if (showAllPlatforms) {
|
|
2864
|
+
document.getElementById('btn-all-platforms').classList.add('active-toggle');
|
|
2865
|
+
document.getElementById('platforms-label').textContent = 'Installed only';
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
function flashKeybindButton(selector) {
|
|
2869
|
+
const btn = document.querySelector(selector);
|
|
2870
|
+
if (!btn) return;
|
|
2871
|
+
btn.classList.add('kbd-pressed');
|
|
2872
|
+
setTimeout(() => btn.classList.remove('kbd-pressed'), 160);
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
function animateTokenStats(rawTarget, dietTarget, savedPct) {
|
|
2876
|
+
const el = document.getElementById('stat-tokens');
|
|
2877
|
+
if (!el) return;
|
|
2878
|
+
if (isStaticMode || (lastAnimRaw === rawTarget && lastAnimDiet === dietTarget)) {
|
|
2879
|
+
lastAnimRaw = rawTarget;
|
|
2880
|
+
lastAnimDiet = dietTarget;
|
|
2881
|
+
el.textContent = \`\${rawTarget.toLocaleString()} → \${dietTarget.toLocaleString()} tok (-\${savedPct}%)\`;
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
const startRaw = lastAnimRaw || rawTarget;
|
|
2885
|
+
const startDiet = lastAnimDiet || dietTarget;
|
|
2886
|
+
lastAnimRaw = rawTarget;
|
|
2887
|
+
lastAnimDiet = dietTarget;
|
|
2888
|
+
const startTime = performance.now();
|
|
2889
|
+
const duration = 360;
|
|
2890
|
+
function tick(now) {
|
|
2891
|
+
const t = Math.min(1, (now - startTime) / duration);
|
|
2892
|
+
const ease = 1 - Math.pow(1 - t, 3);
|
|
2893
|
+
const curRaw = Math.round(startRaw + (rawTarget - startRaw) * ease);
|
|
2894
|
+
const curDiet = Math.round(startDiet + (dietTarget - startDiet) * ease);
|
|
2895
|
+
el.textContent = \`\${curRaw.toLocaleString()} → \${curDiet.toLocaleString()} tok (-\${savedPct}%)\`;
|
|
2896
|
+
if (t < 1) requestAnimationFrame(tick);
|
|
2897
|
+
}
|
|
2898
|
+
requestAnimationFrame(tick);
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
function applyTheme(theme) {
|
|
2902
|
+
currentTheme = theme;
|
|
2903
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
2904
|
+
localStorage.setItem('vicem-theme', theme);
|
|
2905
|
+
const logoImg = document.getElementById('official-logo');
|
|
2906
|
+
logoImg.src = theme === 'light' ? '/logo-light.png' : '/logo-dark.png';
|
|
2907
|
+
document.getElementById('theme-icon').textContent = theme === 'light' ? '☾' : '☀';
|
|
2908
|
+
document.getElementById('theme-label').textContent = theme === 'light' ? 'Dark' : 'Light';
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
function toggleTheme() {
|
|
2912
|
+
applyTheme(currentTheme === 'dark' ? 'light' : 'dark');
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
const urlTheme = new URLSearchParams(window.location.search).get('theme');
|
|
2916
|
+
if (urlTheme === 'light' || urlTheme === 'dark') {
|
|
2917
|
+
applyTheme(urlTheme);
|
|
2918
|
+
} else {
|
|
2919
|
+
applyTheme(currentTheme);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
const taskInput = document.getElementById('task-input');
|
|
2923
|
+
taskInput.addEventListener('focus', () => { isEditingGoal = true; });
|
|
2924
|
+
taskInput.addEventListener('blur', () => { isEditingGoal = false; });
|
|
2925
|
+
|
|
2926
|
+
function setStatus(msg) {
|
|
2927
|
+
const el = document.getElementById('status-line');
|
|
2928
|
+
el.textContent = msg;
|
|
2929
|
+
el.style.color = 'var(--text-primary)';
|
|
2930
|
+
setTimeout(() => {
|
|
2931
|
+
el.style.color = 'var(--text-muted)';
|
|
2932
|
+
}, 3200);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
function previewRoute(targetName, targetId) {
|
|
2936
|
+
const el = document.getElementById('relay-target');
|
|
2937
|
+
const graph = document.querySelector('.relay-graph');
|
|
2938
|
+
if (!el) return;
|
|
2939
|
+
document.querySelectorAll('#agent-rows tr').forEach(r => {
|
|
2940
|
+
r.classList.toggle('route-target-hover', Boolean(targetId && r.dataset.agentId === targetId));
|
|
2941
|
+
});
|
|
2942
|
+
if (targetName) {
|
|
2943
|
+
el.textContent = targetName;
|
|
2944
|
+
el.classList.add('active-route');
|
|
2945
|
+
if (graph) graph.classList.add('route-locked');
|
|
2946
|
+
} else if (!relayingTargetId) {
|
|
2947
|
+
el.textContent = 'Select target';
|
|
2948
|
+
el.classList.remove('active-route');
|
|
2949
|
+
if (graph) graph.classList.remove('route-locked');
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
function buildSegmentedBar(pct, isWorking, isWarn, hasBar) {
|
|
2954
|
+
const total = 20;
|
|
2955
|
+
const filled = hasBar ? Math.round((pct / 100) * total) : 0;
|
|
2956
|
+
let html = '<div class="seg-bar">';
|
|
2957
|
+
for (let i = 0; i < total; i++) {
|
|
2958
|
+
let cls = 'seg';
|
|
2959
|
+
if (i < filled) {
|
|
2960
|
+
cls += ' on';
|
|
2961
|
+
if (isWarn) cls += ' warn';
|
|
2962
|
+
else if (isWorking) cls += ' working';
|
|
2963
|
+
}
|
|
2964
|
+
html += \`<span class="\${cls}" style="--seg-idx:\${i}"></span>\`;
|
|
2965
|
+
}
|
|
2966
|
+
html += '</div>';
|
|
2967
|
+
return html;
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
function toggleLaunchApp() {
|
|
2971
|
+
if (launchMode === 'app') launchMode = 'cli';
|
|
2972
|
+
else if (launchMode === 'cli') launchMode = 'off';
|
|
2973
|
+
else launchMode = 'app';
|
|
2974
|
+
|
|
2975
|
+
const btn = document.getElementById('btn-launch-toggle');
|
|
2976
|
+
const lbl = document.getElementById('launch-label');
|
|
2977
|
+
btn.classList.toggle('active-toggle', launchMode !== 'off');
|
|
2978
|
+
if (launchMode === 'app') {
|
|
2979
|
+
lbl.textContent = 'Launch: Desktop App';
|
|
2980
|
+
setStatus('Mode: Open Desktop GUI App (Claude.app / ChatGPT.app / IDE)');
|
|
2981
|
+
} else if (launchMode === 'cli') {
|
|
2982
|
+
lbl.textContent = 'Launch: Terminal CLI';
|
|
2983
|
+
setStatus('Mode: Open Terminal.app running CLI binary');
|
|
2984
|
+
} else {
|
|
2985
|
+
lbl.textContent = 'Launch: Off (Copy only)';
|
|
2986
|
+
setStatus('Mode: Copy baton + mount rule file only');
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
function toggleInspector() {
|
|
2991
|
+
inspectorOpen = !inspectorOpen;
|
|
2992
|
+
document.getElementById('inspector-drawer').classList.toggle('open', inspectorOpen);
|
|
2993
|
+
document.getElementById('btn-inspect').classList.toggle('active-toggle', inspectorOpen);
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
function toggleAllPlatforms() {
|
|
2997
|
+
showAllPlatforms = !showAllPlatforms;
|
|
2998
|
+
document.getElementById('btn-all-platforms').classList.toggle('active-toggle', showAllPlatforms);
|
|
2999
|
+
document.getElementById('platforms-label').textContent = showAllPlatforms
|
|
3000
|
+
? 'Installed only'
|
|
3001
|
+
: 'All 12 platforms';
|
|
3002
|
+
render();
|
|
3003
|
+
setStatus(
|
|
3004
|
+
showAllPlatforms
|
|
3005
|
+
? 'Showing all 12 supported platforms (macOS / Windows / Linux)'
|
|
3006
|
+
: 'Showing installed & primary platforms'
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
function toggleBatonPreview() {
|
|
3011
|
+
batonPreviewOpen = !batonPreviewOpen;
|
|
3012
|
+
document.getElementById('baton-preview-box').classList.toggle('open', batonPreviewOpen);
|
|
3013
|
+
document.getElementById('btn-preview-label').textContent =
|
|
3014
|
+
batonPreviewOpen ? 'Hide .vicem/BATON.md' : 'View .vicem/BATON.md';
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
function closeReceiptPanel() {
|
|
3018
|
+
receiptDismissed = true;
|
|
3019
|
+
document.getElementById('receipt-panel').classList.remove('visible');
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
async function switchWorkspace(projectRoot) {
|
|
3023
|
+
setStatus('Switching workspace...');
|
|
3024
|
+
const res = await fetch('/api/workspace', {
|
|
3025
|
+
method: 'POST',
|
|
3026
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3027
|
+
body: JSON.stringify({ projectRoot }),
|
|
3028
|
+
});
|
|
3029
|
+
state = await res.json();
|
|
3030
|
+
render();
|
|
3031
|
+
setStatus(\`Switched workspace to \${state.project}\`);
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
async function saveCustomGoal(goal) {
|
|
3035
|
+
const res = await fetch('/api/goal', {
|
|
3036
|
+
method: 'POST',
|
|
3037
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3038
|
+
body: JSON.stringify({ goal }),
|
|
3039
|
+
});
|
|
3040
|
+
state = await res.json();
|
|
3041
|
+
render();
|
|
3042
|
+
setStatus('Updated handoff instruction');
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
function render() {
|
|
3046
|
+
if (!state) return;
|
|
3047
|
+
|
|
3048
|
+
// 1. Workspace Tabs
|
|
3049
|
+
const tabsEl = document.getElementById('ws-tabs');
|
|
3050
|
+
tabsEl.innerHTML = '';
|
|
3051
|
+
for (const ws of state.workspaces.slice(0, 4)) {
|
|
3052
|
+
const btn = document.createElement('button');
|
|
3053
|
+
btn.className = 'ws-tab' + (ws.path === state.projectRoot ? ' active' : '');
|
|
3054
|
+
btn.title = ws.shortPath;
|
|
3055
|
+
btn.onclick = () => switchWorkspace(ws.path);
|
|
3056
|
+
btn.innerHTML = \`<span>\${ws.name}</span>\`;
|
|
3057
|
+
tabsEl.appendChild(btn);
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
document.getElementById('branch-badge').textContent =
|
|
3061
|
+
\`\${state.project} (\${state.branch})\`;
|
|
3062
|
+
|
|
3063
|
+
document.getElementById('mounted-status').textContent =
|
|
3064
|
+
state.mountedFiles.length > 0
|
|
3065
|
+
? \`Mounted: \${state.mountedFiles.join(', ')}\`
|
|
3066
|
+
: 'Live telemetry';
|
|
3067
|
+
|
|
3068
|
+
const workingNode = state.nodes.find(n => n.status === 'Working') || state.nodes[0];
|
|
3069
|
+
document.getElementById('relay-source').textContent = workingNode.name;
|
|
3070
|
+
if (!document.getElementById('relay-target').classList.contains('active-route')) {
|
|
3071
|
+
document.getElementById('relay-target').textContent = 'Select target';
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
if (!isEditingGoal) {
|
|
3075
|
+
taskInput.value = state.taskTitle;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
animateTokenStats(state.stats.rawTokens, state.stats.dietTokens, state.stats.savedPct);
|
|
3079
|
+
document.getElementById('stat-secrets').textContent =
|
|
3080
|
+
\`\${state.stats.secretsMasked} masked\`;
|
|
3081
|
+
|
|
3082
|
+
// Render Live 5-Step Handoff Verification Receipt Panel if present
|
|
3083
|
+
const receiptPanel = document.getElementById('receipt-panel');
|
|
3084
|
+
if (state.lastHandoffReceipt && !receiptDismissed) {
|
|
3085
|
+
const r = state.lastHandoffReceipt;
|
|
3086
|
+
const isNewReceipt = lastRenderedReceiptTs !== r.timestamp;
|
|
3087
|
+
lastRenderedReceiptTs = r.timestamp;
|
|
3088
|
+
receiptPanel.classList.add('visible');
|
|
3089
|
+
document.getElementById('receipt-title').textContent =
|
|
3090
|
+
\`✓ Handoff Verified (\${r.timestamp}) — \${r.sourceAgent} → \${r.targetId} (\${r.project})\`;
|
|
3091
|
+
if (isNewReceipt || !document.getElementById('receipt-steps').children.length) {
|
|
3092
|
+
document.getElementById('receipt-steps').innerHTML = r.steps
|
|
3093
|
+
.map(
|
|
3094
|
+
(s, idx) => \`<div class="step-row\${isNewReceipt ? ' step-cascade' : ''}" style="--step-idx:\${idx}">
|
|
3095
|
+
<span class="step-name"><span class="step-ok">\${s.ok ? '[✓]' : '[!]'}</span> \${s.title}</span>
|
|
3096
|
+
<span class="step-detail">\${s.detail}</span>
|
|
3097
|
+
</div>\`
|
|
3098
|
+
)
|
|
3099
|
+
.join('');
|
|
3100
|
+
}
|
|
3101
|
+
document.getElementById('baton-preview-box').textContent = r.batonPreview || '';
|
|
3102
|
+
} else {
|
|
3103
|
+
receiptPanel.classList.remove('visible');
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// Inspector Drawer
|
|
3107
|
+
document.getElementById('insp-files').innerHTML = (state.modifiedFiles || [])
|
|
3108
|
+
.map(f => \`<span class="insp-tag">\${f}</span>\`)
|
|
3109
|
+
.join('');
|
|
3110
|
+
document.getElementById('insp-mounts').innerHTML = state.mountedFiles.length > 0
|
|
3111
|
+
? state.mountedFiles.map(m => \`<span class="insp-tag">\${m}</span>\`).join('') +
|
|
3112
|
+
\`<button class="cmd-btn" style="height:22px" onclick="unmountRules()">Unmount all</button>\`
|
|
3113
|
+
: '<span style="color:var(--text-muted)">None (clean working tree)</span>';
|
|
3114
|
+
|
|
3115
|
+
// Agent Process Rows — Keyed Non-Destructive DOM Update (preserves hover & transitions)
|
|
3116
|
+
const tbody = document.getElementById('agent-rows');
|
|
3117
|
+
const visibleNodes = showAllPlatforms
|
|
3118
|
+
? state.nodes
|
|
3119
|
+
: state.nodes.filter(n => n.primary || n.installed);
|
|
3120
|
+
|
|
3121
|
+
const existingRows = new Map();
|
|
3122
|
+
Array.from(tbody.querySelectorAll('tr[data-agent-id]')).forEach(tr => {
|
|
3123
|
+
existingRows.set(tr.dataset.agentId, tr);
|
|
3124
|
+
});
|
|
3125
|
+
|
|
3126
|
+
const visibleIds = new Set(visibleNodes.map(n => n.id));
|
|
3127
|
+
for (const [id, tr] of existingRows.entries()) {
|
|
3128
|
+
if (!visibleIds.has(id)) tr.remove();
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
visibleNodes.forEach((node, idx) => {
|
|
3132
|
+
const isWorking = node.status === 'Working';
|
|
3133
|
+
const hasBar = Boolean(node.quota?.hasBar);
|
|
3134
|
+
const pct = node.quota?.remainPct ?? 100;
|
|
3135
|
+
const isWarn = hasBar && pct < 55;
|
|
3136
|
+
const isRelayingThis = relayingTargetId === node.id;
|
|
3137
|
+
|
|
3138
|
+
const actionHtml = isWorking
|
|
3139
|
+
? \`<span class="holding-pill">● Holding</span>\`
|
|
3140
|
+
: isRelayingThis
|
|
3141
|
+
? \`<button class="handoff-btn is-relaying"><span>Relaying</span><span class="arr">···</span></button>\`
|
|
3142
|
+
: \`<button class="handoff-btn" onmouseenter="previewRoute('\${node.name}', '\${node.id}')" onmouseleave="previewRoute(null, null)" onclick="handOffTo('\${node.id}', '\${node.name}')"><span>Hand off</span><span class="arr">→</span></button>\`;
|
|
3143
|
+
|
|
3144
|
+
const rowInnerHtml = \`
|
|
3145
|
+
<td>
|
|
3146
|
+
<div class="agent-cell">
|
|
3147
|
+
<div class="agent-info">
|
|
3148
|
+
<div class="agent-title-row">
|
|
3149
|
+
<span class="agent-name">\${node.name}</span>
|
|
3150
|
+
<span class="model-badge">\${node.model}</span>
|
|
3151
|
+
</div>
|
|
3152
|
+
<span class="agent-path">\${node.workspace}</span>
|
|
3153
|
+
</div>
|
|
3154
|
+
</div>
|
|
3155
|
+
</td>
|
|
3156
|
+
<td class="td-state">
|
|
3157
|
+
<span class="state-badge \${node.status}">
|
|
3158
|
+
<span class="state-sq \${node.status}"></span>
|
|
3159
|
+
<span>\${node.status}</span>
|
|
3160
|
+
</span>
|
|
3161
|
+
</td>
|
|
3162
|
+
<td>
|
|
3163
|
+
<div class="quota-cell">
|
|
3164
|
+
<div class="quota-meta">
|
|
3165
|
+
<span class="quota-val\${hasBar ? '' : ' subtle'}">\${node.quota?.label || 'Ready'}</span>
|
|
3166
|
+
<span class="quota-sub">\${node.quota?.detail || ''}</span>
|
|
3167
|
+
</div>
|
|
3168
|
+
\${buildSegmentedBar(pct, isWorking, isWarn, hasBar)}
|
|
3169
|
+
</div>
|
|
3170
|
+
</td>
|
|
3171
|
+
<td class="seen-cell">\${node.lastSeen}</td>
|
|
3172
|
+
<td>\${actionHtml}</td>
|
|
3173
|
+
\`;
|
|
3174
|
+
|
|
3175
|
+
let tr = existingRows.get(node.id);
|
|
3176
|
+
const rowSig = \`\${node.status}|\${node.model}|\${node.workspace}|\${node.quota?.label}|\${node.quota?.detail}|\${pct}|\${node.lastSeen}|\${isRelayingThis}\`;
|
|
3177
|
+
if (!tr) {
|
|
3178
|
+
tr = document.createElement('tr');
|
|
3179
|
+
tr.dataset.agentId = node.id;
|
|
3180
|
+
tr.dataset.sig = rowSig;
|
|
3181
|
+
tr.style.setProperty('--row-idx', String(idx));
|
|
3182
|
+
tr.className = (isWorking ? 'is-working ' : '') + 'row-enter';
|
|
3183
|
+
tr.innerHTML = rowInnerHtml;
|
|
3184
|
+
tbody.appendChild(tr);
|
|
3185
|
+
setTimeout(() => tr.classList.remove('row-enter'), 550);
|
|
3186
|
+
} else {
|
|
3187
|
+
tr.classList.toggle('is-working', isWorking);
|
|
3188
|
+
if (tr.dataset.sig !== rowSig) {
|
|
3189
|
+
tr.dataset.sig = rowSig;
|
|
3190
|
+
tr.innerHTML = rowInnerHtml;
|
|
3191
|
+
}
|
|
3192
|
+
tbody.appendChild(tr);
|
|
3193
|
+
}
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
async function handOffTo(targetId, targetName) {
|
|
3198
|
+
receiptDismissed = false;
|
|
3199
|
+
relayingTargetId = targetId;
|
|
3200
|
+
previewRoute(targetName, targetId);
|
|
3201
|
+
const graph = document.querySelector('.relay-graph');
|
|
3202
|
+
if (graph) graph.classList.add('relaying-surge');
|
|
3203
|
+
render();
|
|
3204
|
+
setStatus(\`Executing 5-step handoff to \${targetName}...\`);
|
|
3205
|
+
try {
|
|
3206
|
+
const res = await fetch('/api/handoff', {
|
|
3207
|
+
method: 'POST',
|
|
3208
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3209
|
+
body: JSON.stringify({ targetId, launchMode }),
|
|
3210
|
+
});
|
|
3211
|
+
state = await res.json();
|
|
3212
|
+
} finally {
|
|
3213
|
+
relayingTargetId = null;
|
|
3214
|
+
if (graph) graph.classList.remove('relaying-surge');
|
|
3215
|
+
}
|
|
3216
|
+
render();
|
|
3217
|
+
const targetRow = document.querySelector(\`#agent-rows tr[data-agent-id="\${targetId}"]\`);
|
|
3218
|
+
if (targetRow) {
|
|
3219
|
+
targetRow.classList.remove('just-relayed');
|
|
3220
|
+
void targetRow.offsetWidth;
|
|
3221
|
+
targetRow.classList.add('just-relayed');
|
|
3222
|
+
}
|
|
3223
|
+
setStatus(state.actionReceipt || \`Relayed baton to \${targetName}\`);
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
async function unmountRules() {
|
|
3227
|
+
const res = await fetch('/api/unmount', { method: 'POST' });
|
|
3228
|
+
state = await res.json();
|
|
3229
|
+
render();
|
|
3230
|
+
setStatus('Unmounted temporary rule files');
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
async function triggerSelfDiet() {
|
|
3234
|
+
receiptDismissed = false;
|
|
3235
|
+
setStatus('Compacting active session...');
|
|
3236
|
+
const res = await fetch('/api/handoff', {
|
|
3237
|
+
method: 'POST',
|
|
3238
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3239
|
+
body: JSON.stringify({ targetId: state?.workingAgentId || 'antigravity', launchMode: 'off' }),
|
|
3240
|
+
});
|
|
3241
|
+
state = await res.json();
|
|
3242
|
+
render();
|
|
3243
|
+
setStatus(\`Compacted active session (-\${state?.stats?.savedPct || 94.2}%) & copied to clipboard\`);
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
async function triggerBack() {
|
|
3247
|
+
if (!state || !state.lastHandedOffFrom) {
|
|
3248
|
+
setStatus('No previous agent in session history');
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
const prev = state.nodes.find(n => n.id === state.lastHandedOffFrom);
|
|
3252
|
+
await handOffTo(state.lastHandedOffFrom, prev ? prev.name : state.lastHandedOffFrom);
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
async function copyBaton() {
|
|
3256
|
+
await fetch('/api/clip', { method: 'POST' });
|
|
3257
|
+
setStatus('Copied .vicem/BATON.md to macOS clipboard');
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
window.addEventListener('keydown', (e) => {
|
|
3261
|
+
if (isEditingGoal || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
3262
|
+
const k = e.key.toLowerCase();
|
|
3263
|
+
if (k === 'r') { flashKeybindButton('button[onclick="triggerSelfDiet()"]'); triggerSelfDiet(); }
|
|
3264
|
+
else if (k === 'z') { flashKeybindButton('button[onclick="triggerBack()"]'); triggerBack(); }
|
|
3265
|
+
else if (k === 'c') { flashKeybindButton('button[onclick="copyBaton()"]'); copyBaton(); }
|
|
3266
|
+
else if (k === 'i') { flashKeybindButton('#btn-inspect'); toggleInspector(); }
|
|
3267
|
+
else if (k === 'a') { flashKeybindButton('#btn-all-platforms'); toggleAllPlatforms(); }
|
|
3268
|
+
else if (k === 't') { flashKeybindButton('.theme-btn'); toggleTheme(); }
|
|
3269
|
+
});
|
|
3270
|
+
|
|
3271
|
+
async function load(initialData) {
|
|
3272
|
+
try {
|
|
3273
|
+
const nextState = initialData || await (await fetch('/api/state')).json();
|
|
3274
|
+
if (nextState.guiVersion && nextState.guiVersion !== CLIENT_GUI_VERSION) {
|
|
3275
|
+
window.location.reload();
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
if (isDemoMode) {
|
|
3279
|
+
const origProject = nextState.project;
|
|
3280
|
+
const origShort = nextState.shortProject;
|
|
3281
|
+
nextState.project = 'vicem';
|
|
3282
|
+
nextState.branch = 'main';
|
|
3283
|
+
nextState.workspaces = [
|
|
3284
|
+
{ name: 'vicem', path: nextState.projectRoot, shortPath: '~/workspace/vicem' },
|
|
3285
|
+
{ name: 'core-sdk', path: '/ws/core-sdk', shortPath: '~/workspace/core-sdk' },
|
|
3286
|
+
{ name: 'api-gateway', path: '/ws/api-gateway', shortPath: '~/workspace/api-gateway' },
|
|
3287
|
+
{ name: 'web-studio', path: '/ws/web-studio', shortPath: '~/workspace/web-studio' },
|
|
3288
|
+
];
|
|
3289
|
+
nextState.nodes = (nextState.nodes || []).map(n => ({
|
|
3290
|
+
...n,
|
|
3291
|
+
workspace: n.workspace.startsWith('~/') ? '~/workspace/vicem' : n.workspace,
|
|
3292
|
+
}));
|
|
3293
|
+
if (nextState.lastHandoffReceipt) {
|
|
3294
|
+
nextState.lastHandoffReceipt.project = 'vicem';
|
|
3295
|
+
nextState.lastHandoffReceipt.steps = nextState.lastHandoffReceipt.steps.map(s => ({
|
|
3296
|
+
...s,
|
|
3297
|
+
detail: s.detail
|
|
3298
|
+
.replaceAll(origShort, '~/workspace/vicem')
|
|
3299
|
+
.replaceAll(origProject, 'vicem')
|
|
3300
|
+
.replaceAll('(master)', '(main)'),
|
|
3301
|
+
}));
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
state = nextState;
|
|
3305
|
+
render();
|
|
3306
|
+
} catch {}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
load(window.__INITIAL_STATE__ || null);
|
|
3310
|
+
if (!isStaticMode) setInterval(() => load(null), 3000);
|
|
3311
|
+
</script>
|
|
3312
|
+
</body>
|
|
3313
|
+
</html>`;
|
|
3314
|
+
|
|
3315
|
+
const PORT = Number(process.env.VICEM_GUI_PORT || 4310);
|
|
3316
|
+
|
|
3317
|
+
const server = http.createServer(async (req, res) => {
|
|
3318
|
+
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
|
|
3319
|
+
|
|
3320
|
+
if (url.pathname === '/') {
|
|
3321
|
+
const initData = await collectState();
|
|
3322
|
+
const safeJson = JSON.stringify(initData).replace(/</g, '\\u003c');
|
|
3323
|
+
const hydratedHtml = HTML_PAGE.replace(
|
|
3324
|
+
'<script>',
|
|
3325
|
+
`<script>window.__INITIAL_STATE__ = ${safeJson};\n`
|
|
3326
|
+
);
|
|
3327
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
3328
|
+
res.end(hydratedHtml);
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
if (url.pathname === '/logo-dark.png' && fs.existsSync(LOGO_DARK_PATH)) {
|
|
3333
|
+
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' });
|
|
3334
|
+
fs.createReadStream(LOGO_DARK_PATH).pipe(res);
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
if (url.pathname === '/logo-light.png' && fs.existsSync(LOGO_LIGHT_PATH)) {
|
|
3339
|
+
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' });
|
|
3340
|
+
fs.createReadStream(LOGO_LIGHT_PATH).pipe(res);
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
if (url.pathname === '/api/state') {
|
|
3345
|
+
const data = await collectState();
|
|
3346
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3347
|
+
res.end(JSON.stringify(data));
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
if (url.pathname === '/api/workspace' && req.method === 'POST') {
|
|
3352
|
+
let body = '';
|
|
3353
|
+
req.on('data', (c) => (body += c));
|
|
3354
|
+
req.on('end', async () => {
|
|
3355
|
+
try {
|
|
3356
|
+
const { projectRoot } = JSON.parse(body || '{}');
|
|
3357
|
+
if (projectRoot && fs.existsSync(projectRoot)) {
|
|
3358
|
+
selectedProjectRoot = projectRoot;
|
|
3359
|
+
customGoalOverride = '';
|
|
3360
|
+
}
|
|
3361
|
+
const updated = await collectState();
|
|
3362
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3363
|
+
res.end(JSON.stringify(updated));
|
|
3364
|
+
} catch (e) {
|
|
3365
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3366
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
3367
|
+
}
|
|
3368
|
+
});
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
if (url.pathname === '/api/goal' && req.method === 'POST') {
|
|
3373
|
+
let body = '';
|
|
3374
|
+
req.on('data', (c) => (body += c));
|
|
3375
|
+
req.on('end', async () => {
|
|
3376
|
+
try {
|
|
3377
|
+
const { goal } = JSON.parse(body || '{}');
|
|
3378
|
+
customGoalOverride = String(goal || '').trim();
|
|
3379
|
+
const updated = await collectState();
|
|
3380
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3381
|
+
res.end(JSON.stringify(updated));
|
|
3382
|
+
} catch (e) {
|
|
3383
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3384
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
3385
|
+
}
|
|
3386
|
+
});
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
if (url.pathname === '/api/handoff' && req.method === 'POST') {
|
|
3391
|
+
let body = '';
|
|
3392
|
+
req.on('data', (c) => (body += c));
|
|
3393
|
+
req.on('end', async () => {
|
|
3394
|
+
try {
|
|
3395
|
+
const { targetId, launchMode } = JSON.parse(body || '{}');
|
|
3396
|
+
const updated = await executeRealHandoff(targetId || 'codex', launchMode || 'app');
|
|
3397
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3398
|
+
res.end(JSON.stringify(updated));
|
|
3399
|
+
} catch (e) {
|
|
3400
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3401
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
3402
|
+
}
|
|
3403
|
+
});
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
if (url.pathname === '/api/unmount' && req.method === 'POST') {
|
|
3408
|
+
try {
|
|
3409
|
+
RuleMountAdapter.unmount(selectedProjectRoot);
|
|
3410
|
+
lastHandoffReceipt = null;
|
|
3411
|
+
lastHandedOffTo = null;
|
|
3412
|
+
const updated = await collectState();
|
|
3413
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3414
|
+
res.end(JSON.stringify(updated));
|
|
3415
|
+
} catch (e) {
|
|
3416
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3417
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
3418
|
+
}
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
if (url.pathname === '/api/clip' && req.method === 'POST') {
|
|
3423
|
+
await VicemApp.run(['clip'], { projectRoot: selectedProjectRoot, skipLaunch: true });
|
|
3424
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3425
|
+
res.end(JSON.stringify({ ok: true }));
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
res.writeHead(404);
|
|
3430
|
+
res.end('Not found');
|
|
3431
|
+
});
|
|
3432
|
+
|
|
3433
|
+
server.on('error', (err) => {
|
|
3434
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
3435
|
+
if (err && err.code === 'EADDRINUSE') {
|
|
3436
|
+
console.log(`[Vicem Utility GUI] Already running at ${url}`);
|
|
3437
|
+
if (process.env.VICEM_AUTO_OPEN === '1') {
|
|
3438
|
+
openExternalCrossPlatform(url);
|
|
3439
|
+
}
|
|
3440
|
+
process.exit(0);
|
|
3441
|
+
}
|
|
3442
|
+
console.error('[Vicem Utility GUI] Server error:', err);
|
|
3443
|
+
process.exit(1);
|
|
3444
|
+
});
|
|
3445
|
+
|
|
3446
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
3447
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
3448
|
+
console.log(`[Vicem Utility GUI] Running at ${url}`);
|
|
3449
|
+
if (process.env.VICEM_AUTO_OPEN === '1') {
|
|
3450
|
+
openExternalCrossPlatform(url);
|
|
3451
|
+
}
|
|
3452
|
+
});
|