clideck 1.26.2 → 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 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 demo roles (Programmer, Reviewer, Product Manager) and 3 starter prompts in the prompt library to get going quickly.
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. Works with any LLM provider (Anthropic, OpenAI, Google, Groq, xAI, Mistral, and more). Notifies you when work is complete or blocked.
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
@@ -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": "Built-in (no configuration needed)",
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
- detected = true;
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
- const choices = require('./transcript').detectMenu(msg.lines, sess.presetId);
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);
@@ -269,13 +281,29 @@ function onConnection(ws) {
269
281
 
270
282
  case 'dirs.list': {
271
283
  const target = msg.path || cfg.defaultPath;
272
- const result = listDirs(target);
284
+ const result = listDirs(target, !!msg.showHidden);
273
285
  const entries = Array.isArray(result) ? result : [];
274
286
  const error = result.error || undefined;
275
287
  ws.send(JSON.stringify({ type: 'dirs', path: target, entries, error }));
276
288
  break;
277
289
  }
278
290
 
291
+ case 'dirs.mkdir': {
292
+ const name = (msg.name || '').trim();
293
+ if (!name || name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
294
+ ws.send(JSON.stringify({ type: 'dirs.mkdir', success: false, error: 'Invalid folder name' }));
295
+ break;
296
+ }
297
+ const dirPath = join(msg.parent, name);
298
+ try {
299
+ mkdirSync(dirPath);
300
+ ws.send(JSON.stringify({ type: 'dirs.mkdir', success: true, path: dirPath }));
301
+ } catch (e) {
302
+ ws.send(JSON.stringify({ type: 'dirs.mkdir', success: false, error: e.message }));
303
+ }
304
+ break;
305
+ }
306
+
279
307
  case 'plugin.settings.update':
280
308
  plugins.updateSetting(msg.pluginId, msg.key, msg.value);
281
309
  sessions.broadcast({ type: 'plugins', list: plugins.getInfo() });
@@ -362,15 +390,54 @@ function applyTelemetryConfig(preset) {
362
390
  const home = os.homedir();
363
391
 
364
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
+
365
417
  if (preset.presetId === 'codex') {
366
418
  const configPath = join(home, '.codex', 'config.toml');
367
419
  let content = '';
368
420
  if (existsSync(configPath)) content = readFileSync(configPath, 'utf8');
369
- if (content.includes('[otel]')) return { success: true, message: 'Already configured' };
370
- const section = `\n[otel]\nexporter = { otlp-http = { endpoint = "http://localhost:${port}/v1/logs", protocol = "json" } }\n`;
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
+ }
371
438
  mkdirSync(dirname(configPath), { recursive: true });
372
- writeFileSync(configPath, content.trimEnd() + '\n' + section);
373
- return { success: true, message: 'Added [otel] section to ~/.codex/config.toml' };
439
+ writeFileSync(configPath, content);
440
+ return { success: true, message: 'Added otel + notify to ~/.codex/config.toml' };
374
441
  }
375
442
 
376
443
  if (preset.presetId === 'gemini-cli') {
@@ -415,14 +482,31 @@ function removeTelemetryConfig(preset) {
415
482
  const home = os.homedir();
416
483
 
417
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
+
418
502
  if (preset.presetId === 'codex') {
419
503
  const configPath = join(home, '.codex', 'config.toml');
420
504
  if (!existsSync(configPath)) return { success: true, message: 'No config file to clean' };
421
505
  let content = readFileSync(configPath, 'utf8');
422
- // Remove [otel] section and everything until the next section or EOF
423
506
  content = content.replace(/\n?\[otel\][^\[]*/, '');
507
+ content = content.replace(/\n?notify\s*=\s*\[.*?notify-helper.*?\]\s*/g, '');
424
508
  writeFileSync(configPath, content.trimEnd() + '\n');
425
- return { success: true, message: 'Removed [otel] section from ~/.codex/config.toml' };
509
+ return { success: true, message: 'Removed otel + notify from ~/.codex/config.toml' };
426
510
  }
427
511
 
428
512
  if (preset.presetId === 'gemini-cli') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.26.2",
3
+ "version": "1.27.0",
4
4
  "description": "One screen for all your AI coding agents — run, monitor, and manage multiple CLI agents from a single browser tab",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -461,7 +461,17 @@
461
461
  <!-- Folder picker -->
462
462
  <div id="folder-picker" class="absolute inset-0 z-[300] bg-black/60 backdrop-blur-sm hidden items-center justify-center">
463
463
  <div class="bg-slate-800 border border-slate-600 rounded-xl shadow-2xl shadow-black/50 w-[420px] max-h-[460px] flex flex-col">
464
- <div class="px-4 py-3 border-b border-slate-700 text-sm font-semibold">Choose Directory</div>
464
+ <div class="px-4 py-3 border-b border-slate-700 flex items-center justify-between">
465
+ <span class="text-sm font-semibold">Choose Directory</span>
466
+ <div class="flex items-center gap-2">
467
+ <button id="fp-new-folder" class="p-1 rounded hover:bg-slate-700 text-slate-400 hover:text-slate-200 transition-colors" title="New folder">
468
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
469
+ </button>
470
+ <button id="fp-toggle-hidden" class="p-1 rounded hover:bg-slate-700 text-slate-500 transition-colors" title="Show hidden files">
471
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
472
+ </button>
473
+ </div>
474
+ </div>
465
475
  <div id="fp-path" class="px-4 py-2 text-xs text-slate-400 border-b border-slate-700 break-all"></div>
466
476
  <div id="fp-listing" class="flex-1 overflow-y-auto py-1 min-h-[200px]"></div>
467
477
  <div class="px-4 py-3 border-t border-slate-700 flex justify-end gap-2">
package/public/js/app.js CHANGED
@@ -1,9 +1,9 @@
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';
6
- import { handleDirsResponse, openFolderPicker } from './folder-picker.js';
6
+ import { handleDirsResponse, handleMkdirResponse, openFolderPicker } from './folder-picker.js';
7
7
  import { confirmClose } from './confirm.js';
8
8
  import { applyTheme } from './profiles.js';
9
9
  import { toggleMode, applyMode } from './color-mode.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);
@@ -145,6 +157,9 @@ function connect() {
145
157
  case 'dirs':
146
158
  handleDirsResponse(msg);
147
159
  break;
160
+ case 'dirs.mkdir':
161
+ handleMkdirResponse(msg);
162
+ break;
148
163
  case 'session.theme': {
149
164
  const entry = state.terms.get(msg.id);
150
165
  if (entry) {
@@ -179,16 +194,15 @@ function connect() {
179
194
  const actionsEl = toast.querySelector('.setup-actions');
180
195
  if (msg.success) {
181
196
  const sid = toast.dataset.sessionId;
182
- const cmdId = toast.dataset.commandId;
183
197
  actionsEl.innerHTML = `
184
198
  <div class="flex-1 flex items-center gap-1.5 text-xs text-emerald-400">
185
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>
186
200
  Configured
187
201
  </div>
188
- <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>
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>` : ''}
189
203
  <button class="dismiss-btn px-3 py-2 text-xs text-slate-500 hover:text-slate-300 transition-colors">Dismiss</button>`;
190
204
  actionsEl.querySelector('.dismiss-btn').onclick = () => toast.remove();
191
- actionsEl.querySelector('.restart-btn').onclick = () => {
205
+ if (sid) actionsEl.querySelector('.restart-btn').onclick = () => {
192
206
  const entry = state.terms.get(sid);
193
207
  send({ type: 'session.restart', id: sid, themeId: entry?.themeId, cols: entry?.term?.cols, rows: entry?.term?.rows });
194
208
  toast.remove();
@@ -384,6 +398,7 @@ document.querySelectorAll('.filter-tab').forEach(btn => {
384
398
 
385
399
  // Telemetry setup notification — shown once per agent type
386
400
  const shownSetup = new Set();
401
+ document.addEventListener('clideck:setup', (e) => showTelemetrySetup(e.detail.commandId, null));
387
402
  function showTelemetrySetup(commandId, sessionId) {
388
403
  const cmd = state.cfg.commands.find(c => c.id === commandId);
389
404
  if (!cmd) return;
@@ -400,7 +415,7 @@ function showTelemetrySetup(commandId, sessionId) {
400
415
  const [desc, ...codeParts] = setupText.split('\n\n');
401
416
  const code = codeParts.join('\n\n');
402
417
  const auto = preset.telemetryAutoSetup;
403
- const iconSrc = preset.icon?.startsWith('/') ? preset.icon : null;
418
+ const iconSrc = preset.icon?.startsWith('/') ? resolveIconPath(preset.icon) : null;
404
419
  const title = preset.bridge ? 'Bridge Plugin' : 'Status Tracking';
405
420
 
406
421
  const toast = document.createElement('div');
@@ -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
- const missing = isPresetMissing(p);
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: preset.presetId === 'claude-code',
96
- telemetryStatus: preset.presetId === 'claude-code' ? { ok: true } : null,
105
+ telemetryEnabled: false,
106
+ telemetryStatus: null,
97
107
  bridge: preset.bridge,
98
108
  };
99
109
  state.cfg.commands.push(cmd);
@@ -124,13 +134,13 @@ export function openCreator() {
124
134
  ${(state.cfg.projects?.length) ? `
125
135
  <input type="hidden" id="creator-project" value="">
126
136
  <button type="button" id="creator-project-trigger" class="w-full px-3 py-1.5 text-xs bg-slate-900 border border-slate-700 rounded-md text-slate-400 text-left flex items-center justify-between outline-none hover:border-slate-500 transition-colors cursor-pointer mb-2">
127
- <span id="creator-project-label">Select project</span>
137
+ <span id="creator-project-label">Select project <span class="opacity-40">- optional</span></span>
128
138
  <span class="text-slate-600 ml-2">&#9662;</span>
129
139
  </button>` : ''}
130
140
  ${(state.cfg.roles?.length) ? `
131
141
  <input type="hidden" id="creator-role" value="">
132
142
  <button type="button" id="creator-role-trigger" class="w-full px-3 py-1.5 text-xs bg-slate-900 border border-slate-700 rounded-md text-slate-400 text-left flex items-center justify-between outline-none hover:border-slate-500 transition-colors cursor-pointer mb-2">
133
- <span id="creator-role-label">Select role</span>
143
+ <span id="creator-role-label">Select role <span class="opacity-40">- optional</span></span>
134
144
  <span class="text-slate-600 ml-2">&#9662;</span>
135
145
  </button>` : ''}
136
146
  <input id="creator-name" type="text" maxlength="35" placeholder="Session / Agent name"
@@ -199,7 +209,7 @@ export function openCreator() {
199
209
  if (!item) return;
200
210
  hidden.value = item.dataset.value;
201
211
  const proj = projects.find(p => p.id === item.dataset.value);
202
- label.textContent = proj ? proj.name : 'Select project';
212
+ label.innerHTML = proj ? esc(proj.name) : 'Select project <span class="opacity-40">- optional</span>';
203
213
  // Auto-set working directory from project path
204
214
  if (proj?.path) cwdInput.value = proj.path;
205
215
  else cwdInput.value = defaultPath;
@@ -252,7 +262,7 @@ export function openCreator() {
252
262
  if (!item) return;
253
263
  hidden.value = item.dataset.value;
254
264
  const roleName = item.dataset.name;
255
- label.textContent = roleName || 'Select role';
265
+ label.innerHTML = roleName ? esc(roleName) : 'Select role <span class="opacity-40">- optional</span>';
256
266
  // Auto-fill session name from role name (only if user hasn't typed a custom name)
257
267
  if (roleName && (!nameInput.value.trim() || nameInput.dataset.autoFilled === '1')) {
258
268
  nameInput.value = roleName;
@@ -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);
@@ -19,9 +19,12 @@ const overlay = document.getElementById('folder-picker');
19
19
  const pathBar = document.getElementById('fp-path');
20
20
  const listing = document.getElementById('fp-listing');
21
21
  const selectBtn = document.getElementById('fp-select');
22
+ const hiddenBtn = document.getElementById('fp-toggle-hidden');
23
+ const newFolderBtn = document.getElementById('fp-new-folder');
22
24
  let currentPath = '';
23
25
  let pendingPath = '';
24
26
  let onSelect = null;
27
+ let showHidden = false;
25
28
 
26
29
  export function openFolderPicker(startPath, callback) {
27
30
  currentPath = '';
@@ -35,6 +38,7 @@ export function closeFolderPicker() {
35
38
  overlay.classList.add('hidden');
36
39
  overlay.classList.remove('flex');
37
40
  onSelect = null;
41
+ closeNewFolderInput();
38
42
  }
39
43
 
40
44
  function navigate(path) {
@@ -42,7 +46,8 @@ function navigate(path) {
42
46
  pathBar.textContent = path;
43
47
  listing.innerHTML = '<div class="p-4 text-center text-slate-500 text-sm">Loading...</div>';
44
48
  selectBtn.disabled = true;
45
- send({ type: 'dirs.list', path });
49
+ closeNewFolderInput();
50
+ send({ type: 'dirs.list', path, showHidden });
46
51
  }
47
52
 
48
53
  export function handleDirsResponse(msg) {
@@ -62,12 +67,90 @@ export function handleDirsResponse(msg) {
62
67
  if (msg.entries.length === 0 && !html) {
63
68
  html = '<div class="p-4 text-center text-slate-500 text-sm">Empty directory</div>';
64
69
  }
65
- html += msg.entries.map(name =>
66
- `<div class="fp-item px-4 py-1.5 cursor-pointer hover:bg-slate-700 text-sm text-slate-200 transition-colors" data-path="${esc(joinChild(currentPath, name))}">${esc(name)}</div>`
67
- ).join('');
70
+ html += msg.entries.map(name => {
71
+ const dimClass = name.startsWith('.') ? ' text-slate-500' : ' text-slate-200';
72
+ return `<div class="fp-item px-4 py-1.5 cursor-pointer hover:bg-slate-700 text-sm${dimClass} transition-colors" data-path="${esc(joinChild(currentPath, name))}">${esc(name)}</div>`;
73
+ }).join('');
68
74
  listing.innerHTML = html;
69
75
  }
70
76
 
77
+ // --- Hidden files toggle ---
78
+
79
+ function updateHiddenBtn() {
80
+ hiddenBtn.classList.toggle('text-slate-200', showHidden);
81
+ hiddenBtn.classList.toggle('text-slate-500', !showHidden);
82
+ hiddenBtn.title = showHidden ? 'Hide hidden files' : 'Show hidden files';
83
+ }
84
+
85
+ hiddenBtn.addEventListener('click', () => {
86
+ showHidden = !showHidden;
87
+ updateHiddenBtn();
88
+ if (currentPath) navigate(currentPath);
89
+ });
90
+
91
+ // --- New folder inline input ---
92
+
93
+ let newFolderActive = false;
94
+
95
+ function closeNewFolderInput() {
96
+ if (!newFolderActive) return;
97
+ newFolderActive = false;
98
+ const row = listing.querySelector('.fp-new-folder-row');
99
+ if (row) row.remove();
100
+ }
101
+
102
+ function openNewFolderInput() {
103
+ if (newFolderActive || !currentPath) return;
104
+ newFolderActive = true;
105
+ const row = document.createElement('div');
106
+ row.className = 'fp-new-folder-row flex items-center gap-2 px-4 py-1.5';
107
+ row.innerHTML = `
108
+ <svg class="flex-shrink-0 text-slate-400" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
109
+ <input type="text" class="fp-new-folder-input flex-1 bg-slate-700 border border-slate-600 rounded px-2 py-0.5 text-sm text-slate-200 placeholder-slate-500 outline-none focus:border-blue-500 transition-colors" placeholder="Folder name" spellcheck="false" />
110
+ <button class="fp-new-folder-ok p-0.5 rounded hover:bg-slate-700 text-emerald-400 hover:text-emerald-300 transition-colors" title="Create">
111
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
112
+ </button>
113
+ <button class="fp-new-folder-no p-0.5 rounded hover:bg-slate-700 text-slate-400 hover:text-slate-300 transition-colors" title="Cancel">
114
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
115
+ </button>`;
116
+ listing.prepend(row);
117
+ const input = row.querySelector('.fp-new-folder-input');
118
+ input.focus();
119
+
120
+ function submit() {
121
+ const name = input.value.trim();
122
+ if (!name) { closeNewFolderInput(); return; }
123
+ input.disabled = true;
124
+ send({ type: 'dirs.mkdir', parent: currentPath, name });
125
+ }
126
+
127
+ input.addEventListener('keydown', (e) => {
128
+ if (e.key === 'Enter') { e.preventDefault(); submit(); }
129
+ if (e.key === 'Escape') { e.preventDefault(); closeNewFolderInput(); }
130
+ });
131
+ row.querySelector('.fp-new-folder-ok').addEventListener('click', submit);
132
+ row.querySelector('.fp-new-folder-no').addEventListener('click', closeNewFolderInput);
133
+ }
134
+
135
+ newFolderBtn.addEventListener('click', openNewFolderInput);
136
+
137
+ export function handleMkdirResponse(msg) {
138
+ if (!newFolderActive) return;
139
+ closeNewFolderInput();
140
+ if (msg.success) {
141
+ navigate(msg.path);
142
+ } else {
143
+ // Show error inline briefly
144
+ const err = document.createElement('div');
145
+ err.className = 'px-4 py-1.5 text-xs text-red-400';
146
+ err.textContent = msg.error || 'Failed to create folder';
147
+ listing.prepend(err);
148
+ setTimeout(() => err.remove(), 3000);
149
+ }
150
+ }
151
+
152
+ // --- Navigation and select ---
153
+
71
154
  listing.addEventListener('click', (e) => {
72
155
  const item = e.target.closest('.fp-item');
73
156
  if (item) navigate(item.dataset.path);
@@ -110,40 +110,14 @@ function telemetryPreset(cmd) {
110
110
  function integrationSection(c) {
111
111
  const preset = telemetryPreset(c);
112
112
  if (!preset) return '';
113
- const isClaude = preset.presetId === 'claude-code';
114
- const isBridge = !!preset.bridge;
115
- if (!preset.telemetryEnv && !isBridge) return '';
116
-
117
- const enabled = isClaude || !!c.telemetryEnabled;
118
- const title = 'CliDeck integration';
119
- const subtitle = '(live status &amp; 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> &mdash; ${esc(preset.telemetryConfigPath || '')}`
117
+ : `<span class="text-slate-500">Not configured</span> &mdash; enable agent to set up`;
136
118
  return `
137
119
  <div class="mt-3 pt-3 border-t border-slate-700/50">
138
- <label class="flex items-center justify-between text-sm text-slate-300 cursor-pointer select-none">
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: p.presetId === 'claude-code',
245
- telemetryStatus: p.presetId === 'claude-code' ? { ok: true } : null,
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
- if (e.target.classList.contains('agent-telemetry-toggle')) {
298
- const presetId = e.target.dataset.preset;
299
- send({ type: 'telemetry.configure', presetId, enable: e.target.checked });
300
- return; // config broadcast from server will re-render
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: isClaude ? true : (existing.telemetryEnabled || false),
465
- telemetryStatus: isClaude ? { ok: true } : (existing.telemetryStatus || null),
454
+ telemetryEnabled: existing.telemetryEnabled || false,
455
+ telemetryStatus: existing.telemetryStatus || null,
466
456
  bridge: existing.bridge,
467
457
  };
468
458
  });
@@ -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 _telemetryOnly = cmd?.presetId === 'claude-code' || cmd?.presetId === 'codex' || cmd?.presetId === 'gemini-cli';
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 && !_telemetryOnly) || !entry.term) return;
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 _idleTimer = null, _workTimer = null, _lastTyping = 0, _lastRender = 0;
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(() => { if (Date.now() - _lastTyping < 500) return; const entry = state.terms.get(id); if (entry) entry.lastRenderAt = Date.now(); if (_telemetryOnly) return; if (!_workTimer) _workTimer = setTimeout(() => { _workTimer = null; if (Date.now() - _lastRender < 500) setStatus(id, true); }, 1500); clearTimeout(_idleTimer); _idleTimer = setTimeout(() => { clearTimeout(_workTimer); _workTimer = null; setStatus(id, false); send({ type: 'session.statusReport', id, working: false }); }, 1500); });
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);
@@ -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\.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-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 wss = new WebSocketServer({ server });
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
- if (data === '\x1b') {
281
- const s = sessions.get(msg.id);
282
- console.log(`[esc] session=${msg.id.slice(0,8)} working=${s?.working}`);
283
- if (s?.working) telemetry.startEscIdle(msg.id);
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) { console.log('[restart] FAIL: session not found'); ws.send(JSON.stringify({ type: 'session.restarted', id, error: 'not found' })); return; }
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) { console.log('[restart] FAIL: command not found, commandId=', s.commandId); ws.send(JSON.stringify({ type: 'session.restarted', id, error: 'command missing' })); return; }
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] FAIL spawn:', err.message);
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
 
@@ -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)} working=${sessionsFn?.()?.get(resolvedId)?.working}`);
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
- // Status: user_prompt working + start PTY silence monitor for idle
81
- const startEvents = new Set(['user_prompt', 'gemini_cli.user_prompt', 'codex.user_prompt']);
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
- startPendingIdle(resolvedId, serviceName);
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
- const agentSessionId = attrs['session.id'] || attrs['conversation.id'];
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 (isClaude && CLAUDE_WORKING_RE.test(chunk)) silent = false;
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,22 +189,38 @@ 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();
177
211
  const ignoreUntil = started + 500;
178
- console.log(`[escIdle] start session=${id.slice(0,8)}`);
212
+ // console.log(`[escIdle] start session=${id.slice(0,8)}`);
179
213
  const check = setInterval(() => {
180
214
  const lastOut = ioActivity.lastOutputAt(id);
181
215
  const silence = Date.now() - Math.max(ignoreUntil, lastOut);
182
216
  const elapsed = Date.now() - started;
183
217
  if (elapsed > 10000) {
184
- console.log(`[escIdle] timeout session=${id.slice(0,8)} silence=${silence}ms`);
218
+ // console.log(`[escIdle] timeout session=${id.slice(0,8)} silence=${silence}ms`);
185
219
  cancelEscIdle(id);
186
220
  return;
187
221
  }
188
222
  if (silence >= 2000) {
189
- console.log(`[escIdle] idle session=${id.slice(0,8)} silence=${silence}ms`);
223
+ // console.log(`[escIdle] idle session=${id.slice(0,8)} silence=${silence}ms`);
190
224
  escSuppressUntil.set(id, Date.now() + 2000);
191
225
  cancelEscIdle(id);
192
226
  broadcastFn?.({ type: 'session.status', id, working: false, source: 'esc' });
@@ -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 >= 0; 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 >= 0; 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
  }
package/utils.js CHANGED
@@ -44,10 +44,10 @@ function resolveValidDir(dir) {
44
44
  return require('os').homedir();
45
45
  }
46
46
 
47
- function listDirs(path) {
47
+ function listDirs(path, showHidden) {
48
48
  try {
49
49
  return readdirSync(path, { withFileTypes: true })
50
- .filter(d => d.isDirectory() && !d.name.startsWith('.'))
50
+ .filter(d => d.isDirectory() && (showHidden || !d.name.startsWith('.')))
51
51
  .map(d => d.name)
52
52
  .sort();
53
53
  } catch (e) {