@jhizzard/termdeck 0.2.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.
@@ -0,0 +1,421 @@
1
+ // Session manager - PTY lifecycle, metadata tracking, output analysis
2
+ // Each session wraps a node-pty instance with rich metadata
3
+
4
+ const { v4: uuidv4 } = require('uuid');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ // Strip ANSI escape codes for pattern matching
9
+ function stripAnsi(str) {
10
+ return str
11
+ .replace(/\x1b\[[\?]?[0-9;]*[A-Za-z]/g, '') // CSI sequences (including ?-prefixed like bracketed paste)
12
+ .replace(/\x1b\][^\x07]*\x07/g, '') // OSC sequences
13
+ .replace(/\x1b[()][A-Z0-9]/g, '') // Character set sequences
14
+ .replace(/\x1b[>=<]/g, ''); // Keypad/cursor modes
15
+ }
16
+
17
+ // Pattern matchers for detecting terminal type and status
18
+ const PATTERNS = {
19
+ claudeCode: {
20
+ prompt: /^[>❯]\s/m,
21
+ thinking: /\b(thinking|Thinking)\b/,
22
+ editing: /^(Edit|Create|Update|Delete)\s/m,
23
+ tool: /^⏺\s/m,
24
+ idle: /^>\s*$/m
25
+ },
26
+ geminiCli: {
27
+ prompt: /^gemini>\s/m,
28
+ thinking: /\b(Generating|Working)\b/,
29
+ },
30
+ pythonServer: {
31
+ uvicorn: /Uvicorn running on/,
32
+ flask: /Running on http/,
33
+ django: /Starting development server/,
34
+ httpServer: /Serving HTTP on/,
35
+ request: /(?:^|\s|")(GET|POST|PUT|DELETE|PATCH)\s+\S+.*?\s(\d{3})/m,
36
+ port: /(?:port\s+(\d+)|(?:on|at)\s+(?:https?:\/\/)?[\w.\[\]:]*:(\d+))/i
37
+ },
38
+ shell: {
39
+ prompt: /[\$#%❯>]\s*$/m,
40
+ // Match lines ending with common shell control sequences that indicate a new prompt
41
+ // We track commands via input echo instead (see _trackInput)
42
+ command: /^[\$#%❯>]\s+(.+)$/m
43
+ },
44
+ // Broad error markers across shells, compilers, scripts, and HTTP servers.
45
+ error: /\b(error|Error|ERROR|exception|Exception|Traceback|fatal|FATAL|segmentation fault|panic|EACCES|ECONNREFUSED|ENOENT|command not found|undefined reference|cannot find module|failed with exit code|\b5\d\d\b)\b/
46
+ };
47
+
48
+ class Session {
49
+ constructor(options) {
50
+ this.id = options.id || uuidv4();
51
+ this.pid = null;
52
+ this.pty = null;
53
+ this.ws = null;
54
+
55
+ // Metadata
56
+ this.meta = {
57
+ type: options.type || 'shell', // shell, claude-code, gemini, python-server, one-shot
58
+ project: options.project || null,
59
+ label: options.label || '',
60
+ command: options.command || '',
61
+ cwd: options.cwd || os.homedir(),
62
+ createdAt: new Date().toISOString(),
63
+ reason: options.reason || 'manual launch',
64
+
65
+ // Dynamic state (updated by output analyzer)
66
+ status: 'starting', // starting, active, idle, thinking, editing, errored, exited
67
+ statusDetail: '',
68
+ lastCommands: [], // rolling buffer of last 10 commands
69
+ lastActivity: new Date().toISOString(),
70
+ detectedPort: null,
71
+ requestCount: 0,
72
+ exitCode: null,
73
+ childProcesses: [],
74
+
75
+ // Theme
76
+ theme: options.theme || 'tokyo-night',
77
+
78
+ // RAG
79
+ ragEnabled: options.ragEnabled !== false,
80
+ ragEvents: [] // buffer before flush
81
+ };
82
+
83
+ // Output analysis state
84
+ this._outputBuffer = '';
85
+ this._outputFlushTimer = null;
86
+ this._commandBuffer = '';
87
+ this._inputBuffer = ''; // tracks user keyboard input for command detection
88
+ this.onCommand = null; // callback: (sessionId, command) => void
89
+ this.onStatusChange = null; // callback: (session, oldStatus, newStatus) => void
90
+ this.onErrorDetected = null; // callback: (session, { lastCommand, tail }) => void
91
+ this._statusChangeTimer = null;
92
+ this._pendingStatusChange = null;
93
+ this._lastErrorFireAt = 0;
94
+ }
95
+
96
+ // Analyze PTY output to extract metadata
97
+ analyzeOutput(data) {
98
+ this._outputBuffer += data;
99
+ this.meta.lastActivity = new Date().toISOString();
100
+
101
+ // Strip ANSI codes for reliable pattern matching
102
+ const clean = stripAnsi(data);
103
+
104
+ // Detect terminal type if still generic
105
+ if (this.meta.type === 'shell') {
106
+ this._detectType(clean);
107
+ }
108
+
109
+ // Detect ports before status update (so status can reference the port)
110
+ this._detectPort(clean);
111
+
112
+ // Update status based on type-specific patterns
113
+ this._updateStatus(clean);
114
+
115
+ // Extract commands from shell-like prompts
116
+ this._extractCommands(clean);
117
+
118
+ // Count HTTP requests for server terminals
119
+ this._countRequests(clean);
120
+
121
+ // Error detection — transition to 'errored' and fire onErrorDetected (rate limited 30s)
122
+ this._detectErrors(clean);
123
+
124
+ // Flush buffer periodically (don't hold too much in memory)
125
+ clearTimeout(this._outputFlushTimer);
126
+ this._outputFlushTimer = setTimeout(() => {
127
+ // Keep last 4KB for pattern matching
128
+ if (this._outputBuffer.length > 4096) {
129
+ this._outputBuffer = this._outputBuffer.slice(-4096);
130
+ }
131
+ // Server types: revert to 'listening' after output settles
132
+ if (this.meta.type === 'python-server' && this.meta.status === 'active') {
133
+ this.meta.status = 'listening';
134
+ this.meta.statusDetail = this.meta.detectedPort
135
+ ? `Serving on :${this.meta.detectedPort}`
136
+ : 'Server running';
137
+ }
138
+ }, 2000);
139
+ }
140
+
141
+ _detectType(data) {
142
+ if (PATTERNS.claudeCode.prompt.test(data) || /claude/i.test(this.meta.command)) {
143
+ this.meta.type = 'claude-code';
144
+ } else if (PATTERNS.geminiCli.prompt.test(data) || /gemini/i.test(this.meta.command)) {
145
+ this.meta.type = 'gemini';
146
+ } else if (
147
+ PATTERNS.pythonServer.uvicorn.test(data) ||
148
+ PATTERNS.pythonServer.flask.test(data) ||
149
+ PATTERNS.pythonServer.django.test(data) ||
150
+ PATTERNS.pythonServer.httpServer.test(data)
151
+ ) {
152
+ this.meta.type = 'python-server';
153
+ }
154
+ }
155
+
156
+ _updateStatus(data) {
157
+ const p = PATTERNS;
158
+ const oldStatus = this.meta.status;
159
+
160
+ switch (this.meta.type) {
161
+ case 'claude-code':
162
+ if (p.claudeCode.thinking.test(data)) {
163
+ this.meta.status = 'thinking';
164
+ this.meta.statusDetail = 'Claude is reasoning...';
165
+ } else if (p.claudeCode.editing.test(data)) {
166
+ this.meta.status = 'editing';
167
+ const match = data.match(/^(Edit|Create|Update|Delete)\s+(.+)$/m);
168
+ this.meta.statusDetail = match ? `${match[1]} ${match[2]}` : 'Editing files';
169
+ } else if (p.claudeCode.tool.test(data)) {
170
+ this.meta.status = 'active';
171
+ this.meta.statusDetail = 'Using tools';
172
+ } else if (p.claudeCode.idle.test(data)) {
173
+ this.meta.status = 'idle';
174
+ this.meta.statusDetail = 'Waiting for input';
175
+ }
176
+ break;
177
+
178
+ case 'gemini':
179
+ if (p.geminiCli.thinking.test(data)) {
180
+ this.meta.status = 'thinking';
181
+ this.meta.statusDetail = 'Gemini is generating...';
182
+ } else if (p.geminiCli.prompt.test(data)) {
183
+ this.meta.status = 'idle';
184
+ this.meta.statusDetail = 'Waiting for input';
185
+ }
186
+ break;
187
+
188
+ case 'python-server':
189
+ if (p.pythonServer.request.test(data)) {
190
+ this.meta.status = 'active';
191
+ const match = data.match(p.pythonServer.request);
192
+ if (match) {
193
+ this.meta.statusDetail = `${match[1]} → ${match[2]}`;
194
+ }
195
+ } else {
196
+ this.meta.status = 'listening';
197
+ this.meta.statusDetail = this.meta.detectedPort
198
+ ? `Serving on :${this.meta.detectedPort}`
199
+ : 'Server running';
200
+ }
201
+ break;
202
+
203
+ default:
204
+ if (p.shell.prompt.test(data)) {
205
+ this.meta.status = 'idle';
206
+ this.meta.statusDetail = 'Ready';
207
+ } else {
208
+ this.meta.status = 'active';
209
+ }
210
+ }
211
+
212
+ // Debounce status change events (3s) to avoid flooding RAG with active↔idle flaps
213
+ if (this.meta.status !== oldStatus && this.onStatusChange) {
214
+ clearTimeout(this._statusChangeTimer);
215
+ this._pendingStatusChange = { oldStatus, newStatus: this.meta.status };
216
+ this._statusChangeTimer = setTimeout(() => {
217
+ if (this._pendingStatusChange) {
218
+ this.onStatusChange(this, this._pendingStatusChange.oldStatus, this._pendingStatusChange.newStatus);
219
+ this._pendingStatusChange = null;
220
+ }
221
+ }, 3000);
222
+ }
223
+ }
224
+
225
+ _detectPort(data) {
226
+ const match = data.match(PATTERNS.pythonServer.port);
227
+ if (match) {
228
+ // Two capture groups: match[1] for "port XXXX", match[2] for ":XXXX"
229
+ this.meta.detectedPort = parseInt(match[1] || match[2], 10);
230
+ }
231
+ }
232
+
233
+ // Track user input to detect commands (called from server when PTY receives input)
234
+ trackInput(data) {
235
+ for (let i = 0; i < data.length; i++) {
236
+ const ch = data[i];
237
+ const code = ch.charCodeAt(0);
238
+
239
+ if (ch === '\r' || ch === '\n') {
240
+ // Enter — flush the buffer as a command
241
+ const cmd = this._inputBuffer.trim();
242
+ if (cmd.length > 0 && cmd.length < 500) {
243
+ const clean = cmd.replace(/\x1b\[[A-Za-z0-9;]*[A-Za-z]/g, '').trim();
244
+ if (clean.length > 0) {
245
+ this.meta.lastCommands.push({
246
+ command: clean,
247
+ timestamp: new Date().toISOString()
248
+ });
249
+ if (this.meta.lastCommands.length > 10) {
250
+ this.meta.lastCommands.shift();
251
+ }
252
+ if (this.onCommand) {
253
+ this.onCommand(this.id, clean);
254
+ }
255
+ }
256
+ }
257
+ this._inputBuffer = '';
258
+ } else if (ch === '\x7f' || ch === '\b') {
259
+ this._inputBuffer = this._inputBuffer.slice(0, -1);
260
+ } else if (ch === '\x1b') {
261
+ // Skip escape sequences
262
+ if (i + 1 < data.length && data[i + 1] === '[') {
263
+ i += 2; // skip \x1b[
264
+ while (i < data.length && !/[A-Za-z]/.test(data[i])) i++;
265
+ // i now points at the final letter, loop increment will skip it
266
+ }
267
+ } else if (code >= 32) {
268
+ this._inputBuffer += ch;
269
+ }
270
+ }
271
+ }
272
+
273
+ _detectErrors(clean) {
274
+ if (!PATTERNS.error.test(clean)) return;
275
+
276
+ const oldStatus = this.meta.status;
277
+ this.meta.status = 'errored';
278
+ this.meta.statusDetail = 'Error detected in output';
279
+
280
+ // Mirror status-change callback so T1 sees 'errored' in status_broadcast without
281
+ // waiting for the 3s debounce.
282
+ if (oldStatus !== 'errored' && this.onStatusChange) {
283
+ try { this.onStatusChange(this, oldStatus, 'errored'); } catch {}
284
+ }
285
+
286
+ // Server-side rate limit: at most one error_detected event every 30s per session
287
+ const now = Date.now();
288
+ if (now - this._lastErrorFireAt < 30000) return;
289
+ this._lastErrorFireAt = now;
290
+
291
+ if (this.onErrorDetected) {
292
+ const lastCommand = this.meta.lastCommands.length > 0
293
+ ? this.meta.lastCommands[this.meta.lastCommands.length - 1].command
294
+ : '';
295
+ const tail = this._outputBuffer.slice(-200).replace(/\x1b\[[\?]?[0-9;]*[A-Za-z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
296
+ try {
297
+ this.onErrorDetected(this, { lastCommand, tail });
298
+ } catch (err) {
299
+ console.error('[session] onErrorDetected handler error:', err);
300
+ }
301
+ }
302
+ }
303
+
304
+ _extractCommands(data) {
305
+ // Output-based command extraction as fallback (e.g. for commands echoed by shell)
306
+ // Primary command tracking is via trackInput()
307
+ }
308
+
309
+ _countRequests(data) {
310
+ const globalRequest = new RegExp(PATTERNS.pythonServer.request.source, 'gm');
311
+ const matches = data.match(globalRequest);
312
+ if (matches) {
313
+ this.meta.requestCount += matches.length;
314
+ }
315
+ }
316
+
317
+ toJSON() {
318
+ return {
319
+ id: this.id,
320
+ pid: this.pid,
321
+ meta: { ...this.meta }
322
+ };
323
+ }
324
+
325
+ destroy() {
326
+ clearTimeout(this._outputFlushTimer);
327
+ clearTimeout(this._statusChangeTimer);
328
+ this._outputBuffer = '';
329
+ }
330
+ }
331
+
332
+ class SessionManager {
333
+ constructor(db) {
334
+ this.sessions = new Map();
335
+ this.db = db;
336
+ this._listeners = new Map(); // event listeners
337
+ }
338
+
339
+ create(options) {
340
+ const session = new Session(options);
341
+ this.sessions.set(session.id, session);
342
+
343
+ // Persist to SQLite
344
+ if (this.db) {
345
+ this.db.prepare(`
346
+ INSERT INTO sessions (id, type, project, label, command, cwd, created_at, reason, theme)
347
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
348
+ `).run(
349
+ session.id,
350
+ session.meta.type,
351
+ session.meta.project,
352
+ session.meta.label,
353
+ session.meta.command,
354
+ session.meta.cwd,
355
+ session.meta.createdAt,
356
+ session.meta.reason,
357
+ session.meta.theme
358
+ );
359
+ }
360
+
361
+ this._emit('session:created', session);
362
+ return session;
363
+ }
364
+
365
+ get(id) {
366
+ return this.sessions.get(id);
367
+ }
368
+
369
+ getAll() {
370
+ return Array.from(this.sessions.values()).map(s => s.toJSON());
371
+ }
372
+
373
+ updateMeta(id, updates) {
374
+ const session = this.sessions.get(id);
375
+ if (!session) return null;
376
+
377
+ Object.assign(session.meta, updates);
378
+
379
+ // Persist theme changes to SQLite
380
+ if (updates.theme && this.db) {
381
+ this.db.prepare('UPDATE sessions SET theme = ? WHERE id = ?')
382
+ .run(updates.theme, id);
383
+ }
384
+
385
+ this._emit('session:updated', session);
386
+ return session;
387
+ }
388
+
389
+ remove(id) {
390
+ const session = this.sessions.get(id);
391
+ if (!session) return false;
392
+
393
+ session.destroy();
394
+ this.sessions.delete(id);
395
+
396
+ if (this.db) {
397
+ this.db.prepare(`
398
+ UPDATE sessions SET exited_at = ?, exit_code = ? WHERE id = ?
399
+ `).run(new Date().toISOString(), session.meta.exitCode, id);
400
+ }
401
+
402
+ this._emit('session:removed', session);
403
+ return true;
404
+ }
405
+
406
+ on(event, fn) {
407
+ if (!this._listeners.has(event)) {
408
+ this._listeners.set(event, []);
409
+ }
410
+ this._listeners.get(event).push(fn);
411
+ }
412
+
413
+ _emit(event, data) {
414
+ const fns = this._listeners.get(event) || [];
415
+ for (const fn of fns) {
416
+ try { fn(data); } catch (e) { console.error(`[events] handler error for ${event}:`, e); }
417
+ }
418
+ }
419
+ }
420
+
421
+ module.exports = { Session, SessionManager, PATTERNS };
@@ -0,0 +1,250 @@
1
+ // Terminal themes for xterm.js
2
+ // Each theme is a complete xterm ITheme object
3
+
4
+ const themes = {
5
+ 'tokyo-night': {
6
+ label: 'Tokyo Night',
7
+ category: 'dark',
8
+ theme: {
9
+ background: '#1a1b26',
10
+ foreground: '#c0caf5',
11
+ cursor: '#c0caf5',
12
+ cursorAccent: '#1a1b26',
13
+ selectionBackground: '#33467c',
14
+ selectionForeground: '#c0caf5',
15
+ black: '#15161e',
16
+ red: '#f7768e',
17
+ green: '#9ece6a',
18
+ yellow: '#e0af68',
19
+ blue: '#7aa2f7',
20
+ magenta: '#bb9af7',
21
+ cyan: '#7dcfff',
22
+ white: '#a9b1d6',
23
+ brightBlack: '#414868',
24
+ brightRed: '#f7768e',
25
+ brightGreen: '#9ece6a',
26
+ brightYellow: '#e0af68',
27
+ brightBlue: '#7aa2f7',
28
+ brightMagenta: '#bb9af7',
29
+ brightCyan: '#7dcfff',
30
+ brightWhite: '#c0caf5'
31
+ }
32
+ },
33
+
34
+ 'rose-pine-dawn': {
35
+ label: 'Rosé Pine Dawn',
36
+ category: 'light',
37
+ theme: {
38
+ background: '#faf4ed',
39
+ foreground: '#575279',
40
+ cursor: '#575279',
41
+ cursorAccent: '#faf4ed',
42
+ selectionBackground: '#dfdad9',
43
+ selectionForeground: '#575279',
44
+ black: '#f2e9e1',
45
+ red: '#b4637a',
46
+ green: '#286983',
47
+ yellow: '#ea9d34',
48
+ blue: '#56949f',
49
+ magenta: '#907aa9',
50
+ cyan: '#d7827e',
51
+ white: '#575279',
52
+ brightBlack: '#9893a5',
53
+ brightRed: '#b4637a',
54
+ brightGreen: '#286983',
55
+ brightYellow: '#ea9d34',
56
+ brightBlue: '#56949f',
57
+ brightMagenta: '#907aa9',
58
+ brightCyan: '#d7827e',
59
+ brightWhite: '#575279'
60
+ }
61
+ },
62
+
63
+ 'catppuccin-mocha': {
64
+ label: 'Catppuccin Mocha',
65
+ category: 'dark',
66
+ theme: {
67
+ background: '#1e1e2e',
68
+ foreground: '#cdd6f4',
69
+ cursor: '#f5e0dc',
70
+ cursorAccent: '#1e1e2e',
71
+ selectionBackground: '#45475a',
72
+ selectionForeground: '#cdd6f4',
73
+ black: '#45475a',
74
+ red: '#f38ba8',
75
+ green: '#a6e3a1',
76
+ yellow: '#f9e2af',
77
+ blue: '#89b4fa',
78
+ magenta: '#f5c2e7',
79
+ cyan: '#94e2d5',
80
+ white: '#bac2de',
81
+ brightBlack: '#585b70',
82
+ brightRed: '#f38ba8',
83
+ brightGreen: '#a6e3a1',
84
+ brightYellow: '#f9e2af',
85
+ brightBlue: '#89b4fa',
86
+ brightMagenta: '#f5c2e7',
87
+ brightCyan: '#94e2d5',
88
+ brightWhite: '#a6adc8'
89
+ }
90
+ },
91
+
92
+ 'github-light': {
93
+ label: 'GitHub Light',
94
+ category: 'light',
95
+ theme: {
96
+ background: '#ffffff',
97
+ foreground: '#24292f',
98
+ cursor: '#044289',
99
+ cursorAccent: '#ffffff',
100
+ selectionBackground: '#0969da33',
101
+ selectionForeground: '#24292f',
102
+ black: '#24292f',
103
+ red: '#cf222e',
104
+ green: '#116329',
105
+ yellow: '#4d2d00',
106
+ blue: '#0550ae',
107
+ magenta: '#8250df',
108
+ cyan: '#1b7c83',
109
+ white: '#6e7781',
110
+ brightBlack: '#57606a',
111
+ brightRed: '#a40e26',
112
+ brightGreen: '#1a7f37',
113
+ brightYellow: '#633c01',
114
+ brightBlue: '#0969da',
115
+ brightMagenta: '#8250df',
116
+ brightCyan: '#3192aa',
117
+ brightWhite: '#8c959f'
118
+ }
119
+ },
120
+
121
+ 'dracula': {
122
+ label: 'Dracula',
123
+ category: 'dark',
124
+ theme: {
125
+ background: '#282a36',
126
+ foreground: '#f8f8f2',
127
+ cursor: '#f8f8f2',
128
+ cursorAccent: '#282a36',
129
+ selectionBackground: '#44475a',
130
+ selectionForeground: '#f8f8f2',
131
+ black: '#21222c',
132
+ red: '#ff5555',
133
+ green: '#50fa7b',
134
+ yellow: '#f1fa8c',
135
+ blue: '#bd93f9',
136
+ magenta: '#ff79c6',
137
+ cyan: '#8be9fd',
138
+ white: '#f8f8f2',
139
+ brightBlack: '#6272a4',
140
+ brightRed: '#ff6e6e',
141
+ brightGreen: '#69ff94',
142
+ brightYellow: '#ffffa5',
143
+ brightBlue: '#d6acff',
144
+ brightMagenta: '#ff92df',
145
+ brightCyan: '#a4ffff',
146
+ brightWhite: '#ffffff'
147
+ }
148
+ },
149
+
150
+ 'solarized-dark': {
151
+ label: 'Solarized Dark',
152
+ category: 'dark',
153
+ theme: {
154
+ background: '#002b36',
155
+ foreground: '#839496',
156
+ cursor: '#839496',
157
+ cursorAccent: '#002b36',
158
+ selectionBackground: '#073642',
159
+ selectionForeground: '#93a1a1',
160
+ black: '#073642',
161
+ red: '#dc322f',
162
+ green: '#859900',
163
+ yellow: '#b58900',
164
+ blue: '#268bd2',
165
+ magenta: '#d33682',
166
+ cyan: '#2aa198',
167
+ white: '#eee8d5',
168
+ brightBlack: '#586e75',
169
+ brightRed: '#cb4b16',
170
+ brightGreen: '#586e75',
171
+ brightYellow: '#657b83',
172
+ brightBlue: '#839496',
173
+ brightMagenta: '#6c71c4',
174
+ brightCyan: '#93a1a1',
175
+ brightWhite: '#fdf6e3'
176
+ }
177
+ },
178
+
179
+ 'nord': {
180
+ label: 'Nord',
181
+ category: 'dark',
182
+ theme: {
183
+ background: '#2e3440',
184
+ foreground: '#d8dee9',
185
+ cursor: '#d8dee9',
186
+ cursorAccent: '#2e3440',
187
+ selectionBackground: '#434c5e',
188
+ selectionForeground: '#d8dee9',
189
+ black: '#3b4252',
190
+ red: '#bf616a',
191
+ green: '#a3be8c',
192
+ yellow: '#ebcb8b',
193
+ blue: '#81a1c1',
194
+ magenta: '#b48ead',
195
+ cyan: '#88c0d0',
196
+ white: '#e5e9f0',
197
+ brightBlack: '#4c566a',
198
+ brightRed: '#bf616a',
199
+ brightGreen: '#a3be8c',
200
+ brightYellow: '#ebcb8b',
201
+ brightBlue: '#81a1c1',
202
+ brightMagenta: '#b48ead',
203
+ brightCyan: '#8fbcbb',
204
+ brightWhite: '#eceff4'
205
+ }
206
+ },
207
+
208
+ 'gruvbox-dark': {
209
+ label: 'Gruvbox Dark',
210
+ category: 'dark',
211
+ theme: {
212
+ background: '#282828',
213
+ foreground: '#ebdbb2',
214
+ cursor: '#ebdbb2',
215
+ cursorAccent: '#282828',
216
+ selectionBackground: '#504945',
217
+ selectionForeground: '#ebdbb2',
218
+ black: '#282828',
219
+ red: '#cc241d',
220
+ green: '#98971a',
221
+ yellow: '#d79921',
222
+ blue: '#458588',
223
+ magenta: '#b16286',
224
+ cyan: '#689d6a',
225
+ white: '#a89984',
226
+ brightBlack: '#928374',
227
+ brightRed: '#fb4934',
228
+ brightGreen: '#b8bb26',
229
+ brightYellow: '#fabd2f',
230
+ brightBlue: '#83a598',
231
+ brightMagenta: '#d3869b',
232
+ brightCyan: '#8ec07c',
233
+ brightWhite: '#ebdbb2'
234
+ }
235
+ }
236
+ };
237
+
238
+ // Status indicator colors (for the dot next to terminal name)
239
+ const statusColors = {
240
+ starting: '#7aa2f7', // blue
241
+ active: '#9ece6a', // green
242
+ idle: '#888780', // gray
243
+ thinking: '#bb9af7', // purple
244
+ editing: '#e0af68', // amber
245
+ listening: '#7dcfff', // cyan
246
+ errored: '#f7768e', // red
247
+ exited: '#414868' // dim
248
+ };
249
+
250
+ module.exports = { themes, statusColors };