clideck 1.26.3 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/agent-presets.json +5 -1
- package/bin/notify-helper.js +18 -0
- package/handlers.js +85 -17
- package/package.json +1 -1
- package/public/js/app.js +17 -5
- package/public/js/creator.js +33 -10
- package/public/js/settings.js +31 -41
- package/public/js/terminals.js +8 -5
- package/public/tailwind.css +1 -1
- package/server.js +69 -1
- package/sessions.js +14 -13
- package/telemetry-receiver.js +52 -14
- package/transcript.js +5 -3
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ npx clideck
|
|
|
26
26
|
|
|
27
27
|
Open [http://localhost:4000](http://localhost:4000). Click **+**, pick an agent and optionally a project and role, start working.
|
|
28
28
|
|
|
29
|
-
New users get 3 built-in
|
|
29
|
+
New users get 3 built-in roles (Programmer, Reviewer, Product Manager) and 3 starter prompts in the prompt library.
|
|
30
30
|
|
|
31
31
|
Or install globally:
|
|
32
32
|
|
|
@@ -38,7 +38,7 @@ clideck
|
|
|
38
38
|
## What You Get
|
|
39
39
|
|
|
40
40
|
- **Roles** — define reusable agent identities (Programmer, Reviewer, PM) and assign them when creating sessions. Instructions are injected into the agent automatically.
|
|
41
|
-
- **Autopilot** — project-level workflow routing. Watches your role-assigned agents, waits for them to finish, forwards output to the next specialist. Fingerprints each output, tracks handoff history, and guards against repeat loops.
|
|
41
|
+
- **Autopilot** — project-level workflow routing. Watches your role-assigned agents, waits for them to finish, forwards output to the next specialist. Fingerprints each output, tracks handoff history, and guards against repeat loops. Supports 8 LLM providers (Anthropic, OpenAI, Google, Groq, xAI, Mistral, OpenRouter, Cerebras). Notifies you when work is complete or blocked.
|
|
42
42
|
- **Mobile access** — check on your agents from your phone with a QR scan. E2E encrypted.
|
|
43
43
|
- **Live working/idle status** — see which agent is thinking and which is waiting for you, without checking each terminal
|
|
44
44
|
- **Session resume** — close clideck, reopen it tomorrow, pick up where you left off
|
package/agent-presets.json
CHANGED
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
"resumeCommand": "claude --resume {{sessionId}}",
|
|
11
11
|
"sessionIdPattern": "Session ID:\\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
|
|
12
12
|
"outputMarker": "\u23fa",
|
|
13
|
-
"telemetryConfigPath": "
|
|
13
|
+
"telemetryConfigPath": "~/.claude/settings.json",
|
|
14
|
+
"telemetrySetup": "Required for working/idle status, Autopilot, notifications, and mobile remote.\n\nCliDeck will add start/stop hooks to ~/.claude/settings.json. Claude will ask for one-time approval on next launch.",
|
|
15
|
+
"telemetryAutoSetup": {
|
|
16
|
+
"label": "Patch Claude"
|
|
17
|
+
},
|
|
14
18
|
"telemetryEnv": {
|
|
15
19
|
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
|
|
16
20
|
"OTEL_LOGS_EXPORTER": "otlp",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tiny helper for Codex notify hook.
|
|
3
|
+
// Usage: node notify-helper.js <port> <json-payload>
|
|
4
|
+
// Codex appends the JSON payload as the last argv argument.
|
|
5
|
+
// Port is passed as the first argument by the notify config.
|
|
6
|
+
|
|
7
|
+
const port = parseInt(process.argv[2], 10);
|
|
8
|
+
const payload = process.argv[process.argv.length - 1];
|
|
9
|
+
if (!port || !payload || payload === String(port)) process.exit(0);
|
|
10
|
+
|
|
11
|
+
const http = require('http');
|
|
12
|
+
const req = http.request({
|
|
13
|
+
hostname: 'localhost', port, path: '/hook/codex/stop',
|
|
14
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
timeout: 2000,
|
|
16
|
+
});
|
|
17
|
+
req.on('error', () => {});
|
|
18
|
+
req.end(payload);
|
package/handlers.js
CHANGED
|
@@ -77,11 +77,18 @@ function detectTelemetryConfig(c) {
|
|
|
77
77
|
if (!preset) continue;
|
|
78
78
|
let detected = false;
|
|
79
79
|
if (preset.presetId === 'claude-code') {
|
|
80
|
-
|
|
80
|
+
try {
|
|
81
|
+
const s = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'));
|
|
82
|
+
const hooks = s.hooks || {};
|
|
83
|
+
const has = (arr, path) => arr?.some(h => h.hooks?.some(x => x.url?.includes('/hook/claude/' + path)));
|
|
84
|
+
detected = has(hooks.UserPromptSubmit, 'start') && has(hooks.Stop, 'stop') && has(hooks.StopFailure, 'stop')
|
|
85
|
+
&& has(hooks.PreToolUse, 'menu')
|
|
86
|
+
&& hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.url?.includes('/hook/claude/idle')));
|
|
87
|
+
} catch {}
|
|
81
88
|
} else if (preset.presetId === 'codex') {
|
|
82
89
|
try {
|
|
83
90
|
const content = readFileSync(join(home, '.codex', 'config.toml'), 'utf8');
|
|
84
|
-
detected = content.includes('[otel]') && content.includes(`localhost:${port}`);
|
|
91
|
+
detected = content.includes('[otel]') && content.includes(`localhost:${port}`) && content.includes('notify-helper');
|
|
85
92
|
} catch {}
|
|
86
93
|
} else if (preset.presetId === 'gemini-cli') {
|
|
87
94
|
try {
|
|
@@ -139,7 +146,17 @@ function onConnection(ws) {
|
|
|
139
146
|
sessions.broadcast({ type: 'screen.updated', id: msg.id });
|
|
140
147
|
const sess = sessions.getSessions().get(msg.id);
|
|
141
148
|
if (sess) {
|
|
142
|
-
|
|
149
|
+
let choices = require('./transcript').detectMenu(msg.lines, sess.presetId);
|
|
150
|
+
// Codex: only trust menu detection if last OTEL event was response.completed
|
|
151
|
+
if (choices && sess.presetId === 'codex') {
|
|
152
|
+
const last = require('./telemetry-receiver').getLastEvent(msg.id);
|
|
153
|
+
if (!last.startsWith('codex.sse_event:response.completed')) {
|
|
154
|
+
console.log(`[codex] menu rejected — lastEvent=${last} session=${msg.id.slice(0,8)}`);
|
|
155
|
+
choices = null;
|
|
156
|
+
} else {
|
|
157
|
+
console.log(`[codex] menu accepted session=${msg.id.slice(0,8)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
143
160
|
// Auto-approve: send Enter immediately when menu detected
|
|
144
161
|
if (choices && plugins.shouldAutoApproveMenu(msg.id)) {
|
|
145
162
|
sessions.input({ id: msg.id, data: '\r' });
|
|
@@ -150,6 +167,7 @@ function onConnection(ws) {
|
|
|
150
167
|
sessions.broadcast({ type: 'session.menu', id: msg.id, choices: choices || [] });
|
|
151
168
|
if (choices) {
|
|
152
169
|
plugins.notifyMenu(msg.id, choices);
|
|
170
|
+
if (sess.presetId === 'codex') require('./telemetry-receiver').cancelCodexMenuPoll(msg.id);
|
|
153
171
|
sessions.broadcast({ type: 'session.status', id: msg.id, working: false, source: 'menu' });
|
|
154
172
|
}
|
|
155
173
|
}
|
|
@@ -187,19 +205,13 @@ function onConnection(ws) {
|
|
|
187
205
|
case 'telemetry.autosetup': {
|
|
188
206
|
const preset = presets.find(p => p.presetId === msg.presetId);
|
|
189
207
|
if (!preset?.telemetryAutoSetup) break;
|
|
190
|
-
// Only allow if caller has a live session using this preset's command
|
|
191
|
-
const liveSessions = sessions.list();
|
|
192
|
-
const hasLive = liveSessions.some(s => {
|
|
193
|
-
const cmd = cfg.commands.find(c => c.id === s.commandId);
|
|
194
|
-
return cmd && binName(cmd.command) === binName(preset.command);
|
|
195
|
-
});
|
|
196
|
-
if (!hasLive) break;
|
|
197
208
|
const result = applyTelemetryConfig(preset);
|
|
198
|
-
// Persist telemetry state in config
|
|
199
209
|
for (const cmd of cfg.commands) {
|
|
200
210
|
if (binName(cmd.command) === binName(preset.command)) {
|
|
201
211
|
cmd.telemetryEnabled = result.success;
|
|
202
212
|
cmd.telemetryStatus = result.success ? { ok: true } : { ok: false, error: result.message };
|
|
213
|
+
// Enable the agent when setup succeeds, disable if it fails
|
|
214
|
+
if (result.success) cmd.enabled = true;
|
|
203
215
|
}
|
|
204
216
|
}
|
|
205
217
|
config.save(cfg);
|
|
@@ -378,15 +390,54 @@ function applyTelemetryConfig(preset) {
|
|
|
378
390
|
const home = os.homedir();
|
|
379
391
|
|
|
380
392
|
try {
|
|
393
|
+
if (preset.presetId === 'claude-code') {
|
|
394
|
+
const configPath = join(home, '.claude', 'settings.json');
|
|
395
|
+
let settings = {};
|
|
396
|
+
if (existsSync(configPath)) {
|
|
397
|
+
try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
398
|
+
}
|
|
399
|
+
const hooks = settings.hooks || {};
|
|
400
|
+
const endpoint = `http://localhost:${port}/hook/claude`;
|
|
401
|
+
const clideckHook = (url) => ({ hooks: [{ type: 'http', url }] });
|
|
402
|
+
const hasClideck = (arr, path) => arr?.some(h => h.hooks?.some(x => x.url?.includes('/hook/claude/' + path)));
|
|
403
|
+
if (hasClideck(hooks.UserPromptSubmit, 'start') && hasClideck(hooks.Stop, 'stop') && hasClideck(hooks.StopFailure, 'stop') && hasClideck(hooks.PreToolUse, 'menu') && hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.url?.includes('/hook/claude/idle')))) {
|
|
404
|
+
return { success: true, message: 'Already configured' };
|
|
405
|
+
}
|
|
406
|
+
if (!hasClideck(hooks.UserPromptSubmit, 'start')) hooks.UserPromptSubmit = [...(hooks.UserPromptSubmit || []), clideckHook(`${endpoint}/start`)];
|
|
407
|
+
if (!hasClideck(hooks.Stop, 'stop')) hooks.Stop = [...(hooks.Stop || []), clideckHook(`${endpoint}/stop`)];
|
|
408
|
+
if (!hasClideck(hooks.StopFailure, 'stop')) hooks.StopFailure = [...(hooks.StopFailure || []), clideckHook(`${endpoint}/stop`)];
|
|
409
|
+
if (!hasClideck(hooks.Notification, 'idle')) hooks.Notification = [...(hooks.Notification || []), { matcher: 'idle_prompt', ...clideckHook(`${endpoint}/idle`) }];
|
|
410
|
+
if (!hasClideck(hooks.PreToolUse, 'menu')) hooks.PreToolUse = [...(hooks.PreToolUse || []), clideckHook(`${endpoint}/menu`)];
|
|
411
|
+
settings.hooks = hooks;
|
|
412
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
413
|
+
writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
|
|
414
|
+
return { success: true, message: 'Added hooks to ~/.claude/settings.json — Claude will ask for one-time approval' };
|
|
415
|
+
}
|
|
416
|
+
|
|
381
417
|
if (preset.presetId === 'codex') {
|
|
382
418
|
const configPath = join(home, '.codex', 'config.toml');
|
|
383
419
|
let content = '';
|
|
384
420
|
if (existsSync(configPath)) content = readFileSync(configPath, 'utf8');
|
|
385
|
-
|
|
386
|
-
const
|
|
421
|
+
const hasOtel = content.includes('[otel]');
|
|
422
|
+
const hasNotify = content.includes('notify-helper');
|
|
423
|
+
if (hasOtel && hasNotify) return { success: true, message: 'Already configured' };
|
|
424
|
+
if (!hasNotify) {
|
|
425
|
+
const helperPath = join(__dirname, 'bin', 'notify-helper.js').replace(/\\/g, '/');
|
|
426
|
+
const notifyLine = `notify = ["${process.execPath.replace(/\\/g, '/')}", "${helperPath}", "${port}"]\n`;
|
|
427
|
+
// Insert before the first [section] so it stays top-level
|
|
428
|
+
const firstSection = content.search(/^\[/m);
|
|
429
|
+
if (firstSection >= 0) {
|
|
430
|
+
content = content.slice(0, firstSection) + notifyLine + '\n' + content.slice(firstSection);
|
|
431
|
+
} else {
|
|
432
|
+
content = content + '\n' + notifyLine;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!hasOtel) {
|
|
436
|
+
content = content.trimEnd() + `\n\n[otel]\nexporter = { otlp-http = { endpoint = "http://localhost:${port}/v1/logs", protocol = "json" } }\n`;
|
|
437
|
+
}
|
|
387
438
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
388
|
-
writeFileSync(configPath, content
|
|
389
|
-
return { success: true, message: 'Added
|
|
439
|
+
writeFileSync(configPath, content);
|
|
440
|
+
return { success: true, message: 'Added otel + notify to ~/.codex/config.toml' };
|
|
390
441
|
}
|
|
391
442
|
|
|
392
443
|
if (preset.presetId === 'gemini-cli') {
|
|
@@ -431,14 +482,31 @@ function removeTelemetryConfig(preset) {
|
|
|
431
482
|
const home = os.homedir();
|
|
432
483
|
|
|
433
484
|
try {
|
|
485
|
+
if (preset.presetId === 'claude-code') {
|
|
486
|
+
const configPath = join(home, '.claude', 'settings.json');
|
|
487
|
+
if (!existsSync(configPath)) return { success: true, message: 'No config file to clean' };
|
|
488
|
+
let settings = {};
|
|
489
|
+
try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
490
|
+
if (!settings.hooks) return { success: true, message: 'No hooks to remove' };
|
|
491
|
+
for (const event of ['UserPromptSubmit', 'Stop', 'StopFailure', 'Notification', 'PreToolUse']) {
|
|
492
|
+
const arr = settings.hooks[event];
|
|
493
|
+
if (!arr) continue;
|
|
494
|
+
settings.hooks[event] = arr.filter(h => !h.hooks?.some(x => x.url?.includes('/hook/claude/')));
|
|
495
|
+
if (!settings.hooks[event].length) delete settings.hooks[event];
|
|
496
|
+
}
|
|
497
|
+
if (!Object.keys(settings.hooks).length) delete settings.hooks;
|
|
498
|
+
writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
|
|
499
|
+
return { success: true, message: 'Removed CliDeck hooks from ~/.claude/settings.json' };
|
|
500
|
+
}
|
|
501
|
+
|
|
434
502
|
if (preset.presetId === 'codex') {
|
|
435
503
|
const configPath = join(home, '.codex', 'config.toml');
|
|
436
504
|
if (!existsSync(configPath)) return { success: true, message: 'No config file to clean' };
|
|
437
505
|
let content = readFileSync(configPath, 'utf8');
|
|
438
|
-
// Remove [otel] section and everything until the next section or EOF
|
|
439
506
|
content = content.replace(/\n?\[otel\][^\[]*/, '');
|
|
507
|
+
content = content.replace(/\n?notify\s*=\s*\[.*?notify-helper.*?\]\s*/g, '');
|
|
440
508
|
writeFileSync(configPath, content.trimEnd() + '\n');
|
|
441
|
-
return { success: true, message: 'Removed
|
|
509
|
+
return { success: true, message: 'Removed otel + notify from ~/.codex/config.toml' };
|
|
442
510
|
}
|
|
443
511
|
|
|
444
512
|
if (preset.presetId === 'gemini-cli') {
|
package/package.json
CHANGED
package/public/js/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { state, send } from './state.js';
|
|
2
|
-
import { esc, binName } from './utils.js';
|
|
2
|
+
import { esc, binName, resolveIconPath } from './utils.js';
|
|
3
3
|
import { addTerminal, removeTerminal, select, startRename, startProjectRename, setSessionTheme, openMenu, closeMenu, setStatus, updateMuteIndicator, updatePreview, markUnread, applyFilter, setTab, renderResumable, regroupSessions, toggleProjectCollapse, setSessionProject, estimateSize, restartComplete, positionMenu, addPill, updatePill, removePill, appendPillLog, setPillLogs, closePillLog } from './terminals.js';
|
|
4
4
|
import { renderSettings, updateVersionFooter } from './settings.js';
|
|
5
5
|
import { openCreator, closeCreator, refreshCreator } from './creator.js';
|
|
@@ -39,6 +39,7 @@ function connect() {
|
|
|
39
39
|
renderSettings();
|
|
40
40
|
renderPrompts();
|
|
41
41
|
renderRoles();
|
|
42
|
+
refreshCreator();
|
|
42
43
|
for (const [, entry] of state.terms) applyTheme(entry.term, entry.themeId);
|
|
43
44
|
break;
|
|
44
45
|
case 'themes':
|
|
@@ -82,6 +83,17 @@ function connect() {
|
|
|
82
83
|
case 'session.status':
|
|
83
84
|
setStatus(msg.id, msg.working);
|
|
84
85
|
break;
|
|
86
|
+
// Server requests screen capture (e.g. after PermissionRequest hook)
|
|
87
|
+
case 'screen.capture': {
|
|
88
|
+
const ce = state.terms.get(msg.id);
|
|
89
|
+
if (ce?.term) {
|
|
90
|
+
const buf = ce.term.buffer.active;
|
|
91
|
+
const lines = [];
|
|
92
|
+
for (let i = 0; i < buf.length; i++) { const line = buf.getLine(i); if (line) lines.push(line.translateToString(true)); }
|
|
93
|
+
send({ type: 'terminal.buffer', id: msg.id, lines });
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
85
97
|
// Bridge preview text (OpenCode plugin)
|
|
86
98
|
case 'session.preview': {
|
|
87
99
|
const pe = state.terms.get(msg.id);
|
|
@@ -182,16 +194,15 @@ function connect() {
|
|
|
182
194
|
const actionsEl = toast.querySelector('.setup-actions');
|
|
183
195
|
if (msg.success) {
|
|
184
196
|
const sid = toast.dataset.sessionId;
|
|
185
|
-
const cmdId = toast.dataset.commandId;
|
|
186
197
|
actionsEl.innerHTML = `
|
|
187
198
|
<div class="flex-1 flex items-center gap-1.5 text-xs text-emerald-400">
|
|
188
199
|
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path d="M5 13l4 4L19 7"/></svg>
|
|
189
200
|
Configured
|
|
190
201
|
</div>
|
|
191
|
-
|
|
202
|
+
${sid ? `<button class="restart-btn px-3 py-2 text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors">Restart Session</button>` : ''}
|
|
192
203
|
<button class="dismiss-btn px-3 py-2 text-xs text-slate-500 hover:text-slate-300 transition-colors">Dismiss</button>`;
|
|
193
204
|
actionsEl.querySelector('.dismiss-btn').onclick = () => toast.remove();
|
|
194
|
-
actionsEl.querySelector('.restart-btn').onclick = () => {
|
|
205
|
+
if (sid) actionsEl.querySelector('.restart-btn').onclick = () => {
|
|
195
206
|
const entry = state.terms.get(sid);
|
|
196
207
|
send({ type: 'session.restart', id: sid, themeId: entry?.themeId, cols: entry?.term?.cols, rows: entry?.term?.rows });
|
|
197
208
|
toast.remove();
|
|
@@ -387,6 +398,7 @@ document.querySelectorAll('.filter-tab').forEach(btn => {
|
|
|
387
398
|
|
|
388
399
|
// Telemetry setup notification — shown once per agent type
|
|
389
400
|
const shownSetup = new Set();
|
|
401
|
+
document.addEventListener('clideck:setup', (e) => showTelemetrySetup(e.detail.commandId, null));
|
|
390
402
|
function showTelemetrySetup(commandId, sessionId) {
|
|
391
403
|
const cmd = state.cfg.commands.find(c => c.id === commandId);
|
|
392
404
|
if (!cmd) return;
|
|
@@ -403,7 +415,7 @@ function showTelemetrySetup(commandId, sessionId) {
|
|
|
403
415
|
const [desc, ...codeParts] = setupText.split('\n\n');
|
|
404
416
|
const code = codeParts.join('\n\n');
|
|
405
417
|
const auto = preset.telemetryAutoSetup;
|
|
406
|
-
const iconSrc = preset.icon?.startsWith('/') ? preset.icon : null;
|
|
418
|
+
const iconSrc = preset.icon?.startsWith('/') ? resolveIconPath(preset.icon) : null;
|
|
407
419
|
const title = preset.bridge ? 'Bridge Plugin' : 'Status Tracking';
|
|
408
420
|
|
|
409
421
|
const toast = document.createElement('div');
|
package/public/js/creator.js
CHANGED
|
@@ -35,25 +35,35 @@ function isPresetMissing(p) {
|
|
|
35
35
|
return cmd.command === p.command;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// True if preset binary exists but telemetry/hooks are not configured yet
|
|
39
|
+
function isPresetUnpatched(p) {
|
|
40
|
+
if (p.available === false || !p.telemetryAutoSetup) return false;
|
|
41
|
+
const cmd = findCommandForPreset(p);
|
|
42
|
+
return !cmd || !cmd.telemetryEnabled;
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
function renderPresetButtons() {
|
|
39
46
|
return sortedPresets().map(p => {
|
|
40
|
-
|
|
41
|
-
if (missing) {
|
|
47
|
+
if (isPresetMissing(p)) {
|
|
42
48
|
return `
|
|
43
49
|
<div class="w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-sm text-left text-slate-500">
|
|
44
50
|
<span class="opacity-40">${agentIcon(p.icon, 24)}</span>
|
|
45
|
-
<span class="flex-1 min-w-0">
|
|
46
|
-
<span>${esc(p.name)}</span>
|
|
47
|
-
</span>
|
|
51
|
+
<span class="flex-1 min-w-0">${esc(p.name)}</span>
|
|
48
52
|
<button class="install-btn px-2.5 py-1 text-[11px] font-medium text-blue-400 hover:text-blue-300 bg-blue-500/10 hover:bg-blue-500/20 rounded-md transition-colors" data-preset="${p.presetId}">Add</button>
|
|
49
53
|
</div>`;
|
|
50
54
|
}
|
|
55
|
+
if (isPresetUnpatched(p)) {
|
|
56
|
+
return `
|
|
57
|
+
<div class="w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-sm text-left text-slate-500">
|
|
58
|
+
<span class="opacity-40">${agentIcon(p.icon, 24)}</span>
|
|
59
|
+
<span class="flex-1 min-w-0">${esc(p.name)}</span>
|
|
60
|
+
<button class="setup-btn px-2.5 py-1 text-[11px] font-medium text-amber-400 hover:text-amber-300 bg-amber-500/10 hover:bg-amber-500/20 rounded-md transition-colors" data-preset="${p.presetId}">Setup</button>
|
|
61
|
+
</div>`;
|
|
62
|
+
}
|
|
51
63
|
return `
|
|
52
64
|
<button class="preset-btn w-full flex items-center gap-2.5 px-3 py-2 rounded-md hover:bg-slate-700/70 text-sm transition-colors text-left text-slate-300" data-preset="${p.presetId}">
|
|
53
65
|
<span>${agentIcon(p.icon, 24)}</span>
|
|
54
|
-
<span class="flex-1 min-w-0">
|
|
55
|
-
<span>${esc(p.name)}</span>
|
|
56
|
-
</span>
|
|
66
|
+
<span class="flex-1 min-w-0">${esc(p.name)}</span>
|
|
57
67
|
</button>`;
|
|
58
68
|
}).join('');
|
|
59
69
|
}
|
|
@@ -92,8 +102,8 @@ function createFromPreset(preset, sessionName, cwd, projectId, roleId) {
|
|
|
92
102
|
resumeCommand: preset.resumeCommand,
|
|
93
103
|
sessionIdPattern: preset.sessionIdPattern,
|
|
94
104
|
outputMarker: preset.outputMarker || null,
|
|
95
|
-
telemetryEnabled:
|
|
96
|
-
telemetryStatus:
|
|
105
|
+
telemetryEnabled: false,
|
|
106
|
+
telemetryStatus: null,
|
|
97
107
|
bridge: preset.bridge,
|
|
98
108
|
};
|
|
99
109
|
state.cfg.commands.push(cmd);
|
|
@@ -286,6 +296,19 @@ export function openCreator() {
|
|
|
286
296
|
if (preset?.installCmd) showInstallToast(preset);
|
|
287
297
|
return;
|
|
288
298
|
}
|
|
299
|
+
const setupBtn = e.target.closest('.setup-btn');
|
|
300
|
+
if (setupBtn) {
|
|
301
|
+
const preset = state.presets.find(p => p.presetId === setupBtn.dataset.preset);
|
|
302
|
+
if (!preset) return;
|
|
303
|
+
let cmd = findCommandForPreset(preset);
|
|
304
|
+
if (!cmd) {
|
|
305
|
+
cmd = { id: crypto.randomUUID(), presetId: preset.presetId, label: preset.name, icon: preset.icon, command: preset.command, enabled: true, defaultPath: '', isAgent: preset.isAgent, canResume: preset.canResume, resumeCommand: preset.resumeCommand, sessionIdPattern: preset.sessionIdPattern, outputMarker: preset.outputMarker || null, telemetryEnabled: false, telemetryStatus: null, bridge: preset.bridge };
|
|
306
|
+
state.cfg.commands.push(cmd);
|
|
307
|
+
send({ type: 'config.update', config: state.cfg });
|
|
308
|
+
}
|
|
309
|
+
document.dispatchEvent(new CustomEvent('clideck:setup', { detail: { commandId: cmd.id } }));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
289
312
|
const btn = e.target.closest('.preset-btn');
|
|
290
313
|
if (!btn) return;
|
|
291
314
|
const preset = state.presets.find(p => p.presetId === btn.dataset.preset);
|
package/public/js/settings.js
CHANGED
|
@@ -110,40 +110,14 @@ function telemetryPreset(cmd) {
|
|
|
110
110
|
function integrationSection(c) {
|
|
111
111
|
const preset = telemetryPreset(c);
|
|
112
112
|
if (!preset) return '';
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const title = 'CliDeck integration';
|
|
119
|
-
const subtitle = '(live status & resume)';
|
|
120
|
-
|
|
121
|
-
if (isClaude) {
|
|
122
|
-
return `
|
|
123
|
-
<div class="mt-3 pt-3 border-t border-slate-700/50">
|
|
124
|
-
<div class="flex items-center gap-2 text-sm text-slate-300">
|
|
125
|
-
<span style="width:8px;height:8px;border-radius:50%;background:#34d399;display:inline-block"></span>
|
|
126
|
-
${title} <span class="text-xs text-slate-500">${subtitle}</span>
|
|
127
|
-
</div>
|
|
128
|
-
<div class="mt-1 text-[11px] text-slate-500">Built-in</div>
|
|
129
|
-
</div>`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const detail = isBridge ? 'Bridge plugin' : esc(preset.telemetryConfigPath || '');
|
|
133
|
-
const toggleBg = enabled ? '#3b82f6' : '#475569';
|
|
134
|
-
const knobX = enabled ? '18' : '2';
|
|
135
|
-
|
|
113
|
+
if (!preset.telemetryAutoSetup && !preset.bridge) return '';
|
|
114
|
+
const configured = !!c.telemetryEnabled;
|
|
115
|
+
const detail = configured
|
|
116
|
+
? `<span class="text-emerald-400/80">Configured</span> — ${esc(preset.telemetryConfigPath || '')}`
|
|
117
|
+
: `<span class="text-slate-500">Not configured</span> — enable agent to set up`;
|
|
136
118
|
return `
|
|
137
119
|
<div class="mt-3 pt-3 border-t border-slate-700/50">
|
|
138
|
-
<
|
|
139
|
-
<span>${title} <span class="text-xs text-slate-500">${subtitle}</span></span>
|
|
140
|
-
<span style="position:relative;display:inline-block;width:36px;height:20px">
|
|
141
|
-
<input type="checkbox" ${enabled ? 'checked' : ''} class="agent-telemetry-toggle" data-preset="${esc(preset.presetId)}" style="position:absolute;opacity:0;width:100%;height:100%;cursor:pointer;margin:0;z-index:1">
|
|
142
|
-
<span style="position:absolute;inset:0;border-radius:10px;background:${toggleBg};transition:background .2s"></span>
|
|
143
|
-
<span style="position:absolute;top:2px;left:${knobX}px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left .2s"></span>
|
|
144
|
-
</span>
|
|
145
|
-
</label>
|
|
146
|
-
<div class="mt-1 text-[11px] text-slate-500" ${!isBridge ? 'style="font-family:monospace"' : ''}>${detail}</div>
|
|
120
|
+
<div class="text-[11px] text-slate-500">${detail}</div>
|
|
147
121
|
</div>`;
|
|
148
122
|
}
|
|
149
123
|
|
|
@@ -241,8 +215,8 @@ function openPresetMenu(anchorEl) {
|
|
|
241
215
|
enabled: true, defaultPath: '', isAgent: p.isAgent, canResume: p.canResume,
|
|
242
216
|
resumeCommand: p.resumeCommand, sessionIdPattern: p.sessionIdPattern,
|
|
243
217
|
outputMarker: p.outputMarker || null,
|
|
244
|
-
telemetryEnabled:
|
|
245
|
-
telemetryStatus:
|
|
218
|
+
telemetryEnabled: false,
|
|
219
|
+
telemetryStatus: null,
|
|
246
220
|
bridge: p.bridge,
|
|
247
221
|
});
|
|
248
222
|
}
|
|
@@ -294,10 +268,27 @@ agentList.addEventListener('change', (e) => {
|
|
|
294
268
|
const card = e.target.closest('.agent-card');
|
|
295
269
|
card.querySelector('.agent-resume-fields').classList.toggle('hidden', !e.target.checked);
|
|
296
270
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
271
|
+
// When enabling an agent that needs setup, trigger auto-setup
|
|
272
|
+
if (e.target.classList.contains('agent-enabled') && e.target.checked) {
|
|
273
|
+
const idx = +e.target.closest('.agent-card').dataset.idx;
|
|
274
|
+
const cmd = state.cfg.commands[idx];
|
|
275
|
+
const preset = telemetryPreset(cmd);
|
|
276
|
+
if (preset?.telemetryAutoSetup && !cmd.telemetryEnabled) {
|
|
277
|
+
send({ type: 'telemetry.autosetup', presetId: preset.presetId });
|
|
278
|
+
return; // config broadcast from server will re-render with enabled + telemetryEnabled
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// When disabling an agent that has setup, remove patches only if no other commands of the same agent are enabled
|
|
282
|
+
if (e.target.classList.contains('agent-enabled') && !e.target.checked) {
|
|
283
|
+
const idx = +e.target.closest('.agent-card').dataset.idx;
|
|
284
|
+
const cmd = state.cfg.commands[idx];
|
|
285
|
+
const preset = telemetryPreset(cmd);
|
|
286
|
+
if (preset && cmd.telemetryEnabled) {
|
|
287
|
+
const othersEnabled = state.cfg.commands.some((c, i) => i !== idx && c.enabled && telemetryPreset(c)?.presetId === preset.presetId);
|
|
288
|
+
if (!othersEnabled) {
|
|
289
|
+
send({ type: 'telemetry.configure', presetId: preset.presetId, enable: false });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
301
292
|
}
|
|
302
293
|
saveConfig();
|
|
303
294
|
});
|
|
@@ -448,7 +439,6 @@ function saveConfig() {
|
|
|
448
439
|
state.cfg.commands = [...agentCards].map((card, i) => {
|
|
449
440
|
const existing = state.cfg.commands[i] || {};
|
|
450
441
|
const command = card.querySelector('.agent-command').value.trim() || state.cfg.defaultShell;
|
|
451
|
-
const isClaude = binName(command) === 'claude';
|
|
452
442
|
return {
|
|
453
443
|
id: existing.id || crypto.randomUUID(),
|
|
454
444
|
label: card.querySelector('.agent-name').value.trim() || 'Untitled',
|
|
@@ -461,8 +451,8 @@ function saveConfig() {
|
|
|
461
451
|
resumeCommand: card.querySelector('.agent-resume-cmd')?.value.trim() || null,
|
|
462
452
|
sessionIdPattern: existing.sessionIdPattern || null,
|
|
463
453
|
outputMarker: existing.outputMarker || null,
|
|
464
|
-
telemetryEnabled:
|
|
465
|
-
telemetryStatus:
|
|
454
|
+
telemetryEnabled: existing.telemetryEnabled || false,
|
|
455
|
+
telemetryStatus: existing.telemetryStatus || null,
|
|
466
456
|
bridge: existing.bridge,
|
|
467
457
|
};
|
|
468
458
|
});
|
package/public/js/terminals.js
CHANGED
|
@@ -340,26 +340,29 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
|
|
|
340
340
|
|
|
341
341
|
// [SCREEN-CAPTURE] extract terminal buffer when BOTH idle AND render-silent (2s)
|
|
342
342
|
// Decoupled from status: telemetry knows when agent is done, onRender knows when terminal is done
|
|
343
|
-
const
|
|
343
|
+
const _hasServerStatus = cmd?.presetId === 'claude-code' || cmd?.presetId === 'codex' || cmd?.presetId === 'gemini-cli' || cmd?.presetId === 'opencode';
|
|
344
344
|
let _screenTimer = null, _renderSilent = false;
|
|
345
345
|
function _tryScreenCapture() {
|
|
346
346
|
const entry = state.terms.get(id);
|
|
347
|
-
if (!entry?.pendingScreenCapture || (!_renderSilent && !
|
|
347
|
+
if (!entry?.pendingScreenCapture || (!_renderSilent && !_hasServerStatus) || !entry.term) return;
|
|
348
348
|
entry.pendingScreenCapture = false;
|
|
349
349
|
const buf = entry.term.buffer.active;
|
|
350
350
|
const lines = [];
|
|
351
351
|
for (let i = 0; i < buf.length; i++) { const line = buf.getLine(i); if (line) lines.push(line.translateToString(true)); }
|
|
352
352
|
send({ type: 'terminal.buffer', id, lines });
|
|
353
353
|
}
|
|
354
|
-
let
|
|
354
|
+
let _lastTyping = 0;
|
|
355
355
|
term.onData(() => { _lastTyping = Date.now(); });
|
|
356
356
|
term.onRender(() => {
|
|
357
|
-
_lastRender = Date.now();
|
|
358
357
|
_renderSilent = false;
|
|
359
358
|
clearTimeout(_screenTimer);
|
|
360
359
|
_screenTimer = setTimeout(() => { _renderSilent = true; _tryScreenCapture(); }, 2000);
|
|
361
360
|
});
|
|
362
|
-
term.onWriteParsed(() => {
|
|
361
|
+
term.onWriteParsed(() => {
|
|
362
|
+
if (Date.now() - _lastTyping < 500) return;
|
|
363
|
+
const entry = state.terms.get(id);
|
|
364
|
+
if (entry) entry.lastRenderAt = Date.now();
|
|
365
|
+
});
|
|
363
366
|
|
|
364
367
|
// Expose capture function so setStatus can trigger it when idle arrives after render silence
|
|
365
368
|
setTimeout(() => { const e = state.terms.get(id); if (e) e.tryScreenCapture = _tryScreenCapture; }, 0);
|
package/public/tailwind.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--color-base:#020617;--color-surface:#0f172a;--color-raised:#1e293b;--color-muted:#334155;--color-border:#475569;--color-subtle:#64748b;--color-dim:#94a3b8;--color-soft:#cbd5e1;--color-text:#e2e8f0;--color-bright:#f8fafc;--color-overlay:rgba(0,0,0,.6);--color-shadow:rgba(0,0,0,.5);--color-dialog:#1e293b;--color-stats-bg:rgba(0,0,0,.95);--color-accent-subtle:rgba(59,130,246,.15);--color-rail:#1d1f1f;--color-rail-active:#2a323f;--color-sidebar:#161717;--color-sidebar-input:#0f1010;--color-sidebar-border:#2e2f2f;--color-chat-hover:#2e2f2f;--color-chat-active:#2e2f2f;--color-rail-badge-bg:#5cbd6d;--color-rail-badge-text:#0a0a0a}.light{--color-base:#edeef1;--color-surface:#f7f8fa;--color-raised:#fff;--color-muted:#e4e6ea;--color-border:#d1d5db;--color-subtle:#8b919a;--color-dim:#5f6672;--color-soft:#3d4450;--color-text:#1a1d24;--color-bright:#0c0e12;--color-overlay:rgba(0,0,0,.25);--color-shadow:rgba(0,0,0,.08);--color-dialog:#fff;--color-stats-bg:hsla(0,0%,100%,.92);--color-accent-subtle:rgba(59,130,246,.08);--color-rail:#fcfdfd;--color-rail-active:#eae9e7;--color-sidebar:#f9fafa;--color-sidebar-input:#fff;--color-sidebar-border:#e0deda;--color-chat-hover:#f6f5f5;--color-chat-active:#eff0f0;--color-rail-badge-bg:#51a868;--color-rail-badge-text:#fff}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-1{inset:.25rem}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-0{bottom:0}.bottom-5{bottom:1.25rem}.left-2\.5{left:.625rem}.right-0{right:0}.right-3{right:.75rem}.right-5{right:1.25rem}.top-1\/2{top:50%}.top-2{top:.5rem}.z-10{z-index:10}.z-\[200\]{z-index:200}.z-\[250\]{z-index:250}.z-\[260\]{z-index:260}.z-\[300\]{z-index:300}.z-\[400\]{z-index:400}.z-\[500\]{z-index:500}.mx-4{margin-left:1rem;margin-right:1rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.-mt-2{margin-top:-.5rem}.-mt-px{margin-top:-1px}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.ml-0\.5{margin-left:.125rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mr-1\.5{margin-right:.375rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.\!hidden{display:none!important}.hidden{display:none}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[180px\]{height:180px}.h-\[18px\]{height:18px}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[160px\]{max-height:160px}.max-h-\[400px\]{max-height:400px}.max-h-\[460px\]{max-height:460px}.min-h-0{min-height:0}.min-h-\[200px\]{min-height:200px}.w-12{width:3rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-44{width:11rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-\[180px\]{width:180px}.w-\[18px\]{width:18px}.w-\[340px\]{width:340px}.w-\[354px\]{width:354px}.w-\[360px\]{width:360px}.w-\[420px\]{width:420px}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[160px\]{min-width:160px}.min-w-\[16px\]{min-width:16px}.min-w-\[220px\]{min-width:220px}.min-w-\[260px\]{min-width:260px}.min-w-\[354px\]{min-width:354px}.max-w-2xl{max-width:42rem}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.scale-110{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.cursor-wait{cursor:wait}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-y{resize:vertical}.resize{resize:both}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem*var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-px>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1px*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-400{--tw-border-opacity:1;border-color:rgb(96 165 250/var(--tw-border-opacity,1))}.border-red-500\/30{border-color:rgba(239,68,68,.3)}.border-slate-600{--tw-border-opacity:1;border-color:rgb(71 85 105/var(--tw-border-opacity,1))}.border-slate-700{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.border-slate-700\/30{border-color:rgba(51,65,85,.3)}.border-slate-700\/40{border-color:rgba(51,65,85,.4)}.border-slate-700\/50{border-color:rgba(51,65,85,.5)}.border-slate-700\/60{border-color:rgba(51,65,85,.6)}.border-slate-900{--tw-border-opacity:1;border-color:rgb(15 23 42/var(--tw-border-opacity,1))}.border-transparent{border-color:transparent}.bg-black\/60{background-color:rgba(0,0,0,.6)}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/15{background-color:rgba(59,130,246,.15)}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-600\/20{background-color:rgba(220,38,38,.2)}.bg-slate-700{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.bg-slate-700\/50{background-color:rgba(51,65,85,.5)}.bg-slate-700\/60{background-color:rgba(51,65,85,.6)}.bg-slate-800{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity,1))}.bg-slate-800\/30{background-color:rgba(30,41,59,.3)}.bg-slate-800\/40{background-color:rgba(30,41,59,.4)}.bg-slate-800\/50{background-color:rgba(30,41,59,.5)}.bg-slate-800\/60{background-color:rgba(30,41,59,.6)}.bg-slate-800\/80{background-color:rgba(30,41,59,.8)}.bg-slate-800\/95{background-color:rgba(30,41,59,.95)}.bg-slate-900{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity,1))}.bg-slate-900\/70{background-color:rgba(15,23,42,.7)}.bg-slate-950{--tw-bg-opacity:1;background-color:rgb(2 6 23/var(--tw-bg-opacity,1))}.bg-transparent{background-color:transparent}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-\[3px\]{padding:3px}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-\[5px\]{padding-top:5px;padding-bottom:5px}.py-\[7px\]{padding-top:7px;padding-bottom:7px}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-2\.5{padding-bottom:.625rem}.pb-3{padding-bottom:.75rem}.pb-3\.5{padding-bottom:.875rem}.pl-2{padding-left:.5rem}.pl-4{padding-left:1rem}.pl-9{padding-left:2.25rem}.pr-3{padding-right:.75rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-3\.5{padding-top:.875rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-\[1\.45\]{line-height:1.45}.leading-\[1\.4\]{line-height:1.4}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-emerald-400\/80{color:rgba(52,211,153,.8)}.text-emerald-500{--tw-text-opacity:1;color:rgb(16 185 129/var(--tw-text-opacity,1))}.text-emerald-500\/70{color:rgba(16,185,129,.7)}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-slate-200{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity,1))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.placeholder-slate-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(100 116 139/var(--tw-placeholder-opacity,1))}.placeholder-slate-500::placeholder{--tw-placeholder-opacity:1;color:rgb(100 116 139/var(--tw-placeholder-opacity,1))}.placeholder-slate-600::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(71 85 105/var(--tw-placeholder-opacity,1))}.placeholder-slate-600::placeholder{--tw-placeholder-opacity:1;color:rgb(71 85 105/var(--tw-placeholder-opacity,1))}.accent-blue-500{accent-color:#3b82f6}.opacity-0{opacity:0}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-60{opacity:.6}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.shadow-black\/40{--tw-shadow-color:rgba(0,0,0,.4);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/50{--tw-shadow-color:rgba(0,0,0,.5);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/60{--tw-shadow-color:rgba(0,0,0,.6);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-white\/40{--tw-ring-color:hsla(0,0%,100%,.4)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.bg-slate-950{background-color:var(--color-base)!important}.bg-slate-900{background-color:var(--color-surface)!important}.bg-slate-800{background-color:var(--color-raised)!important}.bg-slate-700{background-color:var(--color-muted)!important}.bg-slate-800\/50{background-color:color-mix(in srgb,var(--color-raised) 50%,transparent)!important}.bg-slate-800\/30{background-color:color-mix(in srgb,var(--color-raised) 30%,transparent)!important}.bg-slate-800\/60{background-color:color-mix(in srgb,var(--color-raised) 60%,transparent)!important}.bg-slate-800\/80{background-color:color-mix(in srgb,var(--color-raised) 80%,transparent)!important}.bg-slate-900\/70{background-color:color-mix(in srgb,var(--color-surface) 70%,transparent)!important}.bg-slate-800\/95{background-color:color-mix(in srgb,var(--color-raised) 95%,transparent)!important}.bg-slate-700\/50{background-color:color-mix(in srgb,var(--color-muted) 50%,transparent)!important}.bg-slate-700\/60{background-color:color-mix(in srgb,var(--color-muted) 60%,transparent)!important}.hover\:bg-slate-700:hover{background-color:var(--color-muted)!important}.hover\:bg-slate-800:hover{background-color:var(--color-raised)!important}.hover\:bg-slate-800\/50:hover{background-color:color-mix(in srgb,var(--color-raised) 50%,transparent)!important}.hover\:bg-slate-800\/30:hover{background-color:color-mix(in srgb,var(--color-raised) 30%,transparent)!important}.hover\:bg-slate-700\/50:hover{background-color:color-mix(in srgb,var(--color-muted) 50%,transparent)!important}.hover\:bg-slate-700\/70:hover{background-color:color-mix(in srgb,var(--color-muted) 70%,transparent)!important}.text-slate-200{color:var(--color-text)!important}.text-slate-300{color:var(--color-soft)!important}.text-slate-400{color:var(--color-dim)!important}.text-slate-500{color:var(--color-subtle)!important}.text-slate-600{color:var(--color-border)!important}.hover\:text-slate-200:hover{color:var(--color-text)!important}.hover\:text-slate-300:hover{color:var(--color-soft)!important}.hover\:text-slate-400:hover{color:var(--color-dim)!important}.border-slate-600{border-color:var(--color-border)!important}.border-slate-700{border-color:var(--color-muted)!important}.border-slate-700\/50{border-color:color-mix(in srgb,var(--color-muted) 50%,transparent)!important}.border-slate-700\/40{border-color:color-mix(in srgb,var(--color-muted) 40%,transparent)!important}.border-slate-700\/60{border-color:color-mix(in srgb,var(--color-muted) 60%,transparent)!important}.border-slate-600\/60{border-color:color-mix(in srgb,var(--color-border) 60%,transparent)!important}.hover\:border-slate-500:hover{border-color:var(--color-subtle)!important}.focus\:border-slate-600\/60:focus{border-color:color-mix(in srgb,var(--color-border) 60%,transparent)!important}.focus\:bg-slate-800\/80:focus{background-color:color-mix(in srgb,var(--color-raised) 80%,transparent)!important}.placeholder-slate-500::-moz-placeholder{color:var(--color-subtle)!important}.placeholder-slate-500::placeholder{color:var(--color-subtle)!important}.placeholder-slate-600::-moz-placeholder{color:var(--color-border)!important}.placeholder-slate-600::placeholder{color:var(--color-border)!important}.ring-slate-500{--tw-ring-color:var(--color-subtle)!important}.bg-black\/60{background-color:var(--color-overlay)!important}.shadow-black\/40,.shadow-black\/50,.shadow-black\/60{--tw-shadow-color:var(--color-shadow)!important}.disabled\:bg-slate-600:disabled{background-color:var(--color-border)!important}.disabled\:text-slate-400:disabled{color:var(--color-dim)!important}:root{--color-preview:#a2a2a2;--color-time:#7c7d7d;--color-time-recent:#5cbd6d;--color-dormant:#555;--color-proj-meta:#7c7d7d;--color-search-bg:#2e2f2f;--color-search-text:#acacac;--color-session-hover:hsla(0,0%,100%,.04);--color-header-icon:#cbd5e1;--color-rail-icon:#a5a6a6}.light{--color-preview:#626262;--color-time:#666;--color-time-recent:#51a868;--color-dormant:#aaa;--color-proj-meta:#666;--color-search-bg:#f6f5f5;--color-search-text:#626262;--color-session-hover:rgba(0,0,0,.04);--color-header-icon:#3d4450;--color-rail-icon:#636261}.session-preview{color:var(--color-preview)!important}.session-time{color:var(--color-time)!important}.session-time.recent{color:var(--color-time-recent)!important}.session-status.dormant{color:var(--color-dormant)!important}.group[data-id]:hover,.pill-row:hover,.resumable-row:hover{background-color:var(--color-session-hover)!important}.project-count,.project-menu-btn{color:var(--color-proj-meta)!important}#search-input{background-color:var(--color-search-bg)!important;color:var(--color-search-text)!important}#search-input::-moz-placeholder{color:var(--color-search-text)!important;opacity:.7}#search-input::placeholder{color:var(--color-search-text)!important;opacity:.7}.relative:has(#search-input)>svg{color:var(--color-search-text)!important}.rail-btn:not(.text-slate-200){color:var(--color-rail-icon)!important}.rail-btn.bg-slate-800{background-color:var(--color-rail-active)!important}.icon-btn{color:var(--color-header-icon)!important;border-color:var(--color-border)!important;font-weight:700}.icon-btn svg{stroke-width:2}:root{--color-separator:#2e2f2f}.light{--color-separator:#d8d3cd}#nav-rail{background-color:var(--color-rail)!important}#sidebar{background-color:var(--color-sidebar)!important;border-right-color:var(--color-separator)!important}#sidebar input[type=text],#sidebar select{background-color:var(--color-sidebar-input)!important;border-color:var(--color-sidebar-border)!important}.group:hover{background-color:var(--color-chat-hover)!important}.group.active-session{background-color:var(--color-chat-active)!important}.theme-toggle{position:relative;width:36px;height:36px}.theme-toggle svg{position:absolute;inset:0;margin:auto;transition:opacity .3s ease,transform .3s ease}.theme-toggle .icon-sun{opacity:0;transform:rotate(-90deg) scale(.5)}.light .theme-toggle .icon-sun,.theme-toggle .icon-moon{opacity:1;transform:rotate(0) scale(1)}.light .theme-toggle .icon-moon{opacity:0;transform:rotate(90deg) scale(.5)}html{transition:color .3s ease,background-color .3s ease}.term-wrap{visibility:hidden;position:absolute;overflow:hidden;top:4px;left:4px;right:4px;bottom:0}.term-wrap.active{visibility:visible}.term-wrap .xterm{height:100%}.save-indicator{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;border:1px solid var(--color-search-text);margin-right:4px;opacity:.35;transition:opacity .4s ease}.save-indicator.saved{opacity:.5}.save-indicator .save-tick{color:var(--color-time-recent);display:block}.save-indicator .save-spin{display:none;color:var(--color-dim);animation:save-rotate 1.5s cubic-bezier(.4,0,.2,1) infinite}.save-indicator.saving .save-tick{display:none}.save-indicator.saving .save-spin{display:block}.save-indicator.saving{opacity:.5}@keyframes save-rotate{to{transform:rotate(1turn)}}.tmx-toast{box-shadow:0 25px 50px -12px rgba(0,0,0,.5)}.light .tmx-toast{box-shadow:0 25px 50px -12px rgba(0,0,0,.25),0 0 0 1px rgba(0,0,0,.05)}.drop-highlight{border-radius:.25rem;background-color:rgba(59,130,246,.1);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(59,130,246,.3)}.project-drop-line{height:2px;margin:.125rem .5rem;border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.plugin-chevron.collapsed,.project-chevron.collapsed svg{transform:rotate(-90deg)}.tmx-scroll{scrollbar-width:thin;scrollbar-color:transparent transparent}.tmx-scroll::-webkit-scrollbar{width:10px}.tmx-scroll::-webkit-scrollbar-track{background:transparent}.tmx-scroll::-webkit-scrollbar-thumb{background:transparent;border:2px solid transparent;border-radius:9999px;-webkit-transition:background-color .2s ease,border-color .2s ease;transition:background-color .2s ease,border-color .2s ease}.tmx-scroll.is-scrolling,.tmx-scroll:hover{scrollbar-color:color-mix(in srgb,var(--color-muted) 78%,transparent) color-mix(in srgb,var(--color-sidebar) 70%,transparent)}.tmx-scroll.is-scrolling::-webkit-scrollbar-track,.tmx-scroll:hover::-webkit-scrollbar-track{background:color-mix(in srgb,var(--color-sidebar) 70%,transparent)}.tmx-scroll.is-scrolling::-webkit-scrollbar-thumb,.tmx-scroll:hover::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--color-muted) 78%,transparent);border-color:color-mix(in srgb,var(--color-sidebar) 70%,transparent)}.tmx-scroll.is-scrolling::-webkit-scrollbar-thumb:hover,.tmx-scroll:hover::-webkit-scrollbar-thumb:hover{background:color-mix(in srgb,var(--color-subtle) 85%,transparent)}.prompt-autocomplete{position:fixed;width:340px;background:var(--color-raised);border:1px solid var(--color-muted);border-radius:10px;box-shadow:0 20px 40px -8px rgba(0,0,0,.5);z-index:100;display:flex;flex-direction:column;overflow:hidden;animation:pa-in .15s ease}@keyframes pa-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.pa-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid color-mix(in srgb,var(--color-muted) 50%,transparent)}.pa-label{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--color-subtle)}.pa-query{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--color-dim);background:color-mix(in srgb,var(--color-muted) 40%,transparent);padding:2px 6px;border-radius:4px}.pa-list{overflow-y:auto;padding:4px;max-height:184px}.pa-item{padding:8px 10px;border-radius:6px;cursor:pointer;transition:background-color .1s}.pa-item:hover,.pa-selected{background:color-mix(in srgb,var(--color-muted) 40%,transparent)}.pa-name{font-size:13px;font-weight:500;color:var(--color-text)}.pa-name,.pa-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pa-text{font-size:11px;color:var(--color-subtle);margin-top:2px}.pa-item mark{background:rgba(59,130,246,.25);color:inherit;border-radius:2px;padding:0 1px}.pa-footer{display:flex;gap:12px;justify-content:center;padding:6px 12px;border-top:1px solid color-mix(in srgb,var(--color-muted) 50%,transparent);font-size:10px;color:var(--color-subtle)}.pa-footer kbd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;padding:1px 4px;border-radius:3px;background:color-mix(in srgb,var(--color-muted) 50%,transparent);color:var(--color-dim)}.pa-empty{padding:20px 12px;text-align:center;font-size:13px;color:var(--color-subtle)}.pa-hint{padding:0 12px 12px;text-align:center;font-size:11px;color:var(--color-border)}.pa-hint kbd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:color-mix(in srgb,var(--color-muted) 50%,transparent);padding:1px 4px;border-radius:3px}.empty\:hidden:empty{display:none}.hover\:scale-125:hover{--tw-scale-x:1.25;--tw-scale-y:1.25;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-slate-500:hover{--tw-border-opacity:1;border-color:rgb(100 116 139/var(--tw-border-opacity,1))}.hover\:bg-blue-500:hover{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.hover\:bg-blue-500\/20:hover{background-color:rgba(59,130,246,.2)}.hover\:bg-red-500:hover{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.hover\:bg-slate-700:hover{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.hover\:bg-slate-700\/50:hover{background-color:rgba(51,65,85,.5)}.hover\:bg-slate-700\/60:hover{background-color:rgba(51,65,85,.6)}.hover\:bg-slate-700\/70:hover{background-color:rgba(51,65,85,.7)}.hover\:bg-slate-800\/30:hover{background-color:rgba(30,41,59,.3)}.hover\:bg-slate-800\/40:hover{background-color:rgba(30,41,59,.4)}.hover\:bg-slate-800\/50:hover{background-color:rgba(30,41,59,.5)}.hover\:text-blue-300:hover{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.hover\:text-emerald-300:hover{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.hover\:text-emerald-400:hover{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.hover\:text-indigo-400:hover{--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hover\:text-slate-200:hover{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.hover\:text-slate-300:hover{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.hover\:text-slate-400:hover{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.hover\:ring-2:hover{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.hover\:ring-slate-500:hover{--tw-ring-opacity:1;--tw-ring-color:rgb(100 116 139/var(--tw-ring-opacity,1))}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.focus\:border-slate-600\/60:focus{border-color:rgba(71,85,105,.6)}.focus\:bg-slate-800\/80:focus{background-color:rgba(30,41,59,.8)}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-blue-500\/30:focus{--tw-ring-color:rgba(59,130,246,.3)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-slate-600:disabled{--tw-bg-opacity:1;background-color:rgb(71 85 105/var(--tw-bg-opacity,1))}.disabled\:text-slate-400:disabled{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.group:hover .group-hover\:opacity-100{opacity:1}
|
|
1
|
+
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--color-base:#020617;--color-surface:#0f172a;--color-raised:#1e293b;--color-muted:#334155;--color-border:#475569;--color-subtle:#64748b;--color-dim:#94a3b8;--color-soft:#cbd5e1;--color-text:#e2e8f0;--color-bright:#f8fafc;--color-overlay:rgba(0,0,0,.6);--color-shadow:rgba(0,0,0,.5);--color-dialog:#1e293b;--color-stats-bg:rgba(0,0,0,.95);--color-accent-subtle:rgba(59,130,246,.15);--color-rail:#1d1f1f;--color-rail-active:#2a323f;--color-sidebar:#161717;--color-sidebar-input:#0f1010;--color-sidebar-border:#2e2f2f;--color-chat-hover:#2e2f2f;--color-chat-active:#2e2f2f;--color-rail-badge-bg:#5cbd6d;--color-rail-badge-text:#0a0a0a}.light{--color-base:#edeef1;--color-surface:#f7f8fa;--color-raised:#fff;--color-muted:#e4e6ea;--color-border:#d1d5db;--color-subtle:#8b919a;--color-dim:#5f6672;--color-soft:#3d4450;--color-text:#1a1d24;--color-bright:#0c0e12;--color-overlay:rgba(0,0,0,.25);--color-shadow:rgba(0,0,0,.08);--color-dialog:#fff;--color-stats-bg:hsla(0,0%,100%,.92);--color-accent-subtle:rgba(59,130,246,.08);--color-rail:#fcfdfd;--color-rail-active:#eae9e7;--color-sidebar:#f9fafa;--color-sidebar-input:#fff;--color-sidebar-border:#e0deda;--color-chat-hover:#f6f5f5;--color-chat-active:#eff0f0;--color-rail-badge-bg:#51a868;--color-rail-badge-text:#fff}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-1{inset:.25rem}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-0{bottom:0}.bottom-5{bottom:1.25rem}.left-2\.5{left:.625rem}.right-0{right:0}.right-3{right:.75rem}.right-5{right:1.25rem}.top-1\/2{top:50%}.top-2{top:.5rem}.z-10{z-index:10}.z-\[200\]{z-index:200}.z-\[250\]{z-index:250}.z-\[260\]{z-index:260}.z-\[300\]{z-index:300}.z-\[400\]{z-index:400}.z-\[500\]{z-index:500}.mx-4{margin-left:1rem;margin-right:1rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.-mt-2{margin-top:-.5rem}.-mt-px{margin-top:-1px}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.ml-0\.5{margin-left:.125rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mr-1\.5{margin-right:.375rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.\!hidden{display:none!important}.hidden{display:none}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[180px\]{height:180px}.h-\[18px\]{height:18px}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[160px\]{max-height:160px}.max-h-\[400px\]{max-height:400px}.max-h-\[460px\]{max-height:460px}.min-h-0{min-height:0}.min-h-\[200px\]{min-height:200px}.w-12{width:3rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-44{width:11rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-\[180px\]{width:180px}.w-\[18px\]{width:18px}.w-\[340px\]{width:340px}.w-\[354px\]{width:354px}.w-\[360px\]{width:360px}.w-\[420px\]{width:420px}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[160px\]{min-width:160px}.min-w-\[16px\]{min-width:16px}.min-w-\[220px\]{min-width:220px}.min-w-\[260px\]{min-width:260px}.min-w-\[354px\]{min-width:354px}.max-w-2xl{max-width:42rem}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.scale-110{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.cursor-wait{cursor:wait}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-y{resize:vertical}.resize{resize:both}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem*var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-px>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1px*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-400{--tw-border-opacity:1;border-color:rgb(96 165 250/var(--tw-border-opacity,1))}.border-red-500\/30{border-color:rgba(239,68,68,.3)}.border-slate-600{--tw-border-opacity:1;border-color:rgb(71 85 105/var(--tw-border-opacity,1))}.border-slate-700{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.border-slate-700\/30{border-color:rgba(51,65,85,.3)}.border-slate-700\/40{border-color:rgba(51,65,85,.4)}.border-slate-700\/50{border-color:rgba(51,65,85,.5)}.border-slate-700\/60{border-color:rgba(51,65,85,.6)}.border-slate-900{--tw-border-opacity:1;border-color:rgb(15 23 42/var(--tw-border-opacity,1))}.border-transparent{border-color:transparent}.bg-amber-500\/10{background-color:rgba(245,158,11,.1)}.bg-black\/60{background-color:rgba(0,0,0,.6)}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/15{background-color:rgba(59,130,246,.15)}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-600\/20{background-color:rgba(220,38,38,.2)}.bg-slate-700{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.bg-slate-700\/50{background-color:rgba(51,65,85,.5)}.bg-slate-700\/60{background-color:rgba(51,65,85,.6)}.bg-slate-800{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity,1))}.bg-slate-800\/30{background-color:rgba(30,41,59,.3)}.bg-slate-800\/40{background-color:rgba(30,41,59,.4)}.bg-slate-800\/50{background-color:rgba(30,41,59,.5)}.bg-slate-800\/60{background-color:rgba(30,41,59,.6)}.bg-slate-800\/80{background-color:rgba(30,41,59,.8)}.bg-slate-800\/95{background-color:rgba(30,41,59,.95)}.bg-slate-900{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity,1))}.bg-slate-900\/70{background-color:rgba(15,23,42,.7)}.bg-slate-950{--tw-bg-opacity:1;background-color:rgb(2 6 23/var(--tw-bg-opacity,1))}.bg-transparent{background-color:transparent}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-\[3px\]{padding:3px}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-\[5px\]{padding-top:5px;padding-bottom:5px}.py-\[7px\]{padding-top:7px;padding-bottom:7px}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-2\.5{padding-bottom:.625rem}.pb-3{padding-bottom:.75rem}.pb-3\.5{padding-bottom:.875rem}.pl-2{padding-left:.5rem}.pl-4{padding-left:1rem}.pl-9{padding-left:2.25rem}.pr-3{padding-right:.75rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-3\.5{padding-top:.875rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-\[1\.45\]{line-height:1.45}.leading-\[1\.4\]{line-height:1.4}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-emerald-400\/80{color:rgba(52,211,153,.8)}.text-emerald-500{--tw-text-opacity:1;color:rgb(16 185 129/var(--tw-text-opacity,1))}.text-emerald-500\/70{color:rgba(16,185,129,.7)}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-slate-200{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity,1))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.placeholder-slate-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(100 116 139/var(--tw-placeholder-opacity,1))}.placeholder-slate-500::placeholder{--tw-placeholder-opacity:1;color:rgb(100 116 139/var(--tw-placeholder-opacity,1))}.placeholder-slate-600::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(71 85 105/var(--tw-placeholder-opacity,1))}.placeholder-slate-600::placeholder{--tw-placeholder-opacity:1;color:rgb(71 85 105/var(--tw-placeholder-opacity,1))}.accent-blue-500{accent-color:#3b82f6}.opacity-0{opacity:0}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-60{opacity:.6}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.shadow-black\/40{--tw-shadow-color:rgba(0,0,0,.4);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/50{--tw-shadow-color:rgba(0,0,0,.5);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/60{--tw-shadow-color:rgba(0,0,0,.6);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-white\/40{--tw-ring-color:hsla(0,0%,100%,.4)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.bg-slate-950{background-color:var(--color-base)!important}.bg-slate-900{background-color:var(--color-surface)!important}.bg-slate-800{background-color:var(--color-raised)!important}.bg-slate-700{background-color:var(--color-muted)!important}.bg-slate-800\/50{background-color:color-mix(in srgb,var(--color-raised) 50%,transparent)!important}.bg-slate-800\/30{background-color:color-mix(in srgb,var(--color-raised) 30%,transparent)!important}.bg-slate-800\/60{background-color:color-mix(in srgb,var(--color-raised) 60%,transparent)!important}.bg-slate-800\/80{background-color:color-mix(in srgb,var(--color-raised) 80%,transparent)!important}.bg-slate-900\/70{background-color:color-mix(in srgb,var(--color-surface) 70%,transparent)!important}.bg-slate-800\/95{background-color:color-mix(in srgb,var(--color-raised) 95%,transparent)!important}.bg-slate-700\/50{background-color:color-mix(in srgb,var(--color-muted) 50%,transparent)!important}.bg-slate-700\/60{background-color:color-mix(in srgb,var(--color-muted) 60%,transparent)!important}.hover\:bg-slate-700:hover{background-color:var(--color-muted)!important}.hover\:bg-slate-800:hover{background-color:var(--color-raised)!important}.hover\:bg-slate-800\/50:hover{background-color:color-mix(in srgb,var(--color-raised) 50%,transparent)!important}.hover\:bg-slate-800\/30:hover{background-color:color-mix(in srgb,var(--color-raised) 30%,transparent)!important}.hover\:bg-slate-700\/50:hover{background-color:color-mix(in srgb,var(--color-muted) 50%,transparent)!important}.hover\:bg-slate-700\/70:hover{background-color:color-mix(in srgb,var(--color-muted) 70%,transparent)!important}.text-slate-200{color:var(--color-text)!important}.text-slate-300{color:var(--color-soft)!important}.text-slate-400{color:var(--color-dim)!important}.text-slate-500{color:var(--color-subtle)!important}.text-slate-600{color:var(--color-border)!important}.hover\:text-slate-200:hover{color:var(--color-text)!important}.hover\:text-slate-300:hover{color:var(--color-soft)!important}.hover\:text-slate-400:hover{color:var(--color-dim)!important}.border-slate-600{border-color:var(--color-border)!important}.border-slate-700{border-color:var(--color-muted)!important}.border-slate-700\/50{border-color:color-mix(in srgb,var(--color-muted) 50%,transparent)!important}.border-slate-700\/40{border-color:color-mix(in srgb,var(--color-muted) 40%,transparent)!important}.border-slate-700\/60{border-color:color-mix(in srgb,var(--color-muted) 60%,transparent)!important}.border-slate-600\/60{border-color:color-mix(in srgb,var(--color-border) 60%,transparent)!important}.hover\:border-slate-500:hover{border-color:var(--color-subtle)!important}.focus\:border-slate-600\/60:focus{border-color:color-mix(in srgb,var(--color-border) 60%,transparent)!important}.focus\:bg-slate-800\/80:focus{background-color:color-mix(in srgb,var(--color-raised) 80%,transparent)!important}.placeholder-slate-500::-moz-placeholder{color:var(--color-subtle)!important}.placeholder-slate-500::placeholder{color:var(--color-subtle)!important}.placeholder-slate-600::-moz-placeholder{color:var(--color-border)!important}.placeholder-slate-600::placeholder{color:var(--color-border)!important}.ring-slate-500{--tw-ring-color:var(--color-subtle)!important}.bg-black\/60{background-color:var(--color-overlay)!important}.shadow-black\/40,.shadow-black\/50,.shadow-black\/60{--tw-shadow-color:var(--color-shadow)!important}.disabled\:bg-slate-600:disabled{background-color:var(--color-border)!important}.disabled\:text-slate-400:disabled{color:var(--color-dim)!important}:root{--color-preview:#a2a2a2;--color-time:#7c7d7d;--color-time-recent:#5cbd6d;--color-dormant:#555;--color-proj-meta:#7c7d7d;--color-search-bg:#2e2f2f;--color-search-text:#acacac;--color-session-hover:hsla(0,0%,100%,.04);--color-header-icon:#cbd5e1;--color-rail-icon:#a5a6a6}.light{--color-preview:#626262;--color-time:#666;--color-time-recent:#51a868;--color-dormant:#aaa;--color-proj-meta:#666;--color-search-bg:#f6f5f5;--color-search-text:#626262;--color-session-hover:rgba(0,0,0,.04);--color-header-icon:#3d4450;--color-rail-icon:#636261}.session-preview{color:var(--color-preview)!important}.session-time{color:var(--color-time)!important}.session-time.recent{color:var(--color-time-recent)!important}.session-status.dormant{color:var(--color-dormant)!important}.group[data-id]:hover,.pill-row:hover,.resumable-row:hover{background-color:var(--color-session-hover)!important}.project-count,.project-menu-btn{color:var(--color-proj-meta)!important}#search-input{background-color:var(--color-search-bg)!important;color:var(--color-search-text)!important}#search-input::-moz-placeholder{color:var(--color-search-text)!important;opacity:.7}#search-input::placeholder{color:var(--color-search-text)!important;opacity:.7}.relative:has(#search-input)>svg{color:var(--color-search-text)!important}.rail-btn:not(.text-slate-200){color:var(--color-rail-icon)!important}.rail-btn.bg-slate-800{background-color:var(--color-rail-active)!important}.icon-btn{color:var(--color-header-icon)!important;border-color:var(--color-border)!important;font-weight:700}.icon-btn svg{stroke-width:2}:root{--color-separator:#2e2f2f}.light{--color-separator:#d8d3cd}#nav-rail{background-color:var(--color-rail)!important}#sidebar{background-color:var(--color-sidebar)!important;border-right-color:var(--color-separator)!important}#sidebar input[type=text],#sidebar select{background-color:var(--color-sidebar-input)!important;border-color:var(--color-sidebar-border)!important}.group:hover{background-color:var(--color-chat-hover)!important}.group.active-session{background-color:var(--color-chat-active)!important}.theme-toggle{position:relative;width:36px;height:36px}.theme-toggle svg{position:absolute;inset:0;margin:auto;transition:opacity .3s ease,transform .3s ease}.theme-toggle .icon-sun{opacity:0;transform:rotate(-90deg) scale(.5)}.light .theme-toggle .icon-sun,.theme-toggle .icon-moon{opacity:1;transform:rotate(0) scale(1)}.light .theme-toggle .icon-moon{opacity:0;transform:rotate(90deg) scale(.5)}html{transition:color .3s ease,background-color .3s ease}.term-wrap{visibility:hidden;position:absolute;overflow:hidden;top:4px;left:4px;right:4px;bottom:0}.term-wrap.active{visibility:visible}.term-wrap .xterm{height:100%}.save-indicator{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;border:1px solid var(--color-search-text);margin-right:4px;opacity:.35;transition:opacity .4s ease}.save-indicator.saved{opacity:.5}.save-indicator .save-tick{color:var(--color-time-recent);display:block}.save-indicator .save-spin{display:none;color:var(--color-dim);animation:save-rotate 1.5s cubic-bezier(.4,0,.2,1) infinite}.save-indicator.saving .save-tick{display:none}.save-indicator.saving .save-spin{display:block}.save-indicator.saving{opacity:.5}@keyframes save-rotate{to{transform:rotate(1turn)}}.tmx-toast{box-shadow:0 25px 50px -12px rgba(0,0,0,.5)}.light .tmx-toast{box-shadow:0 25px 50px -12px rgba(0,0,0,.25),0 0 0 1px rgba(0,0,0,.05)}.drop-highlight{border-radius:.25rem;background-color:rgba(59,130,246,.1);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(59,130,246,.3)}.project-drop-line{height:2px;margin:.125rem .5rem;border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.plugin-chevron.collapsed,.project-chevron.collapsed svg{transform:rotate(-90deg)}.tmx-scroll{scrollbar-width:thin;scrollbar-color:transparent transparent}.tmx-scroll::-webkit-scrollbar{width:10px}.tmx-scroll::-webkit-scrollbar-track{background:transparent}.tmx-scroll::-webkit-scrollbar-thumb{background:transparent;border:2px solid transparent;border-radius:9999px;-webkit-transition:background-color .2s ease,border-color .2s ease;transition:background-color .2s ease,border-color .2s ease}.tmx-scroll.is-scrolling,.tmx-scroll:hover{scrollbar-color:color-mix(in srgb,var(--color-muted) 78%,transparent) color-mix(in srgb,var(--color-sidebar) 70%,transparent)}.tmx-scroll.is-scrolling::-webkit-scrollbar-track,.tmx-scroll:hover::-webkit-scrollbar-track{background:color-mix(in srgb,var(--color-sidebar) 70%,transparent)}.tmx-scroll.is-scrolling::-webkit-scrollbar-thumb,.tmx-scroll:hover::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--color-muted) 78%,transparent);border-color:color-mix(in srgb,var(--color-sidebar) 70%,transparent)}.tmx-scroll.is-scrolling::-webkit-scrollbar-thumb:hover,.tmx-scroll:hover::-webkit-scrollbar-thumb:hover{background:color-mix(in srgb,var(--color-subtle) 85%,transparent)}.prompt-autocomplete{position:fixed;width:340px;background:var(--color-raised);border:1px solid var(--color-muted);border-radius:10px;box-shadow:0 20px 40px -8px rgba(0,0,0,.5);z-index:100;display:flex;flex-direction:column;overflow:hidden;animation:pa-in .15s ease}@keyframes pa-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.pa-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid color-mix(in srgb,var(--color-muted) 50%,transparent)}.pa-label{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--color-subtle)}.pa-query{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--color-dim);background:color-mix(in srgb,var(--color-muted) 40%,transparent);padding:2px 6px;border-radius:4px}.pa-list{overflow-y:auto;padding:4px;max-height:184px}.pa-item{padding:8px 10px;border-radius:6px;cursor:pointer;transition:background-color .1s}.pa-item:hover,.pa-selected{background:color-mix(in srgb,var(--color-muted) 40%,transparent)}.pa-name{font-size:13px;font-weight:500;color:var(--color-text)}.pa-name,.pa-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pa-text{font-size:11px;color:var(--color-subtle);margin-top:2px}.pa-item mark{background:rgba(59,130,246,.25);color:inherit;border-radius:2px;padding:0 1px}.pa-footer{display:flex;gap:12px;justify-content:center;padding:6px 12px;border-top:1px solid color-mix(in srgb,var(--color-muted) 50%,transparent);font-size:10px;color:var(--color-subtle)}.pa-footer kbd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;padding:1px 4px;border-radius:3px;background:color-mix(in srgb,var(--color-muted) 50%,transparent);color:var(--color-dim)}.pa-empty{padding:20px 12px;text-align:center;font-size:13px;color:var(--color-subtle)}.pa-hint{padding:0 12px 12px;text-align:center;font-size:11px;color:var(--color-border)}.pa-hint kbd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:color-mix(in srgb,var(--color-muted) 50%,transparent);padding:1px 4px;border-radius:3px}.empty\:hidden:empty{display:none}.hover\:scale-125:hover{--tw-scale-x:1.25;--tw-scale-y:1.25;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-slate-500:hover{--tw-border-opacity:1;border-color:rgb(100 116 139/var(--tw-border-opacity,1))}.hover\:bg-amber-500\/20:hover{background-color:rgba(245,158,11,.2)}.hover\:bg-blue-500:hover{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.hover\:bg-blue-500\/20:hover{background-color:rgba(59,130,246,.2)}.hover\:bg-red-500:hover{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.hover\:bg-slate-700:hover{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.hover\:bg-slate-700\/50:hover{background-color:rgba(51,65,85,.5)}.hover\:bg-slate-700\/60:hover{background-color:rgba(51,65,85,.6)}.hover\:bg-slate-700\/70:hover{background-color:rgba(51,65,85,.7)}.hover\:bg-slate-800\/30:hover{background-color:rgba(30,41,59,.3)}.hover\:bg-slate-800\/40:hover{background-color:rgba(30,41,59,.4)}.hover\:bg-slate-800\/50:hover{background-color:rgba(30,41,59,.5)}.hover\:text-amber-300:hover{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.hover\:text-blue-300:hover{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.hover\:text-emerald-300:hover{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.hover\:text-emerald-400:hover{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.hover\:text-indigo-400:hover{--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hover\:text-slate-200:hover{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.hover\:text-slate-300:hover{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.hover\:text-slate-400:hover{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.hover\:ring-2:hover{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.hover\:ring-slate-500:hover{--tw-ring-opacity:1;--tw-ring-color:rgb(100 116 139/var(--tw-ring-opacity,1))}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.focus\:border-slate-600\/60:focus{border-color:rgba(71,85,105,.6)}.focus\:bg-slate-800\/80:focus{background-color:rgba(30,41,59,.8)}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-blue-500\/30:focus{--tw-ring-color:rgba(59,130,246,.3)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-slate-600:disabled{--tw-bg-opacity:1;background-color:rgb(71 85 105/var(--tw-bg-opacity,1))}.disabled\:text-slate-400:disabled{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.group:hover .group-hover\:opacity-100{opacity:1}
|
package/server.js
CHANGED
|
@@ -88,6 +88,63 @@ const server = http.createServer((req, res) => {
|
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// Codex notify hook endpoint — deterministic turn-complete signal
|
|
92
|
+
if (req.method === 'POST' && req.url === '/hook/codex/stop') {
|
|
93
|
+
let body = '';
|
|
94
|
+
req.on('data', chunk => { body += chunk; if (body.length > 1e5) req.destroy(); });
|
|
95
|
+
req.on('end', () => {
|
|
96
|
+
try {
|
|
97
|
+
const payload = JSON.parse(body);
|
|
98
|
+
const threadId = payload['thread-id'];
|
|
99
|
+
if (threadId) {
|
|
100
|
+
const allSessions = sessions.getSessions();
|
|
101
|
+
for (const [id, s] of allSessions) {
|
|
102
|
+
if (s.sessionToken === threadId) {
|
|
103
|
+
console.log(`[codex] hook stop session=${id.slice(0,8)} thread=${threadId.slice(0,8)}`);
|
|
104
|
+
sessions.broadcast({ type: 'session.status', id, working: false, source: 'hook' });
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
res.writeHead(200).end('{}');
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Claude Code hook endpoints — deterministic start/stop/idle signals
|
|
116
|
+
if (req.method === 'POST' && req.url.startsWith('/hook/claude/')) {
|
|
117
|
+
let body = '';
|
|
118
|
+
req.on('data', chunk => { body += chunk; if (body.length > 1e5) req.destroy(); });
|
|
119
|
+
req.on('end', () => {
|
|
120
|
+
try {
|
|
121
|
+
const payload = JSON.parse(body);
|
|
122
|
+
const route = req.url.slice('/hook/claude/'.length);
|
|
123
|
+
const sessionId = payload.session_id;
|
|
124
|
+
// console.log(`[claude] hook ${route} session=${sessionId?.slice(0,8) || '?'}`);
|
|
125
|
+
if (sessionId) {
|
|
126
|
+
const allSessions = sessions.getSessions();
|
|
127
|
+
let clideckId = null;
|
|
128
|
+
for (const [id, s] of allSessions) {
|
|
129
|
+
if (s.sessionToken === sessionId) { clideckId = id; break; }
|
|
130
|
+
}
|
|
131
|
+
if (clideckId) {
|
|
132
|
+
if (route === 'start') {
|
|
133
|
+
sessions.broadcast({ type: 'session.status', id: clideckId, working: true, source: 'hook' });
|
|
134
|
+
} else if (route === 'stop' || route === 'idle') {
|
|
135
|
+
sessions.broadcast({ type: 'session.status', id: clideckId, working: false, source: 'hook' });
|
|
136
|
+
} else if (route === 'menu') {
|
|
137
|
+
// PreToolUse: trigger screen capture — detectMenu will set idle if a choice menu is visible
|
|
138
|
+
setTimeout(() => sessions.broadcast({ type: 'screen.capture', id: clideckId }), 500);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
143
|
+
res.writeHead(200).end('{}');
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
91
148
|
// OpenCode plugin bridge events
|
|
92
149
|
if (req.method === 'POST' && req.url === '/opencode-events') {
|
|
93
150
|
let body = '';
|
|
@@ -125,7 +182,18 @@ const server = http.createServer((req, res) => {
|
|
|
125
182
|
} catch { res.writeHead(500).end(); }
|
|
126
183
|
});
|
|
127
184
|
|
|
128
|
-
const
|
|
185
|
+
const allowedOrigins = new Set([
|
|
186
|
+
`http://localhost:${PORT}`, `http://127.0.0.1:${PORT}`,
|
|
187
|
+
`http://[::1]:${PORT}`, `http://${HOST}:${PORT}`,
|
|
188
|
+
]);
|
|
189
|
+
const wss = new WebSocketServer({
|
|
190
|
+
server,
|
|
191
|
+
verifyClient: ({ req }) => {
|
|
192
|
+
const origin = req.headers.origin;
|
|
193
|
+
if (!origin) return true; // non-browser clients (curl, etc.)
|
|
194
|
+
return allowedOrigins.has(origin);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
129
197
|
wss.on('connection', onConnection);
|
|
130
198
|
|
|
131
199
|
const activity = require('./activity');
|
package/sessions.js
CHANGED
|
@@ -277,10 +277,16 @@ function input(msg) {
|
|
|
277
277
|
activity.trackIn(msg.id, data.length);
|
|
278
278
|
transcript.trackInput(msg.id, data);
|
|
279
279
|
sessions.get(msg.id)?.pty.write(data);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
const s = sessions.get(msg.id);
|
|
281
|
+
if (!s) return;
|
|
282
|
+
// Menu choice selected → back to working (Enter or digit keys only)
|
|
283
|
+
if (s._menuKey && !s.working && (data === '\r' || /^[1-9]$/.test(data))) {
|
|
284
|
+
s._menuKey = '';
|
|
285
|
+
broadcast({ type: 'session.menu', id: msg.id, choices: [] });
|
|
286
|
+
broadcast({ type: 'session.status', id: msg.id, working: true, source: 'menu-input' });
|
|
287
|
+
}
|
|
288
|
+
if (data === '\x1b' && s.working) {
|
|
289
|
+
telemetry.startEscIdle(msg.id);
|
|
284
290
|
}
|
|
285
291
|
}
|
|
286
292
|
function resize(msg) { sessions.get(msg.id)?.pty.resize(msg.cols, msg.rows); }
|
|
@@ -315,15 +321,14 @@ function close(msg, cfg) {
|
|
|
315
321
|
// Uses resume command if available, otherwise re-launches the original command.
|
|
316
322
|
function restart(msg, ws, cfg) {
|
|
317
323
|
const id = msg.id;
|
|
318
|
-
console.log('[restart] received', { id, themeId: msg.themeId });
|
|
324
|
+
// console.log('[restart] received', { id, themeId: msg.themeId });
|
|
319
325
|
const s = sessions.get(id);
|
|
320
|
-
if (!s) {
|
|
326
|
+
if (!s) { ws.send(JSON.stringify({ type: 'session.restarted', id, error: 'not found' })); return; }
|
|
321
327
|
const cmd = cfg.commands.find(c => c.id === s.commandId);
|
|
322
|
-
if (!cmd) {
|
|
328
|
+
if (!cmd) { ws.send(JSON.stringify({ type: 'session.restarted', id, error: 'command missing' })); return; }
|
|
323
329
|
|
|
324
330
|
const themeId = msg.themeId || s.themeId;
|
|
325
331
|
const canResume = cmd.canResume && cmd.resumeCommand && s.sessionToken;
|
|
326
|
-
console.log('[restart] canResume=', canResume, 'token=', s.sessionToken?.slice(0,12), 'cmd=', cmd.command);
|
|
327
332
|
|
|
328
333
|
let parts;
|
|
329
334
|
if (canResume) {
|
|
@@ -331,7 +336,6 @@ function restart(msg, ws, cfg) {
|
|
|
331
336
|
} else {
|
|
332
337
|
parts = parseCommand(cmd.command);
|
|
333
338
|
}
|
|
334
|
-
console.log('[restart] parts=', parts);
|
|
335
339
|
|
|
336
340
|
const savedToken = s.sessionToken;
|
|
337
341
|
const { name, cwd, commandId, projectId } = s;
|
|
@@ -341,19 +345,16 @@ function restart(msg, ws, cfg) {
|
|
|
341
345
|
opencodeBridge.clear(id);
|
|
342
346
|
transcript.clear(id);
|
|
343
347
|
|
|
344
|
-
console.log('[restart] killing old pty');
|
|
345
348
|
s.pty.kill();
|
|
346
349
|
sessions.delete(id);
|
|
347
350
|
|
|
348
|
-
console.log('[restart] spawning new pty, themeId=', themeId, 'cwd=', cwd);
|
|
349
351
|
const err = spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken, projectId, msg.cols, msg.rows);
|
|
350
352
|
if (err) {
|
|
351
|
-
console.error('[restart]
|
|
353
|
+
console.error('[restart] spawn failed:', err.message);
|
|
352
354
|
broadcast({ type: 'session.restarted', id, error: err.message });
|
|
353
355
|
return;
|
|
354
356
|
}
|
|
355
357
|
|
|
356
|
-
console.log('[restart] SUCCESS, broadcasting session.restarted');
|
|
357
358
|
broadcast({ type: 'session.restarted', id, resumed: !!canResume });
|
|
358
359
|
}
|
|
359
360
|
|
package/telemetry-receiver.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
const ioActivity = require('./activity');
|
|
6
6
|
const activity = new Map(); // sessionId → has received events
|
|
7
|
+
const lastEvent = new Map(); // sessionId → last OTEL event name (+ kind)
|
|
7
8
|
const pendingSetup = new Map(); // sessionId → timer (waiting for first event)
|
|
8
9
|
const pendingIdle = new Map(); // sessionId → timer (PTY silence → idle)
|
|
10
|
+
const codexMenuPoll = new Map(); // sessionId → interval (polling for menu after response.completed)
|
|
9
11
|
const escPendingIdle = new Map(); // sessionId → timer (Esc interrupt → confirm idle after output silence)
|
|
10
12
|
const escSuppressUntil = new Map(); // sessionId → ts (briefly ignore telemetry reassertions after Esc)
|
|
11
13
|
let broadcastFn = null;
|
|
@@ -74,18 +76,40 @@ function handleLogs(req, res) {
|
|
|
74
76
|
|
|
75
77
|
// Debug telemetry logs — uncomment as needed, do not delete
|
|
76
78
|
// if (serviceName === 'claude-code' && eventName) console.log(`[telemetry:claude] ${eventName}`);
|
|
77
|
-
// if (serviceName === 'codex_cli_rs' && eventName) console.log(`[telemetry:codex] ${eventName} session=${resolvedId.slice(0,8)}
|
|
79
|
+
// if (serviceName === 'codex_cli_rs' && eventName) console.log(`[telemetry:codex] ${eventName} ${attrs['event.kind'] ? 'kind=' + attrs['event.kind'] : ''} ${attrs['tool'] ? 'tool=' + attrs['tool'] : ''} session=${resolvedId.slice(0,8)}`);
|
|
78
80
|
// if (serviceName === 'gemini-cli' && eventName) console.log(`[telemetry:gemini] ${eventName}`);
|
|
79
81
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
+
// Track last event per session (used by menu detection validation)
|
|
83
|
+
if (eventName) lastEvent.set(resolvedId, eventName + (attrs['event.kind'] ? ':' + attrs['event.kind'] : ''));
|
|
84
|
+
|
|
85
|
+
// Status: user_prompt → working
|
|
86
|
+
// Claude uses hooks; Codex uses notify hook; Gemini uses PTY heuristic
|
|
87
|
+
const startEvents = new Set(['gemini_cli.user_prompt', 'codex.user_prompt']);
|
|
82
88
|
if (startEvents.has(eventName)) {
|
|
83
89
|
cancelPendingIdle(resolvedId);
|
|
84
90
|
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
85
|
-
|
|
91
|
+
// Gemini uses PTY silence heuristic for idle; Codex idle comes from notify hook
|
|
92
|
+
if (serviceName !== 'codex_cli_rs') startPendingIdle(resolvedId, serviceName);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Codex: response.completed → poll for menu until found or timeout
|
|
96
|
+
if (eventName === 'codex.sse_event' && attrs['event.kind'] === 'response.completed') {
|
|
97
|
+
startCodexMenuPoll(resolvedId);
|
|
98
|
+
}
|
|
99
|
+
// Codex: tool_decision → user approved, cancel menu poll, back to working
|
|
100
|
+
if (eventName === 'codex.tool_decision') {
|
|
101
|
+
cancelCodexMenuPoll(resolvedId);
|
|
102
|
+
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
103
|
+
}
|
|
104
|
+
// Codex: user_prompt or next sse_event cancels menu poll
|
|
105
|
+
if ((eventName === 'codex.user_prompt' || (eventName === 'codex.sse_event' && attrs['event.kind'] !== 'response.completed'))) {
|
|
106
|
+
cancelCodexMenuPoll(resolvedId);
|
|
86
107
|
}
|
|
87
108
|
|
|
88
|
-
|
|
109
|
+
// Codex: use conversation.id (maps to thread-id in notify hook)
|
|
110
|
+
const agentSessionId = serviceName === 'codex_cli_rs'
|
|
111
|
+
? attrs['conversation.id']
|
|
112
|
+
: (attrs['session.id'] || attrs['conversation.id']);
|
|
89
113
|
if (agentSessionId && sess) {
|
|
90
114
|
// Prefer interactive session ID (Gemini sends non-interactive init events first)
|
|
91
115
|
const dominated = sess.sessionToken && attrs['interactive'] === true;
|
|
@@ -124,16 +148,12 @@ function cancelPendingSetup(sessionId) {
|
|
|
124
148
|
}
|
|
125
149
|
|
|
126
150
|
// PTY activity monitor: 2s silent → idle, 2s active or user_prompt → working.
|
|
151
|
+
// Used by Gemini only — Claude uses hooks, Codex uses sse_event completion.
|
|
127
152
|
// Agent working indicators in PTY output.
|
|
128
|
-
const CLAUDE_WORKING_RE = /[✳✽✢✻·]|Working…|thinking/;
|
|
129
|
-
const CODEX_WORKING_RE = /Working|•/;
|
|
130
153
|
const GEMINI_WORKING_RE = /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/;
|
|
131
154
|
|
|
132
155
|
function startPendingIdle(id, agent) {
|
|
133
156
|
if (pendingIdle.has(id)) return; // already monitoring
|
|
134
|
-
const isClaude = agent === 'claude-code';
|
|
135
|
-
const isCodex = agent === 'codex_cli_rs';
|
|
136
|
-
const isGemini = agent === 'gemini-cli';
|
|
137
157
|
let isIdle = false;
|
|
138
158
|
let activeStart = 0;
|
|
139
159
|
const check = setInterval(() => {
|
|
@@ -145,9 +165,7 @@ function startPendingIdle(id, agent) {
|
|
|
145
165
|
// Agent override: if recent output has spinner/working chars, not silent
|
|
146
166
|
if (silent && (Date.now() - lastOut) < 2000) {
|
|
147
167
|
const chunk = ioActivity.lastChunk(id);
|
|
148
|
-
if (
|
|
149
|
-
if (isCodex && CODEX_WORKING_RE.test(chunk)) silent = false;
|
|
150
|
-
if (isGemini && GEMINI_WORKING_RE.test(chunk)) silent = false;
|
|
168
|
+
if (GEMINI_WORKING_RE.test(chunk)) silent = false;
|
|
151
169
|
}
|
|
152
170
|
if (silent && !isIdle) {
|
|
153
171
|
isIdle = true;
|
|
@@ -171,6 +189,22 @@ function cancelPendingIdle(id) {
|
|
|
171
189
|
if (timer) { clearInterval(timer); pendingIdle.delete(id); }
|
|
172
190
|
}
|
|
173
191
|
|
|
192
|
+
// Codex: after response.completed, poll screen capture every 500ms for up to 3s
|
|
193
|
+
function startCodexMenuPoll(id) {
|
|
194
|
+
cancelCodexMenuPoll(id);
|
|
195
|
+
const started = Date.now();
|
|
196
|
+
const poll = setInterval(() => {
|
|
197
|
+
if (Date.now() - started > 3000) { cancelCodexMenuPoll(id); return; }
|
|
198
|
+
broadcastFn?.({ type: 'screen.capture', id });
|
|
199
|
+
}, 500);
|
|
200
|
+
codexMenuPoll.set(id, poll);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cancelCodexMenuPoll(id) {
|
|
204
|
+
const timer = codexMenuPoll.get(id);
|
|
205
|
+
if (timer) { clearInterval(timer); codexMenuPoll.delete(id); }
|
|
206
|
+
}
|
|
207
|
+
|
|
174
208
|
function startEscIdle(id) {
|
|
175
209
|
cancelEscIdle(id);
|
|
176
210
|
const started = Date.now();
|
|
@@ -202,16 +236,20 @@ function cancelEscIdle(id) {
|
|
|
202
236
|
|
|
203
237
|
function clear(id) {
|
|
204
238
|
activity.delete(id);
|
|
239
|
+
lastEvent.delete(id);
|
|
205
240
|
cancelPendingIdle(id);
|
|
241
|
+
cancelCodexMenuPoll(id);
|
|
206
242
|
cancelEscIdle(id);
|
|
207
243
|
escSuppressUntil.delete(id);
|
|
208
244
|
const pending = pendingSetup.get(id);
|
|
209
245
|
if (pending) { clearTimeout(pending.timer); pendingSetup.delete(id); }
|
|
210
246
|
}
|
|
211
247
|
|
|
248
|
+
function getLastEvent(id) { return lastEvent.get(id) || ''; }
|
|
249
|
+
|
|
212
250
|
// Returns true if we've received telemetry events for this session
|
|
213
251
|
function hasEvents(id) {
|
|
214
252
|
return activity.has(id);
|
|
215
253
|
}
|
|
216
254
|
|
|
217
|
-
module.exports = { init, handleLogs, clear, hasEvents, watchSession, startPendingIdle, startEscIdle };
|
|
255
|
+
module.exports = { init, handleLogs, clear, hasEvents, getLastEvent, cancelCodexMenuPoll, watchSession, startPendingIdle, startEscIdle };
|
package/transcript.js
CHANGED
|
@@ -321,16 +321,18 @@ const MENU_CHOICE_RE = /^\s*(?:[│❯›●•]\s+)*(\d+)\.\s+(.+)$/;
|
|
|
321
321
|
function detectMenu(lines, presetId) {
|
|
322
322
|
const marker = MENU_MARKERS[presetId];
|
|
323
323
|
if (!marker) return null;
|
|
324
|
+
// Only scan the bottom 40 lines — menus are always near the visible area
|
|
325
|
+
const scanStart = Math.max(0, lines.length - 40);
|
|
324
326
|
let footerIdx = -1;
|
|
325
|
-
for (let i = lines.length - 1; i >=
|
|
327
|
+
for (let i = lines.length - 1; i >= scanStart; i--) {
|
|
326
328
|
if (/\besc\b|\(esc\)/i.test(lines[i])) { footerIdx = MENU_CHOICE_RE.test(lines[i]) ? i + 1 : i; break; }
|
|
327
329
|
}
|
|
328
330
|
if (footerIdx < 0) return null;
|
|
329
331
|
const choices = [];
|
|
330
|
-
for (let i = footerIdx - 1; i >=
|
|
332
|
+
for (let i = footerIdx - 1; i >= scanStart; i--) {
|
|
331
333
|
if (!lines[i].trim() || /^[│\s]+$/.test(lines[i])) continue;
|
|
332
334
|
const m = lines[i].match(MENU_CHOICE_RE);
|
|
333
|
-
if (!m) break;
|
|
335
|
+
if (!m) { if (/^\s{2,}\S/.test(lines[i])) continue; break; }
|
|
334
336
|
if (choices.length && +m[1] >= +choices[0].value) break;
|
|
335
337
|
choices.unshift({ value: m[1], label: m[2].trim(), selected: marker.test(lines[i]) });
|
|
336
338
|
}
|