claude-code-session-manager 0.8.1 → 0.8.3

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.
Files changed (40) hide show
  1. package/README.md +66 -11
  2. package/dist/assets/{cssMode-DKTELvb6.js → cssMode-DyaNC2Cs.js} +1 -1
  3. package/dist/assets/{editor.main-Dx55Am4z.js → editor.main-BhSGi_Jw.js} +3 -3
  4. package/dist/assets/{freemarker2-CBdvn_u-.js → freemarker2-DZH3si5v.js} +1 -1
  5. package/dist/assets/{handlebars-B67ay2ue.js → handlebars-DvzTd6uL.js} +1 -1
  6. package/dist/assets/{html-002uK0_M.js → html-C5GmopAN.js} +1 -1
  7. package/dist/assets/{htmlMode-DsT8oVY_.js → htmlMode-DwnrHwx1.js} +1 -1
  8. package/dist/assets/index-BGshD4Pw.js +2976 -0
  9. package/dist/assets/index-DCK87t79.css +32 -0
  10. package/dist/assets/{javascript-Cfg-gFlu.js → javascript-JqHrxiCa.js} +1 -1
  11. package/dist/assets/{jsonMode-CCIKxANa.js → jsonMode-8rZcy09i.js} +1 -1
  12. package/dist/assets/{liquid-DewgYvox.js → liquid-ClpD_v7G.js} +1 -1
  13. package/dist/assets/{lspLanguageFeatures-BcMPMUo0.js → lspLanguageFeatures-u0WgQBQz.js} +1 -1
  14. package/dist/assets/{mdx-BGrrIvjV.js → mdx-DtViUgdm.js} +1 -1
  15. package/dist/assets/{python-CVhAv32T.js → python-CaAvhRGm.js} +1 -1
  16. package/dist/assets/{razor-DteXtrPO.js → razor-saGNVU7l.js} +1 -1
  17. package/dist/assets/{tsMode-DKeWRYvl.js → tsMode-HZwWTCj8.js} +1 -1
  18. package/dist/assets/{typescript-Dl1KPrAp.js → typescript-BInV4PNE.js} +1 -1
  19. package/dist/assets/{xml-DdyOGE0N.js → xml-tgO806YR.js} +1 -1
  20. package/dist/assets/{yaml-BwFXDW6t.js → yaml-CHApZArv.js} +1 -1
  21. package/dist/index.html +2 -2
  22. package/package.json +1 -1
  23. package/src/main/config.cjs +93 -19
  24. package/src/main/index.cjs +163 -31
  25. package/src/main/ipcSchemas.cjs +59 -2
  26. package/src/main/lib/cleanEnv.cjs +20 -0
  27. package/src/main/lib/credentials.cjs +184 -0
  28. package/src/main/lib/schedulerConfig.cjs +10 -0
  29. package/src/main/logs.cjs +1 -1
  30. package/src/main/otelSettings.cjs +1 -1
  31. package/src/main/pty.cjs +53 -6
  32. package/src/main/scheduler.cjs +518 -147
  33. package/src/main/transcripts.cjs +26 -21
  34. package/src/main/usage.cjs +76 -25
  35. package/src/main/voiceSettings.cjs +1 -1
  36. package/src/main/watchers.cjs +69 -11
  37. package/src/preload/api.d.ts +51 -11
  38. package/src/preload/index.cjs +13 -0
  39. package/dist/assets/index-DsC4vT8M.css +0 -32
  40. package/dist/assets/index-E14-spyd.js +0 -2972
@@ -84,40 +84,45 @@ function classifyLine(obj) {
84
84
  }
85
85
 
86
86
  /**
87
- * Read new bytes from the file starting at `offset`, returning {lines, newOffset}.
88
- * Tolerates partial last lines (keeps them as pending for the next read).
87
+ * Read new bytes from sub.filePath into sub.offset/pending/inode in place.
88
+ * Resets offset+pending when the file inode changes (rename+replace rotation).
89
+ * Returns parsed line strings ready for JSON.parse.
89
90
  */
