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.
Files changed (3) hide show
  1. package/README.md +50 -44
  2. package/index.js +626 -241
  3. 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 → favorites
21
+ * s Cycle sort: time → size → messages → project
20
22
  * n Start new session
21
- * f Toggle favorite on selected session
22
- * # Add/remove tags on selected session
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
- // We check inside an interactive shell so aliases defined in .bashrc/.zshrc
38
- // are visible. Falls back to plain `claude`.
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 raw = execSync(`${shell} -ic "command -v mai-claude" 2>/dev/null`, {
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
- // Interactive shells may print extra lines (e.g. "Restored session: …").
53
- // The relevant output is the last line(s) containing the alias or path.
54
- const lines = raw.split('\n');
55
- const aliasLine = lines.find(l => l.startsWith('alias ')) || lines[lines.length - 1];
56
-
57
- // `command -v` for an alias returns: alias mai-claude='actual command'
58
- // Extract the real command from inside the quotes if it's an alias.
59
- const aliasMatch = aliasLine.match(/^alias [^=]+=(?:'(.+)'|"(.+)")$/s);
60
- if (aliasMatch) {
61
- return { name: 'mai-claude', cmd: aliasMatch[1] || aliasMatch[2] };
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
- // Otherwise it's a binary/function path use the name directly
64
- return { name: 'mai-claude', cmd: 'mai-claude' };
65
- } catch {
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 (favorites & tags) ────────────────────────────────────────
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] || { favorite: false, tags: [] };
142
+ return meta.sessions[sessionId] || {};
106
143
  }
107
144
 
108
- function toggleFavorite(meta, sessionId) {
109
- if (!meta.sessions[sessionId]) meta.sessions[sessionId] = { favorite: false, tags: [] };
110
- meta.sessions[sessionId].favorite = !meta.sessions[sessionId].favorite;
111
- saveMeta(meta);
112
- return meta.sessions[sessionId].favorite;
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 setSessionTags(meta, sessionId, tags) {
116
- if (!meta.sessions[sessionId]) meta.sessions[sessionId] = { favorite: false, tags: [] };
117
- meta.sessions[sessionId].tags = tags;
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 getAllUsedTags(meta) {
122
- const tags = new Set();
123
- for (const s of Object.values(meta.sessions)) {
124
- if (s.tags) s.tags.forEach(t => tags.add(t));
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
- return dirName
133
- .replace(/-Users-[^-]+-Desktop-MSProject-/, '')
134
- .replace(/-Users-[^-]+-Desktop-/, '')
135
- .replace(/-Users-[^-]+/, '~')
136
- .replace(/^-/, '') || '~';
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
- let tailBuf = Buffer.alloc(0);
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 tailSize = Math.min(4096, stat.size - 8192);
150
- tailBuf = Buffer.alloc(tailSize);
151
- fs.readSync(fd, tailBuf, 0, tailSize, stat.size - tailSize);
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: true,
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}🚀 Claude Starter{/}';
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 favCount = allSessions.filter(s => getSessionMeta(meta, s.sessionId).favorite).length;
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
- '{#7aa2f7-fg}{bold}{/} {#565f89-fg}Start/Resume{/}',
459
- '{#7aa2f7-fg}{bold}n{/} {#565f89-fg}New{/}',
460
- '{#7aa2f7-fg}{bold}/{/} {#565f89-fg}Search{/}',
461
- '{#7aa2f7-fg}{bold}f{/} {#565f89-fg}Fav{/}',
462
- '{#7aa2f7-fg}{bold}#{/} {#565f89-fg}Tag{/}',
463
- '{#7aa2f7-fg}{bold}p{/} {#565f89-fg}Project{/}',
464
- '{#7aa2f7-fg}{bold}s{/} {#565f89-fg}Sort{/}',
465
- '{#7aa2f7-fg}{bold}c{/} {#565f89-fg}Copy ID{/}',
466
- '{#7aa2f7-fg}{bold}q{/} {#565f89-fg}Quit{/}',
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}⏱${session.duration}{/}` : '';
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} New Conversation{/}';
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 sm = getSessionMeta(meta, session.sessionId);
518
- const favIcon = sm.favorite ? '{#e0af68-fg}{/}' : ' ';
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 = 2 + 12 + 1 + 16 + 1 + 5 + 2 + 3;
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
- // Append tags inline after topic
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 = `${favIcon}${proj} ${time} ${msgs} `;
606
+ let label = `${modeIcon}${proj} ${time} `;
540
607
  if (session.customTitle) {
541
- label += `{#73daca-fg}{bold}${esc(topicPart)}{/}`;
608
+ label += `{#73daca-fg}{bold}${esc(topic)}{/}`;
542
609
  } else {
543
- label += `{#a9b1d6-fg}${esc(topicPart)}{/}`;
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}Start a New Conversation{/}\n`;
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
- c += ` {#565f89-fg}Command{/} {#565f89-fg}${cli}{/}\n\n`;
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 with favorite indicator
590
- const favLabel = sm.favorite ? ' {#e0af68-fg}{/}' : '';
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}📌 ${esc(session.customTitle)}{/}\n`;
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
- for (const [label, value] of fields) {
610
- c += ` {#565f89-fg}${label.padEnd(12)}{/} ${value}\n`;
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
- // Tags section
614
- if (sm.tags.length > 0) {
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}💬 Conversation{/}\n`;
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 {/} ${trunc}\n`;
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 {/} {#565f89-fg}${aTrunc}{/}\n`;
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', 'favorites'];
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
- // Backspace: always works when there's filter text, regardless of search mode
836
- if (key.name === 'backspace' && filterText) {
837
- filterText = filterText.slice(0, -1);
838
- selectedIndex = -1;
839
- isSearchMode = !!filterText; // exit search when empty
840
- applyFilter();
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\n`);
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\n`);
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 child = spawn(CLI.cmd, { stdio: 'inherit', cwd: process.cwd(), shell: true });
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
- // Tag management handled via keypress since '#' is a shifted character
930
- // that some terminal/blessed combos may not route through screen.key
931
- screen.on('keypress', (ch, key) => {
932
- if (ch === '#' && !isSearchMode && !popupOpen) {
933
- if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
934
- showTagPicker(filteredSessions[selectedIndex]);
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
- function showTagPicker(session) {
939
- const sm = getSessionMeta(meta, session.sessionId);
940
- const currentTags = new Set(sm.tags);
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
- // Build tag list: all known tags (defaults + used), with checkmarks for active ones
943
- const usedTags = getAllUsedTags(meta);
944
- const allTags = [...new Set([...DEFAULT_TAGS, ...usedTags])].sort();
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
- ' {#9ece6a-fg}{bold}+ New custom tag…{/}',
948
- ...allTags.map(t => {
949
- const checked = currentTags.has(t) ? '{#9ece6a-fg}✓{/}' : ' ';
950
- return ` ${checked} {#f7768e-fg}#${t}{/}`;
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: Math.min(45, Math.max(...items.map(i => i.replace(/\{[^}]*\}/g, '').length)) + 8),
957
- height: Math.min(items.length + 4, 20),
958
- label: ' {bold}{#f7768e-fg}🏷️ Tags{/} ',
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: '#f7768e' }, bg: '#24283b', fg: '#a9b1d6',
1115
+ border: { fg: '#bb9af7' }, bg: '#24283b', fg: '#a9b1d6',
962
1116
  selected: { bg: '#3d59a1', fg: 'white', bold: true },
963
- label: { fg: '#f7768e' },
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
- if (index === 0) {
972
- // New custom tag show input
973
- popup.destroy();
974
- popupOpen = false;
975
- showTagInput(session);
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
- // Toggle the selected tag
979
- const tagName = allTags[index - 1];
980
- if (currentTags.has(tagName)) {
981
- currentTags.delete(tagName);
982
- } else {
983
- currentTags.add(tagName);
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
- function showTagInput(session) {
1008
- const inputBox = blessed.textbox({
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: 40, height: 3,
1011
- label: ' {bold}{#f7768e-fg}New Tag{/} ',
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
- inputOnFocus: true,
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
- inputBox.focus();
1234
+ confirmPopup.focus();
1021
1235
  screen.render();
1022
1236
 
1023
- inputBox.on('submit', (value) => {
1024
- inputBox.destroy();
1237
+ confirmPopup.key(['y'], () => {
1238
+ confirmPopup.destroy();
1025
1239
  popupOpen = false;
1026
- const tagName = value.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
1027
- if (tagName) {
1028
- const sm = getSessionMeta(meta, session.sessionId);
1029
- const tags = new Set(sm.tags);
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
- inputBox.on('cancel', () => {
1041
- inputBox.destroy();
1245
+ confirmPopup.key(['n', 'escape', 'q'], () => {
1246
+ confirmPopup.destroy();
1042
1247
  popupOpen = false;
1043
- renderAll();
1248
+ screen.render();
1044
1249
  });
1045
1250
  }
1046
1251
 
1047
- screen.key(['s'], () => { if (!isSearchMode) cycleSort(); });
1048
- screen.key(['p'], () => { if (!isSearchMode) showProjectPicker(); });
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 Launch interactive TUI
1110
- claude-starter --list [N] Print latest N sessions (default: 30)
1111
- claude-starter --help Show this help
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
- / Search (fuzzy filter, supports #tag and fav)
1118
- f Toggle favorite ⭐ on selected session
1119
- # Add/remove tags on selected session
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/favorites)
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