claude-starter 1.2.0 → 1.3.1
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 +50 -44
- package/index.js +626 -241
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* claude-starter # Launch interactive TUI
|
|
10
10
|
* claude-starter --list # Print sessions as a table (no TUI)
|
|
11
11
|
* claude-starter --list N # Print the latest N sessions
|
|
12
|
+
* claude-starter --version # Show version
|
|
13
|
+
* claude-starter --update # Update to the latest version
|
|
12
14
|
*
|
|
13
15
|
* Keyboard shortcuts (TUI mode):
|
|
14
16
|
* ↑/↓ Navigate sessions
|
|
@@ -16,13 +18,14 @@
|
|
|
16
18
|
* / Start search (fuzzy filter)
|
|
17
19
|
* Esc Clear search / cancel
|
|
18
20
|
* p Filter by project (popup)
|
|
19
|
-
* s Cycle sort: time → size → messages → project
|
|
21
|
+
* s Cycle sort: time → size → messages → project
|
|
20
22
|
* n Start new session
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* d Resume with bypassPermissions (danger mode)
|
|
24
|
+
* m Permission mode picker
|
|
23
25
|
* Home / End Jump to top / bottom
|
|
24
26
|
* Ctrl-D/U Page down / up
|
|
25
27
|
* c Copy session ID to clipboard
|
|
28
|
+
* x / Delete Delete selected session
|
|
26
29
|
* q / Ctrl-C Quit
|
|
27
30
|
*/
|
|
28
31
|
|
|
@@ -34,37 +37,72 @@ const os = require('os');
|
|
|
34
37
|
|
|
35
38
|
// ─── CLI Detection ──────────────────────────────────────────────────────────
|
|
36
39
|
// Detect whether `mai-claude` is available (binary, alias, or function).
|
|
37
|
-
//
|
|
38
|
-
//
|
|
40
|
+
// First checks PATH directly, then sources shell config non-interactively
|
|
41
|
+
// to resolve aliases. Falls back to plain `claude`.
|
|
42
|
+
//
|
|
43
|
+
// NOTE: We deliberately avoid `shell -i` (interactive mode) because it
|
|
44
|
+
// triggers SIGTTOU in terminals like Warp that strictly manage TTY process
|
|
45
|
+
// groups, causing `suspended (tty output)`.
|
|
39
46
|
//
|
|
40
47
|
// Returns { name, cmd } where:
|
|
41
48
|
// name = display label ("mai-claude" or "claude")
|
|
42
49
|
// cmd = the actual command string to spawn (resolves aliases)
|
|
43
50
|
|
|
44
51
|
function detectCLI() {
|
|
52
|
+
// Strategy:
|
|
53
|
+
// 1. First try non-interactive lookup (safe for all terminals including Warp)
|
|
54
|
+
// 2. Only fall back to interactive shell if needed for alias resolution
|
|
55
|
+
//
|
|
56
|
+
// IMPORTANT: avoid `shell -i` (interactive mode) — it can trigger SIGTTOU
|
|
57
|
+
// in terminals like Warp that strictly manage TTY process groups, causing
|
|
58
|
+
// the process to be suspended with "suspended (tty output)".
|
|
59
|
+
|
|
45
60
|
const shell = process.env.SHELL || '/bin/sh';
|
|
61
|
+
|
|
62
|
+
// 1) Non-interactive: check if mai-claude exists as a binary on PATH
|
|
46
63
|
try {
|
|
47
|
-
const
|
|
64
|
+
const binPath = execSync('command -v mai-claude 2>/dev/null', {
|
|
48
65
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
66
|
timeout: 3000,
|
|
67
|
+
shell: true,
|
|
50
68
|
}).toString().trim();
|
|
69
|
+
if (binPath) {
|
|
70
|
+
return { name: 'mai-claude', cmd: 'mai-claude' };
|
|
71
|
+
}
|
|
72
|
+
} catch { /* not found as binary, continue */ }
|
|
51
73
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
74
|
+
// 2) Source shell config non-interactively to resolve aliases/functions.
|
|
75
|
+
// This avoids `-i` which would try to claim the TTY and risk SIGTTOU.
|
|
76
|
+
try {
|
|
77
|
+
const isZsh = shell.endsWith('/zsh');
|
|
78
|
+
const rcFile = isZsh
|
|
79
|
+
? path.join(os.homedir(), '.zshrc')
|
|
80
|
+
: path.join(os.homedir(), '.bashrc');
|
|
81
|
+
|
|
82
|
+
if (fs.existsSync(rcFile)) {
|
|
83
|
+
const raw = execSync(
|
|
84
|
+
`${shell} -c 'source "${rcFile}" 2>/dev/null; command -v mai-claude 2>/dev/null'`,
|
|
85
|
+
{
|
|
86
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
87
|
+
timeout: 3000,
|
|
88
|
+
env: { ...process.env, PS1: '', PROMPT: '', NO_TTY: '1' },
|
|
89
|
+
},
|
|
90
|
+
).toString().trim();
|
|
91
|
+
|
|
92
|
+
if (raw) {
|
|
93
|
+
const lines = raw.split('\n');
|
|
94
|
+
const aliasLine = lines.find(l => l.startsWith('alias ')) || lines[lines.length - 1];
|
|
95
|
+
|
|
96
|
+
const aliasMatch = aliasLine.match(/^alias [^=]+=(?:'(.+)'|"(.+)")$/s);
|
|
97
|
+
if (aliasMatch) {
|
|
98
|
+
return { name: 'mai-claude', cmd: aliasMatch[1] || aliasMatch[2] };
|
|
99
|
+
}
|
|
100
|
+
return { name: 'mai-claude', cmd: 'mai-claude' };
|
|
101
|
+
}
|
|
62
102
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return { name: 'claude', cmd: 'claude' };
|
|
67
|
-
}
|
|
103
|
+
} catch { /* alias resolution failed, fall back to claude */ }
|
|
104
|
+
|
|
105
|
+
return { name: 'claude', cmd: 'claude' };
|
|
68
106
|
}
|
|
69
107
|
|
|
70
108
|
const CLI = detectCLI();
|
|
@@ -80,11 +118,8 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
|
80
118
|
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
81
119
|
const META_FILE = path.join(CLAUDE_DIR, 'claude-starter-meta.json');
|
|
82
120
|
|
|
83
|
-
// ─── Session Meta
|
|
121
|
+
// ─── Session Meta ────────────────────────────────────────────────────
|
|
84
122
|
// Stores user-defined metadata for sessions in a simple JSON file.
|
|
85
|
-
// Schema: { "sessions": { "<sessionId>": { "favorite": bool, "tags": string[] } } }
|
|
86
|
-
|
|
87
|
-
const DEFAULT_TAGS = ['bug-fix', 'feature', 'refactor', 'debug', 'review', 'config', 'docs', 'experiment'];
|
|
88
123
|
|
|
89
124
|
function loadMeta() {
|
|
90
125
|
try {
|
|
@@ -95,6 +130,8 @@ function loadMeta() {
|
|
|
95
130
|
return { sessions: {} };
|
|
96
131
|
}
|
|
97
132
|
|
|
133
|
+
const PERMISSION_MODES = ['default', 'bypassPermissions', 'acceptEdits', 'dontAsk', 'plan', 'auto'];
|
|
134
|
+
|
|
98
135
|
function saveMeta(meta) {
|
|
99
136
|
try {
|
|
100
137
|
fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2), 'utf-8');
|
|
@@ -102,38 +139,63 @@ function saveMeta(meta) {
|
|
|
102
139
|
}
|
|
103
140
|
|
|
104
141
|
function getSessionMeta(meta, sessionId) {
|
|
105
|
-
return meta.sessions[sessionId] || {
|
|
142
|
+
return meta.sessions[sessionId] || {};
|
|
106
143
|
}
|
|
107
144
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return
|
|
145
|
+
function getEffectivePermissionMode(meta, session) {
|
|
146
|
+
// Priority: per-session override > session's original mode from JSONL > global default
|
|
147
|
+
const sm = meta.sessions[session.sessionId];
|
|
148
|
+
if (sm && sm.permissionMode) return sm.permissionMode;
|
|
149
|
+
if (session.permissionMode) return session.permissionMode;
|
|
150
|
+
if (meta.defaultPermissionMode) return meta.defaultPermissionMode;
|
|
151
|
+
return '';
|
|
113
152
|
}
|
|
114
153
|
|
|
115
|
-
function
|
|
116
|
-
if (!meta.sessions[sessionId]) meta.sessions[sessionId] = {
|
|
117
|
-
meta.sessions[sessionId].
|
|
154
|
+
function setSessionPermissionMode(meta, sessionId, mode) {
|
|
155
|
+
if (!meta.sessions[sessionId]) meta.sessions[sessionId] = {};
|
|
156
|
+
meta.sessions[sessionId].permissionMode = mode || undefined;
|
|
157
|
+
if (!mode) delete meta.sessions[sessionId].permissionMode;
|
|
118
158
|
saveMeta(meta);
|
|
119
159
|
}
|
|
120
160
|
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
return [...tags];
|
|
161
|
+
function setGlobalPermissionMode(meta, mode) {
|
|
162
|
+
meta.defaultPermissionMode = mode || undefined;
|
|
163
|
+
if (!mode) delete meta.defaultPermissionMode;
|
|
164
|
+
saveMeta(meta);
|
|
127
165
|
}
|
|
128
166
|
|
|
167
|
+
|
|
129
168
|
// ─── Data Layer ──────────────────────────────────────────────────────────────
|
|
130
169
|
|
|
131
170
|
function getProjectDisplayName(dirName) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
171
|
+
// Claude stores projects as path with `-` separators, e.g.:
|
|
172
|
+
// -Users-bob-Desktop-MSProject-my-app
|
|
173
|
+
// -Users-bob-Projects-Router-Maestro
|
|
174
|
+
// -Users-bob-Desktop-GraphConnector
|
|
175
|
+
// -Users-bob
|
|
176
|
+
//
|
|
177
|
+
// Strategy: strip the user home prefix, then take the last meaningful path segment.
|
|
178
|
+
// This gives clean names like "my-app", "Router-Maestro", "GraphConnector".
|
|
179
|
+
|
|
180
|
+
// Remove leading -Users-<username> prefix
|
|
181
|
+
let name = dirName.replace(/^-Users-[^-]+/, '');
|
|
182
|
+
|
|
183
|
+
// If nothing left, it was just the home dir
|
|
184
|
+
if (!name || name === '-') return '~';
|
|
185
|
+
|
|
186
|
+
// Remove leading dash
|
|
187
|
+
name = name.replace(/^-/, '');
|
|
188
|
+
|
|
189
|
+
// Get the last path segment (split by common directory markers)
|
|
190
|
+
// e.g. "Desktop-MSProject-my-app" → "my-app"
|
|
191
|
+
// "Desktop-GraphConnector" → "GraphConnector"
|
|
192
|
+
// "Projects-Router-Maestro" → "Router-Maestro"
|
|
193
|
+
const knownPrefixes = /^(Desktop|Documents|Projects|Downloads|dev|src|code|repos|work|home)(?:-|$)/i;
|
|
194
|
+
while (knownPrefixes.test(name)) {
|
|
195
|
+
name = name.replace(/^[^-]+-?/, '');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return name || dirName.split('-').pop() || '~';
|
|
137
199
|
}
|
|
138
200
|
|
|
139
201
|
function loadSessionQuick(filePath, projectName) {
|
|
@@ -144,19 +206,30 @@ function loadSessionQuick(filePath, projectName) {
|
|
|
144
206
|
const headBuf = Buffer.alloc(Math.min(8192, stat.size));
|
|
145
207
|
fs.readSync(fd, headBuf, 0, headBuf.length, 0);
|
|
146
208
|
|
|
147
|
-
|
|
209
|
+
// Read tail with progressive expansion: start at 32KB, grow up to 256KB
|
|
210
|
+
// until we find a JSON line with a top-level timestamp (to get accurate lastTs).
|
|
211
|
+
let tailStr = '';
|
|
148
212
|
if (stat.size > 8192) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
213
|
+
const tailSizes = [32768, 65536, 131072, 262144];
|
|
214
|
+
for (const ts of tailSizes) {
|
|
215
|
+
const tailSize = Math.min(ts, stat.size - 8192);
|
|
216
|
+
const tailBuf = Buffer.alloc(tailSize);
|
|
217
|
+
fs.readSync(fd, tailBuf, 0, tailSize, stat.size - tailSize);
|
|
218
|
+
tailStr = tailBuf.toString('utf-8');
|
|
219
|
+
// Check if any parseable JSON line has a top-level timestamp
|
|
220
|
+
const hasTopLevelTs = tailStr.split('\n').some(line => {
|
|
221
|
+
try { return !!JSON.parse(line).timestamp; } catch { return false; }
|
|
222
|
+
});
|
|
223
|
+
if (hasTopLevelTs) break;
|
|
224
|
+
if (tailSize >= stat.size - 8192) break; // already read entire file
|
|
225
|
+
}
|
|
152
226
|
}
|
|
153
227
|
fs.closeSync(fd);
|
|
154
228
|
|
|
155
229
|
const headStr = headBuf.toString('utf-8');
|
|
156
|
-
const tailStr = tailBuf.toString('utf-8');
|
|
157
230
|
|
|
158
231
|
let firstTs = null, lastTs = null;
|
|
159
|
-
let version = '', gitBranch = '', cwd = '';
|
|
232
|
+
let version = '', gitBranch = '', cwd = '', permissionMode = '';
|
|
160
233
|
let firstUserMsg = '';
|
|
161
234
|
let userMsgCount = 0;
|
|
162
235
|
let customTitle = '';
|
|
@@ -171,6 +244,7 @@ function loadSessionQuick(filePath, projectName) {
|
|
|
171
244
|
if (!version && d.version) version = d.version;
|
|
172
245
|
if (!gitBranch && d.gitBranch) gitBranch = d.gitBranch;
|
|
173
246
|
if (!cwd && d.cwd) cwd = d.cwd;
|
|
247
|
+
if (!permissionMode && d.permissionMode) permissionMode = d.permissionMode;
|
|
174
248
|
if (d.type === 'custom-title' && d.customTitle) customTitle = d.customTitle;
|
|
175
249
|
if (d.type === 'user') {
|
|
176
250
|
userMsgCount++;
|
|
@@ -209,7 +283,7 @@ function loadSessionQuick(filePath, projectName) {
|
|
|
209
283
|
return {
|
|
210
284
|
sessionId, project: projectName,
|
|
211
285
|
topic: topic || '(no user messages)',
|
|
212
|
-
customTitle,
|
|
286
|
+
customTitle, permissionMode,
|
|
213
287
|
firstTs, lastTs, version, gitBranch, cwd,
|
|
214
288
|
fileSize: stat.size, duration: durationStr,
|
|
215
289
|
estimatedMessages, filePath, _detailLoaded: false,
|
|
@@ -383,12 +457,17 @@ function createApp() {
|
|
|
383
457
|
|
|
384
458
|
// ─── Screen ────────────────────────────────────────────────────────────
|
|
385
459
|
const screen = blessed.screen({
|
|
386
|
-
smartCSR:
|
|
460
|
+
smartCSR: false,
|
|
461
|
+
fastCSR: false,
|
|
387
462
|
title: 'Claude Starter',
|
|
388
463
|
fullUnicode: true,
|
|
389
464
|
autoPadding: true,
|
|
465
|
+
dockBorders: true,
|
|
390
466
|
});
|
|
391
467
|
|
|
468
|
+
// Force screen-level fill color so no terminal bg leaks through
|
|
469
|
+
screen.style = { bg: 234 }; // 234 = xterm color closest to #1a1b26
|
|
470
|
+
|
|
392
471
|
// ─── Header ────────────────────────────────────────────────────────────
|
|
393
472
|
const header = blessed.box({
|
|
394
473
|
parent: screen, top: 0, left: 0, width: '100%', height: 3,
|
|
@@ -396,23 +475,20 @@ function createApp() {
|
|
|
396
475
|
});
|
|
397
476
|
|
|
398
477
|
function updateHeader() {
|
|
399
|
-
const title = '{bold}{#7aa2f7-fg}
|
|
478
|
+
const title = '{bold}{#7aa2f7-fg}Claude Starter{/}';
|
|
400
479
|
const count = `{#9ece6a-fg}${filteredSessions.length}{/}{#565f89-fg}/${allSessions.length} sessions{/}`;
|
|
401
480
|
const proj = `{#bb9af7-fg}${uniqueProjects.length}{/}{#565f89-fg} projects{/}`;
|
|
402
|
-
const
|
|
403
|
-
const fav = favCount > 0 ? `{#e0af68-fg}⭐${favCount}{/}` : '';
|
|
404
|
-
const sort = `{#73daca-fg}↕${sortMode}{/}`;
|
|
481
|
+
const sort = `{#73daca-fg}[${sortMode}]{/}`;
|
|
405
482
|
const search = isSearchMode
|
|
406
483
|
? `{#e0af68-fg}/ ${filterText}▌{/}`
|
|
407
484
|
: (filterText ? `{#e0af68-fg}/ ${filterText}{/}` : '');
|
|
408
485
|
let parts = [title, count, proj];
|
|
409
|
-
if (fav) parts.push(fav);
|
|
410
486
|
parts.push(sort);
|
|
411
487
|
if (search) parts.push(search);
|
|
412
488
|
header.setContent(`\n ${parts.join(' {#414868-fg}│{/} ')}`);
|
|
413
489
|
}
|
|
414
490
|
|
|
415
|
-
blessed.line({ parent: screen, top: 3, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868' } });
|
|
491
|
+
blessed.line({ parent: screen, top: 3, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868', bg: '#1a1b26' } });
|
|
416
492
|
|
|
417
493
|
// ─── Left Panel: blessed.list for correct scroll tracking ──────────────
|
|
418
494
|
const listPanel = blessed.list({
|
|
@@ -433,7 +509,7 @@ function createApp() {
|
|
|
433
509
|
interactive: true,
|
|
434
510
|
});
|
|
435
511
|
|
|
436
|
-
blessed.line({ parent: screen, top: 4, left: '50%', height: '100%-7', orientation: 'vertical', style: { fg: '#414868' } });
|
|
512
|
+
blessed.line({ parent: screen, top: 4, left: '50%', height: '100%-7', orientation: 'vertical', style: { fg: '#414868', bg: '#1a1b26' } });
|
|
437
513
|
|
|
438
514
|
// ─── Right Panel ───────────────────────────────────────────────────────
|
|
439
515
|
const detailPanel = blessed.box({
|
|
@@ -445,7 +521,7 @@ function createApp() {
|
|
|
445
521
|
mouse: true,
|
|
446
522
|
});
|
|
447
523
|
|
|
448
|
-
blessed.line({ parent: screen, bottom: 2, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868' } });
|
|
524
|
+
blessed.line({ parent: screen, bottom: 2, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868', bg: '#1a1b26' } });
|
|
449
525
|
|
|
450
526
|
// ─── Footer ────────────────────────────────────────────────────────────
|
|
451
527
|
const footer = blessed.box({
|
|
@@ -455,15 +531,17 @@ function createApp() {
|
|
|
455
531
|
|
|
456
532
|
function updateFooter() {
|
|
457
533
|
const keys = [
|
|
458
|
-
'{#
|
|
459
|
-
'{#7aa2f7-fg}{bold}
|
|
460
|
-
'{#
|
|
461
|
-
'{#
|
|
462
|
-
'{#
|
|
463
|
-
'{#
|
|
464
|
-
'{#
|
|
465
|
-
'{#
|
|
466
|
-
'{#
|
|
534
|
+
'{#9ece6a-fg}{bold}n{/} {#9ece6a-fg}New{/}',
|
|
535
|
+
'{#7aa2f7-fg}{bold}↵{/} {#7aa2f7-fg}Resume{/}',
|
|
536
|
+
'{#bb9af7-fg}{bold}m{/} {#bb9af7-fg}Mode{/}',
|
|
537
|
+
'{#f7768e-fg}{bold}d{/} {#f7768e-fg}Danger{/}',
|
|
538
|
+
'{#e0af68-fg}{bold}/{/} {#e0af68-fg}Search{/}',
|
|
539
|
+
'{#7dcfff-fg}{bold}p{/} {#7dcfff-fg}Project{/}',
|
|
540
|
+
'{#73daca-fg}{bold}s{/} {#73daca-fg}Sort{/}',
|
|
541
|
+
'{#565f89-fg}{bold}c{/} {#565f89-fg}Copy ID{/}',
|
|
542
|
+
'{#ff9e64-fg}{bold}r{/} {#ff9e64-fg}Rename{/}',
|
|
543
|
+
'{#f7768e-fg}{bold}x{/} {#f7768e-fg}Delete{/}',
|
|
544
|
+
'{#565f89-fg}{bold}q{/} {#565f89-fg}Quit{/}',
|
|
467
545
|
];
|
|
468
546
|
footer.setContent(`\n ${keys.join(' {#414868-fg}│{/} ')}`);
|
|
469
547
|
}
|
|
@@ -486,7 +564,7 @@ function createApp() {
|
|
|
486
564
|
const branch = session.gitBranch
|
|
487
565
|
? `{#73daca-fg}${session.gitBranch.substring(0, 25)}{/}`
|
|
488
566
|
: '';
|
|
489
|
-
const dur = session.duration ? `{#565f89-fg}
|
|
567
|
+
const dur = session.duration ? `{#565f89-fg}${session.duration}{/}` : '';
|
|
490
568
|
|
|
491
569
|
// Compose a multi-line string for each list item.
|
|
492
570
|
// blessed.list renders each item as a single row, so we pack info densely.
|
|
@@ -507,42 +585,30 @@ function createApp() {
|
|
|
507
585
|
|
|
508
586
|
// ─── Populate list ─────────────────────────────────────────────────────
|
|
509
587
|
// Index 0 = "New Session", index 1+ = sessions
|
|
510
|
-
const NEW_SESSION_LABEL = ' {#9ece6a-fg}{bold}
|
|
588
|
+
const NEW_SESSION_LABEL = ' {#9ece6a-fg}{bold}+ New Conversation{/}';
|
|
511
589
|
|
|
512
590
|
function refreshList() {
|
|
513
591
|
const listW = Math.floor((screen.width || 100) / 2) - 2;
|
|
514
592
|
|
|
515
593
|
const sessionItems = filteredSessions.map((session) => {
|
|
516
594
|
const color = getProjectColor(session.project, projectColorMap);
|
|
517
|
-
const
|
|
518
|
-
const
|
|
595
|
+
const eMode = getEffectivePermissionMode(meta, session);
|
|
596
|
+
const modeIcon = (eMode === 'bypassPermissions') ? '{#f7768e-fg}!{/}' : ' ';
|
|
519
597
|
const proj = `{${color}-fg}${session.project.substring(0, 12).padEnd(12)}{/}`;
|
|
520
598
|
const time = `{#e0af68-fg}${formatTimestamp(session.lastTs).padEnd(16)}{/}`;
|
|
521
|
-
const msgs = `{#7aa2f7-fg}${String(session.estimatedMessages).padStart(4)}{/}{#565f89-fg}m{/}`;
|
|
522
599
|
|
|
523
|
-
const fixedLen =
|
|
600
|
+
const fixedLen = 1 + 12 + 1 + 16 + 1 + 3;
|
|
524
601
|
const topicMaxLen = Math.max(10, listW - fixedLen);
|
|
525
602
|
let topic = session.customTitle || session.topic;
|
|
526
603
|
|
|
527
|
-
|
|
528
|
-
const tagStr = sm.tags.length > 0
|
|
529
|
-
? ' ' + sm.tags.map(t => `#${t}`).join(' ')
|
|
530
|
-
: '';
|
|
531
|
-
|
|
532
|
-
let display = topic + tagStr;
|
|
533
|
-
if (display.length > topicMaxLen) display = display.substring(0, topicMaxLen) + '…';
|
|
534
|
-
|
|
535
|
-
// Split display back into topic part and tag part for coloring
|
|
536
|
-
const topicPart = display.substring(0, Math.min(topic.length, topicMaxLen));
|
|
537
|
-
const tagPart = display.substring(topicPart.length);
|
|
604
|
+
if (topic.length > topicMaxLen) topic = topic.substring(0, topicMaxLen) + '…';
|
|
538
605
|
|
|
539
|
-
let label = `${
|
|
606
|
+
let label = `${modeIcon}${proj} ${time} `;
|
|
540
607
|
if (session.customTitle) {
|
|
541
|
-
label += `{#73daca-fg}{bold}${esc(
|
|
608
|
+
label += `{#73daca-fg}{bold}${esc(topic)}{/}`;
|
|
542
609
|
} else {
|
|
543
|
-
label += `{#a9b1d6-fg}${esc(
|
|
610
|
+
label += `{#a9b1d6-fg}${esc(topic)}{/}`;
|
|
544
611
|
}
|
|
545
|
-
if (tagPart) label += `{#f7768e-fg}${esc(tagPart)}{/}`;
|
|
546
612
|
|
|
547
613
|
return label;
|
|
548
614
|
});
|
|
@@ -558,14 +624,19 @@ function createApp() {
|
|
|
558
624
|
function renderDetail() {
|
|
559
625
|
if (selectedIndex === -1) {
|
|
560
626
|
const cli = CLI.name;
|
|
627
|
+
const defaultMode = meta.defaultPermissionMode || '';
|
|
628
|
+
const modeFlag = (defaultMode && defaultMode !== 'default') ? ` --permission-mode ${defaultMode}` : '';
|
|
561
629
|
let c = '';
|
|
562
|
-
c += `\n {#9ece6a-fg}{bold}
|
|
630
|
+
c += `\n {#9ece6a-fg}{bold}Start a New Conversation{/}\n`;
|
|
563
631
|
c += ` {#414868-fg}${'─'.repeat(44)}{/}\n\n`;
|
|
564
632
|
c += ` {#a9b1d6-fg}Open a fresh Claude session and start{/}\n`;
|
|
565
633
|
c += ` {#a9b1d6-fg}coding from scratch.{/}\n\n`;
|
|
566
634
|
c += ` {#565f89-fg}Working Dir{/} {#7dcfff-fg}${process.cwd()}{/}\n`;
|
|
567
635
|
c += ` {#565f89-fg}CLI{/} {#73daca-fg}${cli}{/}\n`;
|
|
568
|
-
|
|
636
|
+
if (defaultMode && defaultMode !== 'default') {
|
|
637
|
+
c += ` {#565f89-fg}Mode{/} {#f7768e-fg}${defaultMode}{/}\n`;
|
|
638
|
+
}
|
|
639
|
+
c += ` {#565f89-fg}Command{/} {#565f89-fg}${cli}${modeFlag}{/}\n\n`;
|
|
569
640
|
c += ` {#414868-fg}${'─'.repeat(44)}{/}\n`;
|
|
570
641
|
c += ` {#9ece6a-fg}{bold}↵ Enter{/}{#9ece6a-fg} or {/}{#9ece6a-fg}{bold}n{/}{#9ece6a-fg} to launch{/}\n`;
|
|
571
642
|
detailPanel.setContent(c);
|
|
@@ -582,15 +653,13 @@ function createApp() {
|
|
|
582
653
|
loadSessionDetail(session);
|
|
583
654
|
|
|
584
655
|
const color = getProjectColor(session.project, projectColorMap);
|
|
585
|
-
const sm = getSessionMeta(meta, session.sessionId);
|
|
586
656
|
let c = '';
|
|
587
657
|
const sep = ` {#414868-fg}${'─'.repeat(44)}{/}`;
|
|
588
658
|
|
|
589
|
-
// Title
|
|
590
|
-
|
|
591
|
-
c += `\n {${color}-fg}{bold}█ ${session.project}{/}${favLabel}\n`;
|
|
659
|
+
// Title
|
|
660
|
+
c += `\n {${color}-fg}{bold}█ ${session.project}{/}\n`;
|
|
592
661
|
if (session.customTitle) {
|
|
593
|
-
c += ` {#73daca-fg}{bold}
|
|
662
|
+
c += ` {#73daca-fg}{bold}${esc(session.customTitle)}{/}\n`;
|
|
594
663
|
}
|
|
595
664
|
c += sep + '\n\n';
|
|
596
665
|
|
|
@@ -606,15 +675,14 @@ function createApp() {
|
|
|
606
675
|
if (session.version) fields.push(['Claude', `{#565f89-fg}v${session.version}{/}`]);
|
|
607
676
|
if (session.cwd) fields.push(['Directory', `{#565f89-fg}${session.cwd}{/}`]);
|
|
608
677
|
|
|
609
|
-
|
|
610
|
-
|
|
678
|
+
const effectiveMode = getEffectivePermissionMode(meta, session);
|
|
679
|
+
if (effectiveMode && effectiveMode !== 'default') {
|
|
680
|
+
const modeColor = effectiveMode === 'bypassPermissions' ? '#f7768e' : '#e0af68';
|
|
681
|
+
fields.push(['Mode', `{${modeColor}-fg}${effectiveMode}{/}`]);
|
|
611
682
|
}
|
|
612
683
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const tagChips = sm.tags.map(t => `{#414868-fg}[{/}{#f7768e-fg}#${t}{/}{#414868-fg}]{/}`).join(' ');
|
|
616
|
-
c += `\n {#f7768e-fg}{bold}🏷️ Tags{/}\n`;
|
|
617
|
-
c += ` ${tagChips}\n`;
|
|
684
|
+
for (const [label, value] of fields) {
|
|
685
|
+
c += ` {#565f89-fg}${label.padEnd(12)}{/} ${value}\n`;
|
|
618
686
|
}
|
|
619
687
|
|
|
620
688
|
if (session.toolsUsed && session.toolsUsed.length > 0) {
|
|
@@ -624,7 +692,7 @@ function createApp() {
|
|
|
624
692
|
if (session.toolsUsed.length > 10) c += ` {#565f89-fg}+${session.toolsUsed.length - 10} more{/}\n`;
|
|
625
693
|
}
|
|
626
694
|
|
|
627
|
-
c += `\n {#bb9af7-fg}{bold}
|
|
695
|
+
c += `\n {#bb9af7-fg}{bold}Conversation{/}\n`;
|
|
628
696
|
c += sep + '\n';
|
|
629
697
|
|
|
630
698
|
const msgs = (session.userMessages || []).slice(0, 10);
|
|
@@ -636,11 +704,11 @@ function createApp() {
|
|
|
636
704
|
msgs.forEach((msg, i) => {
|
|
637
705
|
const clean = esc(msg.replace(/\n/g, ' ').trim());
|
|
638
706
|
const trunc = clean.length > 80 ? clean.substring(0, 80) + '…' : clean;
|
|
639
|
-
c += `\n {#7aa2f7-fg}{bold}You
|
|
707
|
+
c += `\n {#7aa2f7-fg}{bold}You >{/} ${trunc}\n`;
|
|
640
708
|
if (assists[i]) {
|
|
641
709
|
const aClean = esc(assists[i].replace(/\n/g, ' ').trim());
|
|
642
710
|
const aTrunc = aClean.length > 80 ? aClean.substring(0, 80) + '…' : aClean;
|
|
643
|
-
c += ` {#9ece6a-fg}Claude
|
|
711
|
+
c += ` {#9ece6a-fg}Claude >{/} {#565f89-fg}${aTrunc}{/}\n`;
|
|
644
712
|
}
|
|
645
713
|
});
|
|
646
714
|
}
|
|
@@ -670,19 +738,9 @@ function createApp() {
|
|
|
670
738
|
} else {
|
|
671
739
|
const terms = filterText.toLowerCase().split(/\s+/);
|
|
672
740
|
filteredSessions = allSessions.filter(s => {
|
|
673
|
-
const sm = getSessionMeta(meta, s.sessionId);
|
|
674
741
|
const haystack = [s.project, s.topic, s.customTitle || '', s.gitBranch || '', s.sessionId, ...(s.userMessages || [])].join(' ').toLowerCase();
|
|
675
742
|
|
|
676
743
|
return terms.every(t => {
|
|
677
|
-
// #tag syntax: match against session tags
|
|
678
|
-
if (t.startsWith('#') && t.length > 1) {
|
|
679
|
-
const tagQuery = t.substring(1);
|
|
680
|
-
return sm.tags.some(tag => tag.toLowerCase().includes(tagQuery));
|
|
681
|
-
}
|
|
682
|
-
// ⭐ or "fav" keyword: match only favorited sessions
|
|
683
|
-
if (t === '⭐' || t === 'fav' || t === 'favorite' || t === 'favorites') {
|
|
684
|
-
return sm.favorite;
|
|
685
|
-
}
|
|
686
744
|
return haystack.includes(t);
|
|
687
745
|
});
|
|
688
746
|
});
|
|
@@ -698,19 +756,13 @@ function createApp() {
|
|
|
698
756
|
|
|
699
757
|
// ─── Sort ──────────────────────────────────────────────────────────────
|
|
700
758
|
function cycleSort() {
|
|
701
|
-
const modes = ['time', 'size', 'messages', 'project'
|
|
759
|
+
const modes = ['time', 'size', 'messages', 'project'];
|
|
702
760
|
sortMode = modes[(modes.indexOf(sortMode) + 1) % modes.length];
|
|
703
761
|
const sorters = {
|
|
704
762
|
time: (a, b) => (new Date(b.lastTs || 0).getTime()) - (new Date(a.lastTs || 0).getTime()),
|
|
705
763
|
size: (a, b) => b.fileSize - a.fileSize,
|
|
706
764
|
messages: (a, b) => b.estimatedMessages - a.estimatedMessages,
|
|
707
765
|
project: (a, b) => a.project.localeCompare(b.project) || (new Date(b.lastTs || 0).getTime()) - (new Date(a.lastTs || 0).getTime()),
|
|
708
|
-
favorites: (a, b) => {
|
|
709
|
-
const fa = getSessionMeta(meta, a.sessionId).favorite ? 1 : 0;
|
|
710
|
-
const fb = getSessionMeta(meta, b.sessionId).favorite ? 1 : 0;
|
|
711
|
-
if (fb !== fa) return fb - fa; // favorites first
|
|
712
|
-
return (new Date(b.lastTs || 0).getTime()) - (new Date(a.lastTs || 0).getTime());
|
|
713
|
-
},
|
|
714
766
|
};
|
|
715
767
|
allSessions.sort(sorters[sortMode]);
|
|
716
768
|
selectedIndex = 0;
|
|
@@ -789,17 +841,15 @@ function createApp() {
|
|
|
789
841
|
}
|
|
790
842
|
|
|
791
843
|
screen.key(['down'], () => {
|
|
792
|
-
if (popupOpen) return;
|
|
793
|
-
if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
|
|
844
|
+
if (renameMode || popupOpen || isSearchMode) return;
|
|
794
845
|
moveSelection(1);
|
|
795
846
|
});
|
|
796
847
|
screen.key(['up'], () => {
|
|
797
|
-
if (popupOpen) return;
|
|
798
|
-
if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
|
|
848
|
+
if (renameMode || popupOpen || isSearchMode) return;
|
|
799
849
|
moveSelection(-1);
|
|
800
850
|
});
|
|
801
851
|
screen.key(['home'], () => {
|
|
802
|
-
if (popupOpen) return;
|
|
852
|
+
if (renameMode || popupOpen) return;
|
|
803
853
|
if (isSearchMode) { isSearchMode = false; }
|
|
804
854
|
selectedIndex = -1;
|
|
805
855
|
suppressSelectEvent = true; listPanel.select(0); suppressSelectEvent = false;
|
|
@@ -807,7 +857,7 @@ function createApp() {
|
|
|
807
857
|
renderDetail(); updateHeader(); screen.render();
|
|
808
858
|
});
|
|
809
859
|
screen.key(['end'], () => {
|
|
810
|
-
if (popupOpen) return;
|
|
860
|
+
if (renameMode || popupOpen) return;
|
|
811
861
|
if (isSearchMode) { isSearchMode = false; }
|
|
812
862
|
selectedIndex = Math.max(0, filteredSessions.length - 1);
|
|
813
863
|
suppressSelectEvent = true; listPanel.select(selectedIndex + 1); suppressSelectEvent = false;
|
|
@@ -815,31 +865,86 @@ function createApp() {
|
|
|
815
865
|
renderDetail(); updateHeader(); screen.render();
|
|
816
866
|
});
|
|
817
867
|
screen.key(['pagedown', 'C-d'], () => {
|
|
818
|
-
if (popupOpen) return;
|
|
868
|
+
if (renameMode || popupOpen) return;
|
|
819
869
|
if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
|
|
820
870
|
moveSelection(Math.floor((listPanel.height || 20) / 2));
|
|
821
871
|
});
|
|
822
872
|
screen.key(['pageup', 'C-u'], () => {
|
|
823
|
-
if (popupOpen) return;
|
|
873
|
+
if (renameMode || popupOpen) return;
|
|
824
874
|
if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
|
|
825
875
|
moveSelection(-Math.floor((listPanel.height || 20) / 2));
|
|
826
876
|
});
|
|
827
877
|
|
|
828
878
|
// Search
|
|
829
879
|
screen.key(['/'], () => {
|
|
830
|
-
if (isSearchMode) return;
|
|
880
|
+
if (renameMode || isSearchMode) return;
|
|
831
881
|
isSearchMode = true; filterText = ''; applyFilter();
|
|
832
882
|
});
|
|
833
883
|
|
|
834
884
|
screen.on('keypress', (ch, key) => {
|
|
835
|
-
//
|
|
836
|
-
if (
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
885
|
+
// ── Rename mode: capture all input ──
|
|
886
|
+
if (renameMode) {
|
|
887
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
888
|
+
const session = renameSession;
|
|
889
|
+
const value = renameValue;
|
|
890
|
+
closeRename();
|
|
891
|
+
submitRename(session, value);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (key.name === 'escape') {
|
|
895
|
+
closeRename();
|
|
896
|
+
listPanel.focus();
|
|
897
|
+
screen.render();
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (key.name === 'backspace') {
|
|
901
|
+
if (renameValue.length > 0) {
|
|
902
|
+
renameValue = [...renameValue].slice(0, -1).join('');
|
|
903
|
+
renderRenameInput();
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (ch && ch.length >= 1 && ch.charCodeAt(0) >= 32 && !key.ctrl && !key.meta) {
|
|
908
|
+
renameValue += ch;
|
|
909
|
+
renderRenameInput();
|
|
910
|
+
}
|
|
911
|
+
return; // swallow all keys while in rename mode
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Backspace: delete search char, or exit search mode if empty
|
|
915
|
+
if (key.name === 'backspace') {
|
|
916
|
+
if (filterText) {
|
|
917
|
+
filterText = filterText.slice(0, -1);
|
|
918
|
+
selectedIndex = -1;
|
|
919
|
+
isSearchMode = !!filterText;
|
|
920
|
+
applyFilter();
|
|
921
|
+
} else if (isSearchMode) {
|
|
922
|
+
isSearchMode = false;
|
|
923
|
+
applyFilter();
|
|
924
|
+
}
|
|
841
925
|
return;
|
|
842
926
|
}
|
|
927
|
+
|
|
928
|
+
// Vim-like navigation (only when NOT in search mode)
|
|
929
|
+
if (!isSearchMode && !popupOpen) {
|
|
930
|
+
if (ch === 'j') { moveSelection(1); return; }
|
|
931
|
+
if (ch === 'k') { moveSelection(-1); return; }
|
|
932
|
+
if (ch === 'G') {
|
|
933
|
+
selectedIndex = Math.max(0, filteredSessions.length - 1);
|
|
934
|
+
suppressSelectEvent = true; listPanel.select(selectedIndex + 1); suppressSelectEvent = false;
|
|
935
|
+
listPanel.childBase = Math.max(0, selectedIndex + 1 - listPanel.height + 1);
|
|
936
|
+
renderDetail(); updateHeader(); screen.render();
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (ch === 'g') {
|
|
940
|
+
selectedIndex = -1;
|
|
941
|
+
suppressSelectEvent = true; listPanel.select(0); suppressSelectEvent = false;
|
|
942
|
+
listPanel.childBase = 0;
|
|
943
|
+
renderDetail(); updateHeader(); screen.render();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
843
948
|
if (!isSearchMode) return;
|
|
844
949
|
if (key.name === 'return' || key.name === 'enter') { isSearchMode = false; renderAll(); return; }
|
|
845
950
|
if (key.name === 'escape') { isSearchMode = false; filterText = ''; applyFilter(); return; }
|
|
@@ -850,35 +955,46 @@ function createApp() {
|
|
|
850
955
|
// ─── Resume Session ─────────────────────────────────────────────────────
|
|
851
956
|
// Auto-detect: use mai-claude if available, otherwise fall back to claude
|
|
852
957
|
|
|
853
|
-
function resumeSession(session) {
|
|
958
|
+
function resumeSession(session, modeOverride) {
|
|
959
|
+
process.stdout.write('\x1b[0m');
|
|
854
960
|
screen.destroy();
|
|
855
961
|
|
|
856
962
|
const label = CLI.name;
|
|
963
|
+
const mode = modeOverride || getEffectivePermissionMode(meta, session);
|
|
964
|
+
const modeFlag = (mode && mode !== 'default') ? ` --permission-mode ${mode}` : '';
|
|
857
965
|
|
|
858
966
|
console.log(`\n\x1b[36m⚡ Resuming conversation with ${label}\x1b[0m`);
|
|
859
967
|
console.log(`\x1b[90m Session: ${session.sessionId}\x1b[0m`);
|
|
860
|
-
console.log(`\x1b[90m Project: ${session.project} │ Branch: ${session.gitBranch || 'N/A'} │ Messages: ${session.estimatedMessages}\x1b[0m
|
|
968
|
+
console.log(`\x1b[90m Project: ${session.project} │ Branch: ${session.gitBranch || 'N/A'} │ Messages: ${session.estimatedMessages}\x1b[0m`);
|
|
969
|
+
if (mode && mode !== 'default') console.log(`\x1b[33m Mode: ${mode}\x1b[0m`);
|
|
970
|
+
console.log('');
|
|
861
971
|
|
|
862
972
|
const child = spawn(
|
|
863
|
-
`${CLI.cmd} --resume ${session.sessionId}`,
|
|
973
|
+
`${CLI.cmd} --resume ${session.sessionId}${modeFlag}`,
|
|
864
974
|
{ stdio: 'inherit', cwd: session.cwd || process.cwd(), shell: true },
|
|
865
975
|
);
|
|
866
976
|
child.on('error', (err) => {
|
|
867
977
|
console.error(`\x1b[31mFailed to resume: ${err.message}\x1b[0m`);
|
|
868
|
-
console.log(`\x1b[33mManual: ${label} --resume ${session.sessionId}\x1b[0m`);
|
|
978
|
+
console.log(`\x1b[33mManual: ${label} --resume ${session.sessionId}${modeFlag}\x1b[0m`);
|
|
869
979
|
process.exit(1);
|
|
870
980
|
});
|
|
871
981
|
child.on('exit', (code) => process.exit(code || 0));
|
|
872
982
|
}
|
|
873
983
|
|
|
874
984
|
function startNewSession() {
|
|
985
|
+
process.stdout.write('\x1b[0m');
|
|
875
986
|
screen.destroy();
|
|
876
987
|
|
|
877
988
|
const label = CLI.name;
|
|
989
|
+
const mode = meta.defaultPermissionMode || '';
|
|
990
|
+
const modeFlag = (mode && mode !== 'default') ? ` --permission-mode ${mode}` : '';
|
|
878
991
|
|
|
879
|
-
console.log(`\n\x1b[36m✨ Starting new conversation with ${label}\x1b[0m
|
|
992
|
+
console.log(`\n\x1b[36m✨ Starting new conversation with ${label}\x1b[0m`);
|
|
993
|
+
if (mode && mode !== 'default') console.log(`\x1b[33m Mode: ${mode}\x1b[0m`);
|
|
994
|
+
console.log('');
|
|
880
995
|
|
|
881
|
-
const
|
|
996
|
+
const cmd = modeFlag ? `${CLI.cmd}${modeFlag}` : CLI.cmd;
|
|
997
|
+
const child = spawn(cmd, { stdio: 'inherit', cwd: process.cwd(), shell: true });
|
|
882
998
|
child.on('error', (err) => {
|
|
883
999
|
console.error(`\x1b[31mFailed to start: ${err.message}\x1b[0m`);
|
|
884
1000
|
process.exit(1);
|
|
@@ -886,7 +1002,23 @@ function createApp() {
|
|
|
886
1002
|
child.on('exit', (code) => process.exit(code || 0));
|
|
887
1003
|
}
|
|
888
1004
|
|
|
1005
|
+
// Track the rename confirm popup and its session for Enter handling
|
|
1006
|
+
let renameConfirmPopup = null;
|
|
1007
|
+
let renameConfirmSession = null;
|
|
1008
|
+
|
|
889
1009
|
screen.key(['enter'], () => {
|
|
1010
|
+
if (renameMode) return;
|
|
1011
|
+
if (renameJustFinished) return;
|
|
1012
|
+
// Handle rename confirm popup Enter
|
|
1013
|
+
if (renameConfirmPopup && popupOpen) {
|
|
1014
|
+
const session = renameConfirmSession;
|
|
1015
|
+
renameConfirmPopup.destroy();
|
|
1016
|
+
renameConfirmPopup = null;
|
|
1017
|
+
renameConfirmSession = null;
|
|
1018
|
+
popupOpen = false;
|
|
1019
|
+
resumeSession(session);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
890
1022
|
if (isSearchMode) { isSearchMode = false; renderAll(); return; }
|
|
891
1023
|
if (popupOpen) return;
|
|
892
1024
|
if (selectedIndex === -1) { startNewSession(); return; }
|
|
@@ -896,13 +1028,13 @@ function createApp() {
|
|
|
896
1028
|
|
|
897
1029
|
// Quick shortcut: n = new session
|
|
898
1030
|
screen.key(['n'], () => {
|
|
899
|
-
if (isSearchMode) return;
|
|
1031
|
+
if (renameMode || isSearchMode) return;
|
|
900
1032
|
startNewSession();
|
|
901
1033
|
});
|
|
902
1034
|
|
|
903
1035
|
// Copy session ID
|
|
904
1036
|
screen.key(['c'], () => {
|
|
905
|
-
if (isSearchMode) return;
|
|
1037
|
+
if (renameMode || isSearchMode) return;
|
|
906
1038
|
if (filteredSessions.length === 0) return;
|
|
907
1039
|
const sid = filteredSessions[selectedIndex].sessionId;
|
|
908
1040
|
try {
|
|
@@ -914,87 +1046,127 @@ function createApp() {
|
|
|
914
1046
|
} catch (e) { /* silently fail */ }
|
|
915
1047
|
});
|
|
916
1048
|
|
|
917
|
-
// Toggle favorite
|
|
918
|
-
screen.key(['f'], () => {
|
|
919
|
-
if (isSearchMode || popupOpen) return;
|
|
920
|
-
if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
|
|
921
|
-
const session = filteredSessions[selectedIndex];
|
|
922
|
-
const nowFav = toggleFavorite(meta, session.sessionId);
|
|
923
|
-
const icon = nowFav ? '⭐' : '☆';
|
|
924
|
-
footer.setContent(`\n {#e0af68-fg}{bold}${icon} ${nowFav ? 'Favorited' : 'Unfavorited'}{/}`);
|
|
925
|
-
renderAll();
|
|
926
|
-
setTimeout(() => { updateFooter(); screen.render(); }, 1200);
|
|
927
|
-
});
|
|
928
1049
|
|
|
929
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1050
|
+
// ─── Permission Mode Picker ──────────────────────────────────────────────
|
|
1051
|
+
|
|
1052
|
+
function showResumeConfirm(session) {
|
|
1053
|
+
// Delay to avoid the Enter key from mode picker leaking into this popup
|
|
1054
|
+
setTimeout(() => {
|
|
1055
|
+
const mode = getEffectivePermissionMode(meta, session);
|
|
1056
|
+
const modeLabel = (mode && mode !== 'default') ? `{#bb9af7-fg}${mode}{/}` : '{#565f89-fg}default{/}';
|
|
1057
|
+
const confirmPopup = blessed.box({
|
|
1058
|
+
parent: screen, top: 'center', left: 'center',
|
|
1059
|
+
width: 44, height: 7,
|
|
1060
|
+
label: ' {bold}{#9ece6a-fg}Resume?{/} ',
|
|
1061
|
+
tags: true, border: { type: 'line' },
|
|
1062
|
+
style: {
|
|
1063
|
+
border: { fg: '#9ece6a' }, bg: '#24283b', fg: '#a9b1d6',
|
|
1064
|
+
label: { fg: '#9ece6a' },
|
|
1065
|
+
},
|
|
1066
|
+
content: `\n Mode: ${modeLabel}\n\n {#9ece6a-fg}{bold}Enter{/}{#a9b1d6-fg} Resume {/}{#565f89-fg}Esc{/}{#a9b1d6-fg} Cancel{/}`,
|
|
1067
|
+
});
|
|
1068
|
+
popupOpen = true;
|
|
1069
|
+
confirmPopup.focus();
|
|
1070
|
+
screen.render();
|
|
937
1071
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1072
|
+
confirmPopup.key(['enter', 'return'], () => {
|
|
1073
|
+
confirmPopup.destroy();
|
|
1074
|
+
popupOpen = false;
|
|
1075
|
+
resumeSession(session);
|
|
1076
|
+
});
|
|
1077
|
+
confirmPopup.key(['escape', 'q'], () => {
|
|
1078
|
+
confirmPopup.destroy();
|
|
1079
|
+
popupOpen = false;
|
|
1080
|
+
renderAll();
|
|
1081
|
+
});
|
|
1082
|
+
}, 50);
|
|
1083
|
+
}
|
|
941
1084
|
|
|
942
|
-
|
|
943
|
-
const
|
|
944
|
-
const
|
|
1085
|
+
function showPermissionModePicker(session) {
|
|
1086
|
+
const currentSessionMode = (meta.sessions[session.sessionId] && meta.sessions[session.sessionId].permissionMode) || '';
|
|
1087
|
+
const currentGlobalMode = meta.defaultPermissionMode || '';
|
|
1088
|
+
const effectiveMode = getEffectivePermissionMode(meta, session);
|
|
945
1089
|
|
|
946
1090
|
const items = [
|
|
947
|
-
' {#
|
|
948
|
-
...
|
|
949
|
-
const checked =
|
|
950
|
-
|
|
1091
|
+
' {#bb9af7-fg}{bold}── Session Override ──{/}',
|
|
1092
|
+
...PERMISSION_MODES.map(m => {
|
|
1093
|
+
const checked = currentSessionMode === m ? '{#9ece6a-fg}✓{/}' : ' ';
|
|
1094
|
+
const label = m === 'default' ? 'default (none)' : m;
|
|
1095
|
+
return ` ${checked} {#a9b1d6-fg}${label}{/}`;
|
|
951
1096
|
}),
|
|
1097
|
+
' {#7aa2f7-fg}{bold}Clear session override{/}',
|
|
1098
|
+
'',
|
|
1099
|
+
' {#bb9af7-fg}{bold}── Global Default ──{/}',
|
|
1100
|
+
...PERMISSION_MODES.map(m => {
|
|
1101
|
+
const checked = currentGlobalMode === m ? '{#9ece6a-fg}✓{/}' : ' ';
|
|
1102
|
+
const label = m === 'default' ? 'default (none)' : m;
|
|
1103
|
+
return ` ${checked} {#a9b1d6-fg}${label}{/}`;
|
|
1104
|
+
}),
|
|
1105
|
+
' {#7aa2f7-fg}{bold}Clear global default{/}',
|
|
952
1106
|
];
|
|
953
1107
|
|
|
954
1108
|
const popup = blessed.list({
|
|
955
1109
|
parent: screen, top: 'center', left: 'center',
|
|
956
|
-
width:
|
|
957
|
-
height: Math.min(items.length + 4,
|
|
958
|
-
label: ' {bold}{#
|
|
1110
|
+
width: 42,
|
|
1111
|
+
height: Math.min(items.length + 4, 24),
|
|
1112
|
+
label: ' {bold}{#bb9af7-fg}Permission Mode{/} ',
|
|
959
1113
|
tags: true, border: { type: 'line' },
|
|
960
1114
|
style: {
|
|
961
|
-
border: { fg: '#
|
|
1115
|
+
border: { fg: '#bb9af7' }, bg: '#24283b', fg: '#a9b1d6',
|
|
962
1116
|
selected: { bg: '#3d59a1', fg: 'white', bold: true },
|
|
963
|
-
label: { fg: '#
|
|
1117
|
+
label: { fg: '#bb9af7' },
|
|
964
1118
|
},
|
|
965
1119
|
items: items, keys: true, vi: true, mouse: true,
|
|
966
1120
|
});
|
|
967
1121
|
popupOpen = true;
|
|
968
1122
|
popup.focus(); screen.render();
|
|
969
1123
|
|
|
1124
|
+
// Section header indices (0-indexed)
|
|
1125
|
+
const sessionHeaderIdx = 0;
|
|
1126
|
+
const sessionClearIdx = PERMISSION_MODES.length + 1;
|
|
1127
|
+
const spacerIdx = sessionClearIdx + 1;
|
|
1128
|
+
const globalHeaderIdx = spacerIdx + 1;
|
|
1129
|
+
const globalClearIdx = globalHeaderIdx + PERMISSION_MODES.length + 1;
|
|
1130
|
+
|
|
970
1131
|
popup.on('select', (item, index) => {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1132
|
+
// Skip headers and spacer
|
|
1133
|
+
if (index === sessionHeaderIdx || index === globalHeaderIdx || index === spacerIdx) return;
|
|
1134
|
+
|
|
1135
|
+
if (index === sessionClearIdx) {
|
|
1136
|
+
// Clear session override
|
|
1137
|
+
setSessionPermissionMode(meta, session.sessionId, '');
|
|
1138
|
+
popup.destroy(); popupOpen = false; renderAll();
|
|
1139
|
+
showResumeConfirm(session);
|
|
976
1140
|
return;
|
|
977
1141
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1142
|
+
|
|
1143
|
+
if (index === globalClearIdx) {
|
|
1144
|
+
// Clear global default
|
|
1145
|
+
setGlobalPermissionMode(meta, '');
|
|
1146
|
+
footer.setContent(`\n {#9ece6a-fg}{bold}> Global default mode cleared{/}`);
|
|
1147
|
+
popup.destroy(); popupOpen = false; renderAll();
|
|
1148
|
+
setTimeout(() => { updateFooter(); screen.render(); }, 1500);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Session mode selection (indices 1 to PERMISSION_MODES.length)
|
|
1153
|
+
if (index > sessionHeaderIdx && index <= sessionClearIdx - 1) {
|
|
1154
|
+
const mode = PERMISSION_MODES[index - 1];
|
|
1155
|
+
setSessionPermissionMode(meta, session.sessionId, mode === 'default' ? '' : mode);
|
|
1156
|
+
popup.destroy(); popupOpen = false; renderAll();
|
|
1157
|
+
showResumeConfirm(session);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Global mode selection
|
|
1162
|
+
if (index > globalHeaderIdx && index <= globalClearIdx - 1) {
|
|
1163
|
+
const mode = PERMISSION_MODES[index - globalHeaderIdx - 1];
|
|
1164
|
+
setGlobalPermissionMode(meta, mode === 'default' ? '' : mode);
|
|
1165
|
+
footer.setContent(`\n {#9ece6a-fg}{bold}> Global default:{/} {#bb9af7-fg}${mode}{/}`);
|
|
1166
|
+
popup.destroy(); popupOpen = false; renderAll();
|
|
1167
|
+
setTimeout(() => { updateFooter(); screen.render(); }, 1500);
|
|
1168
|
+
return;
|
|
984
1169
|
}
|
|
985
|
-
setSessionTags(meta, session.sessionId, [...currentTags]);
|
|
986
|
-
|
|
987
|
-
// Refresh the popup items to show updated checkmarks
|
|
988
|
-
const refreshedItems = [
|
|
989
|
-
' {#9ece6a-fg}{bold}+ New custom tag…{/}',
|
|
990
|
-
...allTags.map(t => {
|
|
991
|
-
const checked = currentTags.has(t) ? '{#9ece6a-fg}✓{/}' : ' ';
|
|
992
|
-
return ` ${checked} {#f7768e-fg}#${t}{/}`;
|
|
993
|
-
}),
|
|
994
|
-
];
|
|
995
|
-
popup.setItems(refreshedItems);
|
|
996
|
-
popup.select(index);
|
|
997
|
-
screen.render();
|
|
998
1170
|
});
|
|
999
1171
|
|
|
1000
1172
|
popup.key(['escape', 'q'], () => {
|
|
@@ -1004,53 +1176,217 @@ function createApp() {
|
|
|
1004
1176
|
});
|
|
1005
1177
|
}
|
|
1006
1178
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1179
|
+
// ─── Quick dangerous resume (d key) ────────────────────────────────────
|
|
1180
|
+
screen.key(['d'], () => {
|
|
1181
|
+
if (renameMode || isSearchMode || popupOpen) return;
|
|
1182
|
+
if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
|
|
1183
|
+
resumeSession(filteredSessions[selectedIndex], 'bypassPermissions');
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
// ─── Permission mode picker (m key) ───────────────────────────────────
|
|
1187
|
+
screen.key(['m'], () => {
|
|
1188
|
+
if (renameMode || isSearchMode || popupOpen) return;
|
|
1189
|
+
if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
|
|
1190
|
+
showPermissionModePicker(filteredSessions[selectedIndex]);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// ─── Delete Session ───────────────────────────────────────────────────
|
|
1194
|
+
function deleteSession(session) {
|
|
1195
|
+
try {
|
|
1196
|
+
// Delete the .jsonl file
|
|
1197
|
+
if (fs.existsSync(session.filePath)) {
|
|
1198
|
+
fs.unlinkSync(session.filePath);
|
|
1199
|
+
}
|
|
1200
|
+
// Clean up meta entry
|
|
1201
|
+
if (meta.sessions[session.sessionId]) {
|
|
1202
|
+
delete meta.sessions[session.sessionId];
|
|
1203
|
+
saveMeta(meta);
|
|
1204
|
+
}
|
|
1205
|
+
// Remove from in-memory arrays
|
|
1206
|
+
const allIdx = allSessions.indexOf(session);
|
|
1207
|
+
if (allIdx !== -1) allSessions.splice(allIdx, 1);
|
|
1208
|
+
const filtIdx = filteredSessions.indexOf(session);
|
|
1209
|
+
if (filtIdx !== -1) filteredSessions.splice(filtIdx, 1);
|
|
1210
|
+
// Adjust selection
|
|
1211
|
+
if (selectedIndex >= filteredSessions.length) {
|
|
1212
|
+
selectedIndex = Math.max(-1, filteredSessions.length - 1);
|
|
1213
|
+
}
|
|
1214
|
+
} catch (e) { /* silently fail */ }
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function showDeleteConfirm(session) {
|
|
1218
|
+
const topic = (session.customTitle || session.topic || '').substring(0, 30);
|
|
1219
|
+
const confirmPopup = blessed.box({
|
|
1009
1220
|
parent: screen, top: 'center', left: 'center',
|
|
1010
|
-
width:
|
|
1011
|
-
label: ' {bold}{#f7768e-fg}
|
|
1221
|
+
width: 50, height: 9,
|
|
1222
|
+
label: ' {bold}{#f7768e-fg}Delete Session?{/} ',
|
|
1012
1223
|
tags: true, border: { type: 'line' },
|
|
1013
1224
|
style: {
|
|
1014
1225
|
border: { fg: '#f7768e' }, bg: '#24283b', fg: '#a9b1d6',
|
|
1015
1226
|
label: { fg: '#f7768e' },
|
|
1016
1227
|
},
|
|
1017
|
-
|
|
1228
|
+
content:
|
|
1229
|
+
`\n {#a9b1d6-fg}${esc(topic)}{/}\n`
|
|
1230
|
+
+ ` {#565f89-fg}${session.sessionId}{/}\n\n`
|
|
1231
|
+
+ ` {#f7768e-fg}{bold}y{/}{#a9b1d6-fg} Delete {/}{#565f89-fg}n / Esc{/}{#a9b1d6-fg} Cancel{/}`,
|
|
1018
1232
|
});
|
|
1019
1233
|
popupOpen = true;
|
|
1020
|
-
|
|
1234
|
+
confirmPopup.focus();
|
|
1021
1235
|
screen.render();
|
|
1022
1236
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1237
|
+
confirmPopup.key(['y'], () => {
|
|
1238
|
+
confirmPopup.destroy();
|
|
1025
1239
|
popupOpen = false;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
tags.add(tagName);
|
|
1031
|
-
setSessionTags(meta, session.sessionId, [...tags]);
|
|
1032
|
-
footer.setContent(`\n {#9ece6a-fg}{bold}✓ Tagged:{/} {#f7768e-fg}#${tagName}{/}`);
|
|
1033
|
-
renderAll();
|
|
1034
|
-
setTimeout(() => { updateFooter(); screen.render(); }, 1500);
|
|
1035
|
-
} else {
|
|
1036
|
-
renderAll();
|
|
1037
|
-
}
|
|
1240
|
+
deleteSession(session);
|
|
1241
|
+
footer.setContent(`\n {#f7768e-fg}{bold}✗ Deleted:{/} {#565f89-fg}${session.sessionId}{/}`);
|
|
1242
|
+
renderAll();
|
|
1243
|
+
setTimeout(() => { updateFooter(); screen.render(); }, 1500);
|
|
1038
1244
|
});
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
inputBox.destroy();
|
|
1245
|
+
confirmPopup.key(['n', 'escape', 'q'], () => {
|
|
1246
|
+
confirmPopup.destroy();
|
|
1042
1247
|
popupOpen = false;
|
|
1043
|
-
|
|
1248
|
+
screen.render();
|
|
1044
1249
|
});
|
|
1045
1250
|
}
|
|
1046
1251
|
|
|
1047
|
-
screen.key(['
|
|
1048
|
-
|
|
1252
|
+
screen.key(['x', 'delete'], () => {
|
|
1253
|
+
if (renameMode || isSearchMode || popupOpen) return;
|
|
1254
|
+
if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
|
|
1255
|
+
showDeleteConfirm(filteredSessions[selectedIndex]);
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
// ─── Rename Session ───────────────────────────────────────────────────
|
|
1259
|
+
const stringWidth = require('string-width');
|
|
1260
|
+
let renameMode = false;
|
|
1261
|
+
let renameJustFinished = false;
|
|
1262
|
+
let renameValue = '';
|
|
1263
|
+
let renameSession = null;
|
|
1264
|
+
let renamePopup = null;
|
|
1265
|
+
let renameDisplay = null;
|
|
1266
|
+
const renameMaxWidth = 46;
|
|
1267
|
+
|
|
1268
|
+
function renderRenameInput() {
|
|
1269
|
+
let display = renameValue;
|
|
1270
|
+
while (stringWidth(display) > renameMaxWidth && display.length > 0) {
|
|
1271
|
+
display = display.substring(1);
|
|
1272
|
+
}
|
|
1273
|
+
renameDisplay.setContent(display + '▌');
|
|
1274
|
+
screen.render();
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function showRenameInput(session) {
|
|
1278
|
+
renameSession = session;
|
|
1279
|
+
renameValue = session.customTitle || '';
|
|
1280
|
+
|
|
1281
|
+
renamePopup = blessed.box({
|
|
1282
|
+
parent: screen, top: 'center', left: 'center',
|
|
1283
|
+
width: 52, height: 7,
|
|
1284
|
+
label: ' {bold}{#73daca-fg}Rename Session{/} ',
|
|
1285
|
+
tags: true, border: { type: 'line' },
|
|
1286
|
+
style: {
|
|
1287
|
+
border: { fg: '#73daca' }, bg: '#24283b', fg: '#a9b1d6',
|
|
1288
|
+
label: { fg: '#73daca' },
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
renameDisplay = blessed.box({
|
|
1293
|
+
parent: renamePopup,
|
|
1294
|
+
top: 1, left: 1, right: 1, height: 1,
|
|
1295
|
+
tags: false,
|
|
1296
|
+
style: { fg: 'white', bg: '#1a1b26' },
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
blessed.box({
|
|
1300
|
+
parent: renamePopup,
|
|
1301
|
+
top: 3, left: 1, right: 1, height: 1,
|
|
1302
|
+
tags: true,
|
|
1303
|
+
style: { bg: '#24283b' },
|
|
1304
|
+
content: ' {#9ece6a-fg}{bold}Enter{/}{#a9b1d6-fg} Save {/}{#565f89-fg}Esc{/}{#a9b1d6-fg} Cancel{/}',
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
popupOpen = true;
|
|
1308
|
+
renameMode = true;
|
|
1309
|
+
renderRenameInput();
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function closeRename() {
|
|
1313
|
+
renameMode = false;
|
|
1314
|
+
if (renamePopup) { renamePopup.destroy(); renamePopup = null; }
|
|
1315
|
+
popupOpen = false;
|
|
1316
|
+
renameSession = null;
|
|
1317
|
+
renameDisplay = null;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function submitRename(session, newTitle) {
|
|
1321
|
+
newTitle = (newTitle || '').trim();
|
|
1322
|
+
|
|
1323
|
+
// Save to meta
|
|
1324
|
+
if (!meta.sessions[session.sessionId]) meta.sessions[session.sessionId] = {};
|
|
1325
|
+
meta.sessions[session.sessionId].customTitle = newTitle || undefined;
|
|
1326
|
+
if (!newTitle) delete meta.sessions[session.sessionId].customTitle;
|
|
1327
|
+
saveMeta(meta);
|
|
1328
|
+
|
|
1329
|
+
// Update in-memory session
|
|
1330
|
+
session.customTitle = newTitle;
|
|
1331
|
+
|
|
1332
|
+
// Also append to JSONL file so Claude Code sees it
|
|
1333
|
+
if (newTitle && fs.existsSync(session.filePath)) {
|
|
1334
|
+
try {
|
|
1335
|
+
const entry = JSON.stringify({ type: 'custom-title', customTitle: newTitle });
|
|
1336
|
+
fs.appendFileSync(session.filePath, '\n' + entry);
|
|
1337
|
+
} catch (e) { /* silently fail */ }
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
renderAll();
|
|
1341
|
+
|
|
1342
|
+
// Ask whether to resume this session after rename
|
|
1343
|
+
// We use renameJustFinished flag to prevent the Enter key from rename
|
|
1344
|
+
// from immediately triggering resume
|
|
1345
|
+
renameJustFinished = true;
|
|
1346
|
+
setTimeout(() => { renameJustFinished = false; }, 200);
|
|
1347
|
+
|
|
1348
|
+
setTimeout(() => {
|
|
1349
|
+
const titleLabel = newTitle ? `{#73daca-fg}${esc(newTitle)}{/}` : '{#565f89-fg}(title cleared){/}';
|
|
1350
|
+
renameConfirmSession = session;
|
|
1351
|
+
renameConfirmPopup = blessed.box({
|
|
1352
|
+
parent: screen, top: 'center', left: 'center',
|
|
1353
|
+
width: 48, height: 8,
|
|
1354
|
+
label: ' {bold}{#9ece6a-fg}Renamed{/} ',
|
|
1355
|
+
tags: true, border: { type: 'line' },
|
|
1356
|
+
style: {
|
|
1357
|
+
border: { fg: '#9ece6a' }, bg: '#24283b', fg: '#a9b1d6',
|
|
1358
|
+
label: { fg: '#9ece6a' },
|
|
1359
|
+
},
|
|
1360
|
+
content: `\n ${titleLabel}\n\n {#9ece6a-fg}{bold}Enter{/}{#a9b1d6-fg} Resume {/}{#565f89-fg}Esc{/}{#a9b1d6-fg} Back to list{/}`,
|
|
1361
|
+
});
|
|
1362
|
+
popupOpen = true;
|
|
1363
|
+
renameConfirmPopup.focus();
|
|
1364
|
+
screen.render();
|
|
1365
|
+
|
|
1366
|
+
renameConfirmPopup.key(['escape', 'q'], () => {
|
|
1367
|
+
renameConfirmPopup.destroy();
|
|
1368
|
+
renameConfirmPopup = null;
|
|
1369
|
+
renameConfirmSession = null;
|
|
1370
|
+
popupOpen = false;
|
|
1371
|
+
renderAll();
|
|
1372
|
+
});
|
|
1373
|
+
}, 50);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
screen.key(['r'], () => {
|
|
1377
|
+
if (isSearchMode || popupOpen) return;
|
|
1378
|
+
if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
|
|
1379
|
+
showRenameInput(filteredSessions[selectedIndex]);
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
screen.key(['s'], () => { if (!renameMode && !isSearchMode) cycleSort(); });
|
|
1383
|
+
screen.key(['p'], () => { if (!renameMode && !isSearchMode) showProjectPicker(); });
|
|
1049
1384
|
screen.key(['escape'], () => {
|
|
1385
|
+
if (renameMode) return; // handled in keypress
|
|
1050
1386
|
if (isSearchMode) { isSearchMode = false; filterText = ''; applyFilter(); return; }
|
|
1051
1387
|
filterText = ''; selectedIndex = -1; applyFilter();
|
|
1052
1388
|
});
|
|
1053
|
-
screen.key(['q', 'C-c'], () => { screen.destroy(); process.exit(0); });
|
|
1389
|
+
screen.key(['q', 'C-c'], () => { if (renameMode) return; process.stdout.write('\x1b[0m'); screen.destroy(); process.exit(0); });
|
|
1054
1390
|
|
|
1055
1391
|
// Remove blessed's built-in wheel handlers (they call select which changes selection)
|
|
1056
1392
|
listPanel.removeAllListeners('element wheeldown');
|
|
@@ -1099,27 +1435,76 @@ function createApp() {
|
|
|
1099
1435
|
|
|
1100
1436
|
// ─── Entry Point ─────────────────────────────────────────────────────────────
|
|
1101
1437
|
|
|
1438
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
1439
|
+
|
|
1102
1440
|
const args = process.argv.slice(2);
|
|
1103
1441
|
|
|
1442
|
+
if (args.includes('--version') || args.includes('-v') || args.includes('-V')) {
|
|
1443
|
+
console.log(`claude-starter v${PKG.version}`);
|
|
1444
|
+
process.exit(0);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (args.includes('--update') || args.includes('-u')) {
|
|
1448
|
+
const C = {
|
|
1449
|
+
reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
|
|
1450
|
+
cyan: '\x1b[36m', yellow: '\x1b[33m', green: '\x1b[32m',
|
|
1451
|
+
red: '\x1b[31m',
|
|
1452
|
+
};
|
|
1453
|
+
console.log(`\n${C.cyan}🔄 Checking for updates…${C.reset}\n`);
|
|
1454
|
+
|
|
1455
|
+
try {
|
|
1456
|
+
const latest = execSync('npm view claude-starter version 2>/dev/null', {
|
|
1457
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1458
|
+
timeout: 10000,
|
|
1459
|
+
}).toString().trim();
|
|
1460
|
+
|
|
1461
|
+
if (latest === PKG.version) {
|
|
1462
|
+
console.log(`${C.green}✓ Already on the latest version (v${PKG.version})${C.reset}\n`);
|
|
1463
|
+
process.exit(0);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
console.log(`${C.yellow} Current: v${PKG.version}${C.reset}`);
|
|
1467
|
+
console.log(`${C.green} Latest: v${latest}${C.reset}\n`);
|
|
1468
|
+
console.log(`${C.cyan}📦 Updating…${C.reset}\n`);
|
|
1469
|
+
|
|
1470
|
+
try {
|
|
1471
|
+
execSync('npm install -g claude-starter@latest', { stdio: 'inherit', timeout: 60000 });
|
|
1472
|
+
console.log(`\n${C.green}${C.bold}✓ Updated to v${latest}${C.reset}\n`);
|
|
1473
|
+
} catch (e) {
|
|
1474
|
+
console.error(`\n${C.red}✗ Update failed. Try manually:${C.reset}`);
|
|
1475
|
+
console.log(`${C.yellow} npm install -g claude-starter@latest${C.reset}\n`);
|
|
1476
|
+
process.exit(1);
|
|
1477
|
+
}
|
|
1478
|
+
} catch (e) {
|
|
1479
|
+
console.error(`${C.red}✗ Could not check for updates (network error or npm not found)${C.reset}\n`);
|
|
1480
|
+
process.exit(1);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
process.exit(0);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1104
1486
|
if (args.includes('--help') || args.includes('-h')) {
|
|
1105
1487
|
console.log(`
|
|
1106
|
-
\x1b[36m🚀 Claude Starter\x1b[0m
|
|
1488
|
+
\x1b[36m🚀 Claude Starter\x1b[0m \x1b[2mv${PKG.version}\x1b[0m
|
|
1107
1489
|
|
|
1108
1490
|
Usage:
|
|
1109
|
-
claude-starter
|
|
1110
|
-
claude-starter --list [N]
|
|
1111
|
-
claude-starter --
|
|
1491
|
+
claude-starter Launch interactive TUI
|
|
1492
|
+
claude-starter --list [N] Print latest N sessions (default: 30)
|
|
1493
|
+
claude-starter --version Show version
|
|
1494
|
+
claude-starter --update Update to the latest version
|
|
1495
|
+
claude-starter --help Show this help
|
|
1112
1496
|
|
|
1113
1497
|
TUI Keyboard Shortcuts:
|
|
1114
1498
|
↑/↓ Navigate sessions
|
|
1115
1499
|
Enter Start new / resume selected session
|
|
1116
1500
|
n Start new session
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1501
|
+
d Resume with bypassPermissions (danger mode)
|
|
1502
|
+
m Permission mode picker
|
|
1503
|
+
/ Search (fuzzy filter)
|
|
1120
1504
|
p Filter by project
|
|
1121
|
-
s Cycle sort mode (time/size/messages/project
|
|
1505
|
+
s Cycle sort mode (time/size/messages/project)
|
|
1122
1506
|
c Copy session ID
|
|
1507
|
+
x / Delete Delete selected session
|
|
1123
1508
|
Home / End Jump to top / bottom
|
|
1124
1509
|
Ctrl-D/U Page down / up
|
|
1125
1510
|
Esc Clear filter
|