90
- async function readDelta(filePath, offset, pending) {
91
- let stat;
92
- try {
93
- stat = await fsp.stat(filePath);
94
- } catch {
95
- return { lines: [], newOffset: offset, pending };
91
+ async function readDelta(sub) {
92
+ const stat = await fsp.stat(sub.filePath).catch(() => null);
93
+ if (!stat) return [];
94
+ // Inode changed → file was replaced underfoot; restart from the top.
95
+ if (sub.inode !== undefined && stat.ino !== sub.inode) {
96
+ sub.offset = 0;
97
+ sub.pending = '';
96
98
  }
97
- if (stat.size < offset) {
99
+ if (stat.size < sub.offset) {
98
100
  // File was truncated/rotated — start over.
99
- offset = 0;
100
- pending = '';
101
+ sub.offset = 0;
102
+ sub.pending = '';
103
+ }
104
+ if (stat.size === sub.offset) {
105
+ sub.inode = stat.ino;
106
+ return [];
101
107
  }
102
- if (stat.size === offset) return { lines: [], newOffset: offset, pending };
103
- const fd = await fsp.open(filePath, 'r');
108
+ const fd = await fsp.open(sub.filePath, 'r');
104
109
  try {
105
- const length = stat.size - offset;
110
+ const length = stat.size - sub.offset;
106
111
  const buf = Buffer.alloc(length);
107
- await fd.read(buf, 0, length, offset);
108
- const text = pending + buf.toString('utf8');
112
+ await fd.read(buf, 0, length, sub.offset);
113
+ const text = sub.pending + buf.toString('utf8');
109
114
  const parts = text.split('\n');
110
- const newPending = parts.pop() ?? '';
111
- return { lines: parts.filter(Boolean), newOffset: stat.size, pending: newPending };
115
+ sub.pending = parts.pop() ?? '';
116
+ sub.offset = stat.size;
117
+ sub.inode = stat.ino;
118
+ return parts.filter(Boolean);
112
119
  } finally {
113
120
  await fd.close();
114
121
  }
115
122
  }
116
123
 
117
124
  async function flush(sub, { emit = true } = {}) {
118
- const { lines, newOffset, pending } = await readDelta(sub.filePath, sub.offset, sub.pending);
119
- sub.offset = newOffset;
120
- sub.pending = pending;
125
+ const lines = await readDelta(sub);
121
126
  for (const line of lines) {
122
127
  let obj;
123
128
  try {
@@ -20,48 +20,99 @@ const fsp = require('node:fs/promises');
20
20
  const path = require('node:path');
21
21
  const os = require('node:os');
22
22
  const { ipcMain } = require('electron');
23
+ const { refreshIfNeeded, expiresAtMs } = require('./lib/credentials.cjs');
23
24
 
24
- const CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
25
25
  const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
26
+ const CACHE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'billing-cache.json');
26
27
 
27
- async function readCredentials() {
28
- const raw = await fsp.readFile(CREDS_PATH, 'utf8');
29
- const data = JSON.parse(raw);
30
- const oa = data?.claudeAiOauth;
31
- if (!oa?.accessToken) throw new Error('not signed in (missing accessToken)');
32
- return oa;
28
+ let cache = null;
29
+ let hydrationPromise = null;
30
+
31
+ async function hydrateCache() {
32
+ try {
33
+ cache = JSON.parse(await fsp.readFile(CACHE_PATH, 'utf8'));
34
+ } catch {
35
+ cache = null;
36
+ }
37
+ }
38
+
39
+ async function persistCache(c) {
40
+ await fsp.mkdir(path.dirname(CACHE_PATH), { recursive: true });
41
+ const tmp = `${CACHE_PATH}.${process.pid}.${Date.now()}.tmp`;
42
+ await fsp.writeFile(tmp, JSON.stringify(c), { encoding: 'utf8', mode: 0o600 });
43
+ await fsp.rename(tmp, CACHE_PATH);
33
44
  }
34
45
 
35
46
  async function fetchUsage() {
36
- const creds = await readCredentials();
37
- const r = await fetch(USAGE_URL, {
38
- headers: {
39
- Authorization: `Bearer ${creds.accessToken}`,
40
- 'anthropic-beta': 'oauth-2025-04-20',
41
- 'User-Agent': 'claude-code-session-manager',
42
- },
43
- });
47
+ // Check expiry and attempt proactive refresh before touching the network.
48
+ const refresh = await refreshIfNeeded();
49
+ if (refresh.kind === 'auth') {
50
+ return { kind: 'auth', message: refresh.message, httpStatus: 401, expiredAt: refresh.expiredAt ?? null };
51
+ }
52
+ if (refresh.kind === 'config') return refresh;
53
+ // 'ok' or 'unsupported' — creds present and not yet expired
54
+ const creds = refresh.creds;
55
+
56
+ let r;
57
+ try {
58
+ r = await fetch(USAGE_URL, {
59
+ headers: {
60
+ Authorization: `Bearer ${creds.accessToken}`,
61
+ 'anthropic-beta': 'oauth-2025-04-20',
62
+ 'User-Agent': 'claude-code-session-manager',
63
+ },
64
+ signal: AbortSignal.timeout(10_000),
65
+ });
66
+ } catch (e) {
67
+ return { kind: 'transient', message: e.message || String(e), httpStatus: null };
68
+ }
69
+ if (r.status === 401 || r.status === 403) {
70
+ const body = await r.text().catch(() => '');
71
+ const ms = expiresAtMs(creds);
72
+ return { kind: 'auth', message: body.slice(0, 200) || `HTTP ${r.status}`, httpStatus: r.status, expiredAt: ms };
73
+ }
74
+ if (r.status === 408 || r.status === 429 || r.status >= 500) {
75
+ const body = await r.text().catch(() => '');
76
+ return { kind: 'transient', message: body.slice(0, 200) || `HTTP ${r.status}`, httpStatus: r.status };
77
+ }
44
78
  if (!r.ok) {
45
79
  const body = await r.text().catch(() => '');
46
- throw new Error(`usage HTTP ${r.status}: ${body.slice(0, 200)}`);
80
+ return { kind: 'transient', message: body.slice(0, 200) || `HTTP ${r.status}`, httpStatus: r.status };
47
81
  }
48
82
  const usage = await r.json();
49
83
  return {
50
- usage,
51
- subscriptionType: creds.subscriptionType ?? null,
52
- rateLimitTier: creds.rateLimitTier ?? null,
53
- credentialsExpiresAt: creds.expiresAt ?? null,
54
- fetchedAt: Date.now(),
84
+ kind: 'ok',
85
+ data: {
86
+ usage,
87
+ subscriptionType: creds.subscriptionType ?? null,
88
+ rateLimitTier: creds.rateLimitTier ?? null,
89
+ credentialsExpiresAt: creds.expiresAt ?? null,
90
+ fetchedAt: Date.now(),
91
+ },
55
92
  };
56
93
  }
57
94
 
58
95
  function registerBillingHandlers() {
96
+ hydrationPromise = hydrateCache();
97
+
59
98
  ipcMain.handle('billing:fetch', async () => {
60
- try {
61
- return { ok: true, data: await fetchUsage() };
62
- } catch (e) {
63
- return { ok: false, error: e?.message ?? String(e) };
99
+ if (hydrationPromise) { await hydrationPromise; hydrationPromise = null; }
100
+
101
+ const r = await fetchUsage();
102
+ if (r.kind === 'ok') {
103
+ cache = { data: r.data, fetchedAt: Date.now(), sourceCredsExpiresAt: r.data.credentialsExpiresAt };
104
+ persistCache(cache).catch(() => {});
105
+ return { kind: 'ok', data: r.data };
106
+ }
107
+ if (r.kind === 'auth') {
108
+ if (cache) return { kind: 'auth', message: r.message, httpStatus: r.httpStatus, expiredAt: r.expiredAt, cached: cache.data, staleSince: cache.fetchedAt };
109
+ return r;
110
+ }
111
+ if (r.kind === 'transient') {
112
+ if (cache) return { kind: 'ok-stale', data: cache.data, staleSince: cache.fetchedAt, lastError: r.message };
113
+ return r;
64
114
  }
115
+ return r; // config
65
116
  });
66
117
  }
67
118
 
@@ -109,7 +109,7 @@ async function writeMerged(patch) {
109
109
  const next = { ...existing, ...patch };
110
110
  const body = JSON.stringify(next, null, 2) + '\n';
111
111
  const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
112
- await fsp.writeFile(tmp, body, 'utf8', { mode: 0o600 });
112
+ await fsp.writeFile(tmp, body, { encoding: 'utf8', mode: 0o600 });
113
113
  // chmod tmp explicitly because some platforms ignore the mode arg on
114
114
  // writeFile when the file pre-exists. Then rename for atomicity.
115
115
  try { await fsp.chmod(tmp, 0o600); } catch { /* */ }
@@ -10,8 +10,40 @@
10
10
 
11
11
  const { ipcMain } = require('electron');
12
12
  const { spawn } = require('node:child_process');
13
- const readline = require('node:readline');
14
13
  const crypto = require('node:crypto');
14
+ const path = require('node:path');
15
+ const os = require('node:os');
16
+ const fs = require('node:fs');
17
+ const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
18
+
19
+ // Splits a readable stream into lines capped at maxLineBytes. Prevents OOM
20
+ // from commands that produce no newlines (e.g. cat /dev/urandom | base64).
21
+ function lineSplit(stream, onLine, maxLineBytes = 4096) {
22
+ let buf = Buffer.alloc(0);
23
+ stream.on('data', (chunk) => {
24
+ buf = Buffer.concat([buf, chunk]);
25
+ while (true) {
26
+ const nl = buf.indexOf(0x0a);
27
+ if (nl === -1) {
28
+ if (buf.length > maxLineBytes) {
29
+ onLine(buf.slice(0, maxLineBytes).toString('utf8') + '…[truncated]');
30
+ buf = Buffer.alloc(0);
31
+ }
32
+ return;
33
+ }
34
+ const line = buf.slice(0, nl).toString('utf8');
35
+ buf = buf.slice(nl + 1);
36
+ onLine(line.length > maxLineBytes ? line.slice(0, maxLineBytes) + '…[truncated]' : line);
37
+ }
38
+ });
39
+ stream.on('end', () => {
40
+ if (buf.length > 0) {
41
+ onLine(buf.length > maxLineBytes
42
+ ? buf.slice(0, maxLineBytes).toString('utf8') + '…[truncated]'
43
+ : buf.toString('utf8'));
44
+ }
45
+ });
46
+ }
15
47
 
16
48
  class WatcherManager {
17
49
  constructor() {
@@ -25,13 +57,27 @@ class WatcherManager {
25
57
  }
26
58
 
27
59
  add({ tabId, label, command, cwd }) {
60
+ const resolvedCwd = cwd || process.cwd();
61
+
62
+ // Require cwd's realpath to be inside homedir.
63
+ const home = os.homedir();
64
+ let realCwd;
65
+ try {
66
+ realCwd = fs.realpathSync(resolvedCwd);
67
+ } catch {
68
+ realCwd = path.resolve(resolvedCwd);
69
+ }
70
+ if (realCwd !== home && !realCwd.startsWith(home + path.sep)) {
71
+ throw new Error(`watcher cwd outside home directory: ${realCwd}`);
72
+ }
73
+
28
74
  const watcherId = crypto.randomUUID();
29
75
  const trimmedLabel = (label && label.trim()) || command.slice(0, 40);
30
76
  const child = spawn(command, [], {
31
- cwd: cwd || process.cwd(),
77
+ cwd: resolvedCwd,
32
78
  shell: true,
33
79
  stdio: ['ignore', 'pipe', 'pipe'],
34
- env: process.env,
80
+ env: cleanChildEnv(),
35
81
  });
36
82
 
37
83
  const entry = {
@@ -39,7 +85,7 @@ class WatcherManager {
39
85
  tabId,
40
86
  label: trimmedLabel,
41
87
  command,
42
- cwd: cwd || process.cwd(),
88
+ cwd: resolvedCwd,
43
89
  child,
44
90
  startedAt: Date.now(),
45
91
  lineCount: 0,
@@ -59,18 +105,16 @@ class WatcherManager {
59
105
  }
60
106
  };
61
107
 
62
- const stdoutRl = readline.createInterface({ input: child.stdout });
63
- stdoutRl.on('line', emitLine);
64
- const stderrRl = readline.createInterface({ input: child.stderr });
65
- stderrRl.on('line', emitLine);
108
+ lineSplit(child.stdout, emitLine);
109
+ lineSplit(child.stderr, emitLine);
66
110
 
67
111
  child.on('error', (err) => {
68
112
  emitLine(`[watcher error] ${err?.message ?? String(err)}`);
113
+ // Remove from map so list() doesn't return ghosts after a spawn failure.
114
+ this.watchers.delete(watcherId);
69
115
  });
70
116
 
71
117
  child.on('close', (code, signal) => {
72
- stdoutRl.close();
73
- stderrRl.close();
74
118
  // Only emit close if still registered (remove() suppresses by deleting first).
75
119
  if (this.watchers.has(watcherId)) {
76
120
  emitLine(`[exited code=${code ?? 'null'}${signal ? ` signal=${signal}` : ''}]`);
@@ -135,6 +179,12 @@ class WatcherManager {
135
179
 
136
180
  const manager = new WatcherManager();
137
181
 
182
+ // Top-level export so index.cjs can call watchers.attachWindow(mainWindow)
183
+ // without going through the manager instance directly.
184
+ function attachWindow(window) {
185
+ manager.attachWindow(window);
186
+ }
187
+
138
188
  function registerWatcherHandlers() {
139
189
  const { z } = require('zod');
140
190
  const addSchema = z.object({
@@ -145,10 +195,18 @@ function registerWatcherHandlers() {
145
195
  });
146
196
  const listSchema = z.object({ tabId: z.string().min(1).max(128) });
147
197
  const removeSchema = z.object({ watcherId: z.string().min(1).max(128) });
198
+ const killTabSchema = z.object({ tabId: z.string().min(1).max(128) });
148
199
 
149
200
  ipcMain.handle('watchers:add', (_e, payload) => manager.add(addSchema.parse(payload)));
150
201
  ipcMain.handle('watchers:list', (_e, payload) => manager.list(listSchema.parse(payload)));
151
202
  ipcMain.handle('watchers:remove', (_e, payload) => manager.remove(removeSchema.parse(payload)));
203
+ ipcMain.handle('watchers:kill-tab', (_e, payload) => {
204
+ const { tabId } = killTabSchema.parse(payload);
205
+ for (const [id, w] of manager.watchers) {
206
+ if (w.tabId === tabId) manager.remove({ watcherId: id });
207
+ }
208
+ return { ok: true };
209
+ });
152
210
  }
153
211
 
154
- module.exports = { manager, registerWatcherHandlers };
212
+ module.exports = { manager, attachWindow, registerWatcherHandlers };
@@ -10,6 +10,11 @@ export interface PtyExit {
10
10
  signal?: number;
11
11
  }
12
12
 
13
+ export interface WriteErrorEvent {
14
+ tabId: string;
15
+ reason: string;
16
+ }
17
+
13
18
  export interface ReadJsonResult {
14
19
  exists: boolean;
15
20
  raw: string;
@@ -84,6 +89,8 @@ export interface PersistedTab {
84
89
  export interface LoadedSessions {
85
90
  tabs: PersistedTab[];
86
91
  activeTabId: string | null;
92
+ /** True when the main process rotated all session IDs on boot (e.g. force-fresh). */
93
+ freshStart?: boolean;
87
94
  }
88
95
 
89
96
  export interface UsageWindow {
@@ -107,18 +114,21 @@ export interface UsageSnapshot {
107
114
  [key: string]: unknown;
108
115
  }
109
116
 
110
- export interface BillingFetchResult {
111
- ok: boolean;
112
- data?: {
113
- usage: UsageSnapshot;
114
- subscriptionType: string | null;
115
- rateLimitTier: string | null;
116
- credentialsExpiresAt: number | null;
117
- fetchedAt: number;
118
- };
119
- error?: string;
117
+ export interface BillingData {
118
+ usage: UsageSnapshot;
119
+ subscriptionType: string | null;
120
+ rateLimitTier: string | null;
121
+ credentialsExpiresAt: string | null;
122
+ fetchedAt: number;
120
123
  }
121
124
 
125
+ export type BillingFetchResult =
126
+ | { kind: 'ok'; data: BillingData }
127
+ | { kind: 'ok-stale'; data: BillingData; staleSince: number; lastError: string }
128
+ | { kind: 'auth'; message: string; httpStatus: number; expiredAt?: number | null; cached?: BillingData; staleSince?: number }
129
+ | { kind: 'transient'; message: string; httpStatus: number | null }
130
+ | { kind: 'config'; message: string };
131
+
122
132
  export interface VoiceHotkeyConfig {
123
133
  accelerator: string;
124
134
  mode: 'hold' | 'toggle';
@@ -259,7 +269,7 @@ export interface SchedulePaths {
259
269
  queue: string;
260
270
  }
261
271
 
262
- export type SchedulePauseReason = 'rate_limit';
272
+ export type SchedulePauseReason = 'rate_limit' | 'auth' | 'network' | 'manual' | 'reset_failure';
263
273
 
264
274
  export interface SchedulePauseInfo {
265
275
  reason: SchedulePauseReason;
@@ -270,6 +280,27 @@ export interface SchedulePauseInfo {
270
280
  resumeAt: string | null;
271
281
  }
272
282
 
283
+ export interface ScheduleHealthSnapshot {
284
+ bootedAt: number;
285
+ lastPollAt: number | null;
286
+ lastPollOk: boolean;
287
+ consecutiveFailures: number;
288
+ backoffNextAt: number | null;
289
+ nextResetCached: string | null;
290
+ pausedSince: number | null;
291
+ pauseReason: SchedulePauseReason | null;
292
+ runningJobs: { slug: string; startedAt: number; pid: number }[];
293
+ }
294
+
295
+ export interface PrdListItem {
296
+ slug: string;
297
+ parallelGroup: number;
298
+ title: string;
299
+ cwd: string;
300
+ estimateMinutes: number | null;
301
+ mtimeMs: number;
302
+ }
303
+
273
304
  export interface ScheduleStateSnapshot {
274
305
  config: ScheduleConfig;
275
306
  jobs: ScheduleJob[];
@@ -331,6 +362,10 @@ export interface SessionManagerAPI {
331
362
  isE2E: () => Promise<boolean>;
332
363
  onNewSession: (handler: () => void) => () => void;
333
364
  onRebootSession: (handler: () => void) => () => void;
365
+ openInEditor: (cwd: string, editor?: string | null) => Promise<{ ok: boolean; editor?: string; error?: string }>;
366
+ openInFinder: (cwd: string) => Promise<{ ok: boolean; error?: string }>;
367
+ openInTerminal: (cwd: string) => Promise<{ ok: boolean; terminal?: string; error?: string }>;
368
+ archiveProject: (encoded: string) => Promise<{ ok: boolean; error?: string }>;
334
369
  };
335
370
  pty: {
336
371
  spawn: (payload: { tabId: string; cwd: string; cols?: number; rows?: number }) => Promise<SpawnResult>;
@@ -339,6 +374,7 @@ export interface SessionManagerAPI {
339
374
  kill: (tabId: string) => void;
340
375
  onData: (tabId: string, handler: (data: string) => void) => () => void;
341
376
  onExit: (tabId: string, handler: (info: PtyExit) => void) => () => void;
377
+ onWriteError: (handler: (ev: WriteErrorEvent) => void) => () => void;
342
378
  };
343
379
  transcripts: {
344
380
  subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
@@ -393,6 +429,7 @@ export interface SessionManagerAPI {
393
429
  add: (payload: { tabId: string; label?: string; command: string; cwd?: string | null }) => Promise<WatcherAddResult>;
394
430
  list: (tabId: string) => Promise<WatcherInfo[]>;
395
431
  remove: (watcherId: string) => Promise<{ ok: boolean }>;
432
+ killTab: (tabId: string) => Promise<{ ok: boolean }>;
396
433
  onLine: (handler: (ev: WatcherLineEvent) => void) => () => void;
397
434
  onClosed: (handler: (ev: WatcherClosedEvent) => void) => () => void;
398
435
  };
@@ -412,6 +449,9 @@ export interface SessionManagerAPI {
412
449
  openFolder: () => Promise<{ ok: boolean }>;
413
450
  readPrd: (slug: string) => Promise<{ ok: boolean; text?: string; error?: string }>;
414
451
  readLog: (runId: string, slug: string) => Promise<{ ok: boolean; text?: string; error?: string }>;
452
+ writePrd: (slug: string, body: string) => Promise<{ ok: boolean; bytesWritten: number }>;
453
+ listPrds: () => Promise<PrdListItem[]>;
454
+ health: () => Promise<ScheduleHealthSnapshot>;
415
455
  onState: (handler: (snapshot: ScheduleStateSnapshot) => void) => () => void;
416
456
  };
417
457
  }
@@ -9,6 +9,10 @@ contextBridge.exposeInMainWorld('api', {
9
9
  pickDirectory: () => ipcRenderer.invoke('app:pick-directory'),
10
10
  gitBranch: (cwd) => ipcRenderer.invoke('app:git-branch', { cwd }),
11
11
  rebootApp: () => ipcRenderer.send('app:reboot-app'),
12
+ openInEditor: (cwd, editor) => ipcRenderer.invoke('app:open-in-editor', { cwd, editor }),
13
+ openInFinder: (cwd) => ipcRenderer.invoke('app:open-in-finder', { cwd }),
14
+ openInTerminal: (cwd) => ipcRenderer.invoke('app:open-in-terminal', { cwd }),
15
+ archiveProject: (encoded) => ipcRenderer.invoke('app:archive-project', { encoded }),
12
16
  testFireHook: (args) => ipcRenderer.invoke('app:test-fire-hook', args),
13
17
  // F7: lets the renderer suppress the wizard auto-trigger under SM_E2E=1.
14
18
  isE2E: () => ipcRenderer.invoke('app:is-e2e'),
@@ -40,6 +44,11 @@ contextBridge.exposeInMainWorld('api', {
40
44
  ipcRenderer.on(channel, listener);
41
45
  return () => ipcRenderer.removeListener(channel, listener);
42
46
  },
47
+ onWriteError: (handler) => {
48
+ const listener = (_e, payload) => handler(payload);
49
+ ipcRenderer.on('pty:write-error', listener);
50
+ return () => ipcRenderer.removeListener('pty:write-error', listener);
51
+ },
43
52
  },
44
53
  transcripts: {
45
54
  subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
@@ -111,6 +120,7 @@ contextBridge.exposeInMainWorld('api', {
111
120
  add: (payload) => ipcRenderer.invoke('watchers:add', payload),
112
121
  list: (tabId) => ipcRenderer.invoke('watchers:list', { tabId }),
113
122
  remove: (watcherId) => ipcRenderer.invoke('watchers:remove', { watcherId }),
123
+ killTab: (tabId) => ipcRenderer.invoke('watchers:kill-tab', { tabId }),
114
124
  onLine: (handler) => {
115
125
  const listener = (_e, payload) => handler(payload);
116
126
  ipcRenderer.on('watcher:line', listener);
@@ -138,6 +148,9 @@ contextBridge.exposeInMainWorld('api', {
138
148
  openFolder: () => ipcRenderer.invoke('schedule:open-folder'),
139
149
  readPrd: (slug) => ipcRenderer.invoke('schedule:read-prd', { slug }),
140
150
  readLog: (runId, slug) => ipcRenderer.invoke('schedule:read-log', { runId, slug }),
151
+ writePrd: (slug, body) => ipcRenderer.invoke('schedule:write-prd', { slug, body }),
152
+ listPrds: () => ipcRenderer.invoke('schedule:list-prds'),
153
+ health: () => ipcRenderer.invoke('schedule:health'),
141
154
  onState: (handler) => {
142
155
  const listener = (_e, payload) => handler(payload);
143
156
  ipcRenderer.on('schedule:state', listener);
@@ -1,32 +0,0 @@
1
- /**
2
- * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3
- * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4
- * https://github.com/chjj/term.js
5
- * @license MIT
6
- *
7
- * Permission is hereby granted, free of charge, to any person obtaining a copy
8
- * of this software and associated documentation files (the "Software"), to deal
9
- * in the Software without restriction, including without limitation the rights
10
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- * copies of the Software, and to permit persons to whom the Software is
12
- * furnished to do so, subject to the following conditions:
13
- *
14
- * The above copyright notice and this permission notice shall be included in
15
- * all copies or substantial portions of the Software.
16
- *
17
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
- * THE SOFTWARE.
24
- *
25
- * Originally forked from (with the author's permission):
26
- * Fabrice Bellard's javascript vt100 for jslinux:
27
- * http://bellard.org/jslinux/
28
- * Copyright (c) 2011 Fabrice Bellard
29
- * The original design remains. The terminal itself
30
- * has been extended to include xterm CSI codes, among
31
- * other features.
32
- */.xterm{cursor:text;position:relative;-moz-user-select:none;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::-moz-selection{color:transparent}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;-moz-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:double underline;text-decoration:double underline}.xterm-underline-3{-webkit-text-decoration:wavy underline;text-decoration:wavy underline}.xterm-underline-4{-webkit-text-decoration:dotted underline;text-decoration:dotted underline}.xterm-underline-5{-webkit-text-decoration:dashed underline;text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}*,:before,:after{--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: rgb(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: rgb(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: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{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,samp,pre{font-family:JetBrains Mono,ui-monospace,SFMono-Regular,Menlo,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,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{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}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.-bottom-\[5px\]{bottom:-5px}.-bottom-\[7px\]{bottom:-7px}.-top-20{top:-5rem}.-top-28{top:-7rem}.-top-48{top:-12rem}.bottom-0{bottom:0}.bottom-24{bottom:6rem}.bottom-3{bottom:.75rem}.bottom-4{bottom:1rem}.bottom-\[88px\]{bottom:88px}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.left-\[72\%\]{left:72%}.right-0{right:0}.right-2{right:.5rem}.right-4{right:1rem}.top-0{top:0}.top-0\.5{top:.125rem}.top-10{top:2.5rem}.top-4{top:1rem}.top-full{top:100%}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-4{margin-left:1rem;margin-right:1rem}.-mb-px{margin-bottom:-1px}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-0\.5{height:.125rem}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-9{height:2.25rem}.h-full{height:100%}.h-px{height:1px}.max-h-24{max-height:6rem}.max-h-40{max-height:10rem}.max-h-64{max-height:16rem}.max-h-72{max-height:18rem}.max-h-\[60vh\]{max-height:60vh}.min-h-0{min-height:0px}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-10{width:2.5rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-56{width:14rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-\[420px\]{width:420px}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-\[12rem\]{max-width:12rem}.max-w-\[8rem\]{max-width:8rem}.max-w-md{max-width:28rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0\.5{--tw-translate-x: .125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-3\.5{--tw-translate-x: .875rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-45{--tw-rotate: 45deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-col-resize{cursor:col-resize}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.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-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.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-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-1{row-gap:.25rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(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 * calc(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 * calc(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 * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-line>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(36 42 51 / var(--tw-divide-opacity, 1))}.divide-line\/60>:not([hidden])~:not([hidden]){border-color:#242a3399}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.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-xl{border-radius:.75rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-0{border-bottom-width:0px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-accent{--tw-border-opacity: 1;border-color:rgb(217 119 87 / var(--tw-border-opacity, 1))}.border-accent-muted{--tw-border-opacity: 1;border-color:rgb(138 74 51 / var(--tw-border-opacity, 1))}.border-accent\/30{border-color:#d977574d}.border-accent\/40{border-color:#d9775766}.border-accent\/50{border-color:#d9775780}.border-blue-900\/40{border-color:#1e3a8a66}.border-emerald-900\/40{border-color:#064e3b66}.border-line{--tw-border-opacity: 1;border-color:rgb(36 42 51 / var(--tw-border-opacity, 1))}.border-line\/50{border-color:#242a3380}.border-line\/60{border-color:#242a3399}.border-purple-900\/40{border-color:#581c8766}.border-red-500\/70{border-color:#ef4444b3}.border-red-800{--tw-border-opacity: 1;border-color:rgb(153 27 27 / var(--tw-border-opacity, 1))}.border-red-900\/40{border-color:#7f1d1d66}.border-transparent{border-color:transparent}.border-yellow-600\/50{border-color:#ca8a0480}.border-yellow-600\/60{border-color:#ca8a0499}.border-yellow-900\/40{border-color:#713f1266}.bg-accent{--tw-bg-opacity: 1;background-color:rgb(217 119 87 / var(--tw-bg-opacity, 1))}.bg-accent\/15{background-color:#d9775726}.bg-accent\/5{background-color:#d977570d}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-500\/5{background-color:#f59e0b0d}.bg-bg{--tw-bg-opacity: 1;background-color:rgb(11 13 16 / var(--tw-bg-opacity, 1))}.bg-bg-elev{--tw-bg-opacity: 1;background-color:rgb(18 21 26 / var(--tw-bg-opacity, 1))}.bg-bg-elev\/40{background-color:#12151a66}.bg-bg-elev\/60{background-color:#12151a99}.bg-bg-elev\/80{background-color:#12151acc}.bg-bg-hi{--tw-bg-opacity: 1;background-color:rgb(26 31 39 / var(--tw-bg-opacity, 1))}.bg-bg-hi\/30{background-color:#1a1f274d}.bg-bg\/50{background-color:#0b0d1080}.bg-black\/60{background-color:#0009}.bg-blue-950\/30{background-color:#1725544d}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity, 1))}.bg-emerald-950\/30{background-color:#022c224d}.bg-fg{--tw-bg-opacity: 1;background-color:rgb(230 232 236 / var(--tw-bg-opacity, 1))}.bg-fg-dim{--tw-bg-opacity: 1;background-color:rgb(138 147 160 / var(--tw-bg-opacity, 1))}.bg-fg-faint{--tw-bg-opacity: 1;background-color:rgb(84 92 104 / var(--tw-bg-opacity, 1))}.bg-fg-faint\/40{background-color:#545c6866}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-900\/30{background-color:#14532d4d}.bg-line\/60{background-color:#242a3399}.bg-purple-950\/30{background-color:#3b07644d}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-900\/30{background-color:#7f1d1d4d}.bg-red-950\/20{background-color:#450a0a33}.bg-red-950\/30{background-color:#450a0a4d}.bg-red-950\/60{background-color:#450a0a99}.bg-yellow-400{--tw-bg-opacity: 1;background-color:rgb(250 204 21 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-yellow-950\/20{background-color:#42200633}.bg-yellow-950\/30{background-color:#4220064d}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.from-bg{--tw-gradient-from: #0b0d10 var(--tw-gradient-from-position);--tw-gradient-to: rgb(11 13 16 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-bg{--tw-gradient-to: rgb(11 13 16 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #0b0d10 var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-bg-elev{--tw-gradient-to: #12151a var(--tw-gradient-to-position)}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.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-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.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-12{padding-top:3rem;padding-bottom:3rem}.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-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-4{padding-left:1rem}.pr-2{padding-right:.5rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,ui-monospace,SFMono-Regular,Menlo,monospace}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.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-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.tracking-normal{letter-spacing:0em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-accent{--tw-text-opacity: 1;color:rgb(217 119 87 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-bg{--tw-text-opacity: 1;color:rgb(11 13 16 / var(--tw-text-opacity, 1))}.text-blue-400\/80{color:#60a5facc}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity, 1))}.text-fg{--tw-text-opacity: 1;color:rgb(230 232 236 / var(--tw-text-opacity, 1))}.text-fg-dim{--tw-text-opacity: 1;color:rgb(138 147 160 / var(--tw-text-opacity, 1))}.text-fg-faint{--tw-text-opacity: 1;color:rgb(84 92 104 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-purple-400\/80{color:#c084fccc}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-400\/80{color:#f87171cc}.text-yellow-200{--tw-text-opacity: 1;color:rgb(254 240 138 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.text-yellow-500\/70{color:#eab308b3}.text-yellow-500\/80{color:#eab308cc}.text-yellow-500\/90{color:#eab308e6}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline-offset-2{text-underline-offset:2px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.placeholder-fg-faint::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(84 92 104 / var(--tw-placeholder-opacity, 1))}.placeholder-fg-faint::placeholder{--tw-placeholder-opacity: 1;color:rgb(84 92 104 / var(--tw-placeholder-opacity, 1))}.accent-accent{accent-color:#d97757}.opacity-0{opacity:0}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);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 rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring{--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(3px + 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-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)}.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-red-400{--tw-ring-opacity: 1;--tw-ring-color: rgb(248 113 113 / var(--tw-ring-opacity, 1))}.ring-red-500{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1))}.blur{--tw-blur: blur(8px);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)}.drop-shadow-2xl{--tw-drop-shadow: drop-shadow(0 25px 25px rgb(0 0 0 / .15));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)}.invert{--tw-invert: invert(100%);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)}.\!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)!important}.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)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[width\]{transition-property:width;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-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-75{transition-duration:75ms}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}html,body,#root{height:100%;margin:0;overflow:hidden}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:#0b0d10}::-webkit-scrollbar-thumb{background:#242a33;border-radius:5px}::-webkit-scrollbar-thumb:hover{background:#2f3742}.placeholder\:text-fg-faint::-moz-placeholder{--tw-text-opacity: 1;color:rgb(84 92 104 / var(--tw-text-opacity, 1))}.placeholder\:text-fg-faint::placeholder{--tw-text-opacity: 1;color:rgb(84 92 104 / var(--tw-text-opacity, 1))}.hover\:border-accent:hover{--tw-border-opacity: 1;border-color:rgb(217 119 87 / var(--tw-border-opacity, 1))}.hover\:border-fg-faint:hover{--tw-border-opacity: 1;border-color:rgb(84 92 104 / var(--tw-border-opacity, 1))}.hover\:bg-accent:hover{--tw-bg-opacity: 1;background-color:rgb(217 119 87 / var(--tw-bg-opacity, 1))}.hover\:bg-accent\/25:hover{background-color:#d9775740}.hover\:bg-accent\/40:hover{background-color:#d9775766}.hover\:bg-bg:hover{--tw-bg-opacity: 1;background-color:rgb(11 13 16 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-elev:hover{--tw-bg-opacity: 1;background-color:rgb(18 21 26 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-elev\/20:hover{background-color:#12151a33}.hover\:bg-bg-elev\/50:hover{background-color:#12151a80}.hover\:bg-bg-hi:hover{--tw-bg-opacity: 1;background-color:rgb(26 31 39 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-hi\/50:hover{background-color:#1a1f2780}.hover\:bg-transparent:hover{background-color:transparent}.hover\:bg-yellow-600:hover{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity, 1))}.hover\:text-bg:hover{--tw-text-opacity: 1;color:rgb(11 13 16 / var(--tw-text-opacity, 1))}.hover\:text-fg:hover{--tw-text-opacity: 1;color:rgb(230 232 236 / var(--tw-text-opacity, 1))}.hover\:text-fg-dim:hover{--tw-text-opacity: 1;color:rgb(138 147 160 / 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\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-fg-faint:focus{--tw-border-opacity: 1;border-color:rgb(84 92 104 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-accent\/60:active{background-color:#d9775799}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}