claude-starter 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +40 -40
  2. package/index.js +390 -211
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -59,36 +59,25 @@ claude-starter
59
59
  - `auth` → 所有认证相关的对话
60
60
  - `refactor` → 上周的代码重构
61
61
  - `web-app fix` → 某个项目的 bug 修复
62
- - `#bug-fix` → 所有打了 bug-fix 标签的对话
63
- - `fav` → 所有收藏的对话
64
62
 
65
63
  **不需要管理模式,不需要确认。输入即搜,方向键即走。**
66
64
 
67
- ## ⭐ 收藏 & 🏷️ 标签 — 组织你的对话
68
-
69
- 按 `f` 收藏/取消收藏,按 `#` 打标签。
70
-
71
- - **收藏**:重要的对话一键标星,排序切到 `favorites` 模式收藏置顶
72
- - **标签**:预设 8 种标签(`bug-fix`、`feature`、`refactor` 等),支持自定义
73
- - **搜索联动**:`/` 搜索支持 `#tag` 语法和 `fav` 关键字
74
- - **持久化**:数据存在 `~/.claude/claude-starter-meta.json`,重启不丢失
75
-
76
65
  ## 核心能力
77
66
 
78
67
  | | 功能 | 说明 |
79
68
  |---|---|---|
80
69
  | 🎨 | **精美 TUI** | Tokyo Night 配色,分屏布局,终端里的 App |
81
70
  | ✨ | **一键新建** | 列表顶部直接新建对话 |
82
- | 🔍 | **即时搜索** | `/` 全文搜索,无需回车,支持 `#tag` 和 `fav` |
83
- | ⭐ | **收藏** | `f` 收藏重要对话,排序置顶 |
84
- | 🏷️ | **标签** | `#` 分类管理,预设 + 自定义标签 |
71
+ | 🔍 | **即时搜索** | `/` 全文搜索,无需回车 |
85
72
  | 📂 | **项目过滤** | `p` 按项目筛选 |
86
73
  | ⚡ | **秒级恢复** | 选中 → Enter → 回到对话 |
87
74
  | 📋 | **对话预览** | 右侧面板展示完整元数据和对话历史 |
88
- | 🔀 | **多种排序** | 时间 / 大小 / 消息数 / 项目 / 收藏 |
75
+ | 🔀 | **多种排序** | 时间 / 大小 / 消息数 / 项目 |
89
76
  | 📎 | **复制 ID** | `c` 一键复制到剪贴板 |
77
+ | 🔒 | **权限模式** | `m` 设置权限模式,`d` 一键 danger 模式恢复 |
78
+ | 🗑️ | **删除会话** | `x` 删除不需要的会话 |
90
79
  | 🧠 | **智能 CLI** | 自动检测 `mai-claude` / `claude` |
91
- | 🔒 | **完全本地** | 不联网,不上传,不追踪 |
80
+ | 🔐 | **完全本地** | 不联网,不上传,不追踪 |
92
81
 
93
82
  ## 安装
94
83
 
@@ -107,6 +96,16 @@ npm link
107
96
 
108
97
  然后运行 `claude-starter`,就这么简单。
109
98
 
99
+ ## CLI 参数
100
+
101
+ ```bash
102
+ claude-starter # 启动交互式 TUI
103
+ claude-starter --list [N] # 打印最近 N 个会话(默认 30)
104
+ claude-starter --version # 显示版本号
105
+ claude-starter --update # 检查并更新到最新版本
106
+ claude-starter --help # 显示帮助信息
107
+ ```
108
+
110
109
  ## 快捷键
111
110
 
112
111
  | 按键 | 功能 |
@@ -114,14 +113,15 @@ npm link
114
113
  | `↑` `↓` | 上下导航 |
115
114
  | `Enter` | 新建 / 恢复对话 |
116
115
  | `n` | 直接新建 |
117
- | `/` | 搜索(支持 `#tag` 和 `fav`) |
118
- | `f` | 收藏 / 取消收藏 ⭐ |
119
- | `#` | 添加 / 管理标签 🏷️ |
116
+ | `d` | Danger 模式恢复(bypassPermissions) |
117
+ | `m` | 权限模式选择器 |
118
+ | `/` | 搜索 |
120
119
  | `Backspace` | 删除搜索字符,删空自动退出 |
121
120
  | `Esc` | 清空搜索 |
122
121
  | `p` | 按项目过滤 |
123
- | `s` | 切换排序(时间/大小/消息数/项目/收藏) |
122
+ | `s` | 切换排序(时间/大小/消息数/项目) |
124
123
  | `c` | 复制 Session ID |
124
+ | `x` / `Delete` | 删除会话 |
125
125
  | `Home` / `End` | 跳到顶 / 底 |
126
126
  | `Ctrl-D` / `Ctrl-U` | 翻页 |
127
127
  | `q` / `Ctrl-C` | 退出 |
@@ -164,36 +164,25 @@ Searches across **everything** — project names, Git branches, conversation con
164
164
  - `auth` → all auth-related sessions
165
165
  - `refactor` → that cleanup from last week
166
166
  - `web-app fix` → bug fixes in a specific project
167
- - `#bug-fix` → all sessions tagged with bug-fix
168
- - `fav` → all favorited sessions
169
167
 
170
168
  **No modes. No confirmation. Just type and go.**
171
169
 
172
- ## ⭐ Favorites & 🏷️ Tags
173
-
174
- Press `f` to favorite/unfavorite, `#` to add tags.
175
-
176
- - **Favorites**: Star important sessions, sort by favorites to pin them at top
177
- - **Tags**: 8 built-in tags (`bug-fix`, `feature`, `refactor`, etc.) + custom tags
178
- - **Search integration**: Use `#tag` syntax or `fav` keyword in search
179
- - **Persistent**: Stored in `~/.claude/claude-starter-meta.json`, survives restarts
180
-
181
170
  ## Features
182
171
 
183
172
  | | Feature | Description |
184
173
  |---|---|---|
185
174
  | 🎨 | **Beautiful TUI** | Tokyo Night color scheme, split-pane layout, feels native in your terminal |
186
175
  | ✨ | **New Session** | Launch a fresh conversation in one keystroke |
187
- | 🔍 | **Instant Search** | Fuzzy search across everything, supports `#tag` and `fav` |
188
- | ⭐ | **Favorites** | Press `f` to star important sessions |
189
- | 🏷️ | **Tags** | Press `#` to categorize with built-in + custom tags |
176
+ | 🔍 | **Instant Search** | Fuzzy search across everything |
190
177
  | 📂 | **Project Filter** | Press `p` to filter by project |
191
178
  | ⚡ | **One-Key Resume** | Arrow, Enter, you're back in the conversation |
192
179
  | 📋 | **Session Preview** | Full metadata + conversation history in the right panel |
193
- | 🔀 | **Sort Modes** | Sort by time, size, messages, project, or favorites |
180
+ | 🔀 | **Sort Modes** | Sort by time, size, messages, or project |
194
181
  | 📎 | **Copy ID** | Press `c` to copy session ID |
182
+ | 🔒 | **Permission Modes** | Press `m` to configure, `d` for quick danger-mode resume |
183
+ | 🗑️ | **Delete Sessions** | Press `x` to remove unwanted sessions |
195
184
  | 🧠 | **Smart CLI** | Auto-detects `mai-claude` vs `claude` |
196
- | 🔒 | **100% Local** | No network, no telemetry, no data leaves your machine |
185
+ | 🔐 | **100% Local** | No network, no telemetry, no data leaves your machine |
197
186
 
198
187
  ## Install
199
188
 
@@ -216,6 +205,16 @@ Then run:
216
205
  claude-starter
217
206
  ```
218
207
 
208
+ ## CLI Options
209
+
210
+ ```bash
211
+ claude-starter # Launch interactive TUI
212
+ claude-starter --list [N] # Print latest N sessions (default: 30)
213
+ claude-starter --version # Show version
214
+ claude-starter --update # Update to the latest version
215
+ claude-starter --help # Show help
216
+ ```
217
+
219
218
  ## Keyboard Shortcuts
220
219
 
221
220
  | Key | Action |
@@ -223,14 +222,15 @@ claude-starter
223
222
  | `↑` `↓` | Navigate sessions |
224
223
  | `Enter` | Start new / resume selected session |
225
224
  | `n` | New session |
226
- | `/` | Search (supports `#tag` and `fav`) |
227
- | `f` | Toggle favorite |
228
- | `#` | Add/manage tags 🏷️ |
225
+ | `d` | Resume with bypassPermissions (danger mode) |
226
+ | `m` | Permission mode picker |
227
+ | `/` | Search |
229
228
  | `Backspace` | Edit search, auto-exit when empty |
230
229
  | `Esc` | Clear filter |
231
230
  | `p` | Filter by project |
232
- | `s` | Cycle sort mode (time/size/messages/project/favorites) |
231
+ | `s` | Cycle sort mode (time/size/messages/project) |
233
232
  | `c` | Copy session ID |
233
+ | `x` / `Delete` | Delete session |
234
234
  | `Home` / `End` | Jump to first / last |
235
235
  | `Ctrl-D` / `Ctrl-U` | Page down / up |
236
236
  | `q` / `Ctrl-C` | Quit |
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) {
@@ -156,7 +218,7 @@ function loadSessionQuick(filePath, projectName) {
156
218
  const tailStr = tailBuf.toString('utf-8');
157
219
 
158
220
  let firstTs = null, lastTs = null;
159
- let version = '', gitBranch = '', cwd = '';
221
+ let version = '', gitBranch = '', cwd = '', permissionMode = '';
160
222
  let firstUserMsg = '';
161
223
  let userMsgCount = 0;
162
224
  let customTitle = '';
@@ -171,6 +233,7 @@ function loadSessionQuick(filePath, projectName) {
171
233
  if (!version && d.version) version = d.version;
172
234
  if (!gitBranch && d.gitBranch) gitBranch = d.gitBranch;
173
235
  if (!cwd && d.cwd) cwd = d.cwd;
236
+ if (!permissionMode && d.permissionMode) permissionMode = d.permissionMode;
174
237
  if (d.type === 'custom-title' && d.customTitle) customTitle = d.customTitle;
175
238
  if (d.type === 'user') {
176
239
  userMsgCount++;
@@ -209,7 +272,7 @@ function loadSessionQuick(filePath, projectName) {
209
272
  return {
210
273
  sessionId, project: projectName,
211
274
  topic: topic || '(no user messages)',
212
- customTitle,
275
+ customTitle, permissionMode,
213
276
  firstTs, lastTs, version, gitBranch, cwd,
214
277
  fileSize: stat.size, duration: durationStr,
215
278
  estimatedMessages, filePath, _detailLoaded: false,
@@ -383,12 +446,17 @@ function createApp() {
383
446
 
384
447
  // ─── Screen ────────────────────────────────────────────────────────────
385
448
  const screen = blessed.screen({
386
- smartCSR: true,
449
+ smartCSR: false,
450
+ fastCSR: false,
387
451
  title: 'Claude Starter',
388
452
  fullUnicode: true,
389
453
  autoPadding: true,
454
+ dockBorders: true,
390
455
  });
391
456
 
457
+ // Force screen-level fill color so no terminal bg leaks through
458
+ screen.style = { bg: 234 }; // 234 = xterm color closest to #1a1b26
459
+
392
460
  // ─── Header ────────────────────────────────────────────────────────────
393
461
  const header = blessed.box({
394
462
  parent: screen, top: 0, left: 0, width: '100%', height: 3,
@@ -396,23 +464,20 @@ function createApp() {
396
464
  });
397
465
 
398
466
  function updateHeader() {
399
- const title = '{bold}{#7aa2f7-fg}🚀 Claude Starter{/}';
467
+ const title = '{bold}{#7aa2f7-fg}Claude Starter{/}';
400
468
  const count = `{#9ece6a-fg}${filteredSessions.length}{/}{#565f89-fg}/${allSessions.length} sessions{/}`;
401
469
  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}{/}`;
470
+ const sort = `{#73daca-fg}[${sortMode}]{/}`;
405
471
  const search = isSearchMode
406
472
  ? `{#e0af68-fg}/ ${filterText}▌{/}`
407
473
  : (filterText ? `{#e0af68-fg}/ ${filterText}{/}` : '');
408
474
  let parts = [title, count, proj];
409
- if (fav) parts.push(fav);
410
475
  parts.push(sort);
411
476
  if (search) parts.push(search);
412
477
  header.setContent(`\n ${parts.join(' {#414868-fg}│{/} ')}`);
413
478
  }
414
479
 
415
- blessed.line({ parent: screen, top: 3, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868' } });
480
+ blessed.line({ parent: screen, top: 3, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868', bg: '#1a1b26' } });
416
481
 
417
482
  // ─── Left Panel: blessed.list for correct scroll tracking ──────────────
418
483
  const listPanel = blessed.list({
@@ -433,7 +498,7 @@ function createApp() {
433
498
  interactive: true,
434
499
  });
435
500
 
436
- blessed.line({ parent: screen, top: 4, left: '50%', height: '100%-7', orientation: 'vertical', style: { fg: '#414868' } });
501
+ blessed.line({ parent: screen, top: 4, left: '50%', height: '100%-7', orientation: 'vertical', style: { fg: '#414868', bg: '#1a1b26' } });
437
502
 
438
503
  // ─── Right Panel ───────────────────────────────────────────────────────
439
504
  const detailPanel = blessed.box({
@@ -445,7 +510,7 @@ function createApp() {
445
510
  mouse: true,
446
511
  });
447
512
 
448
- blessed.line({ parent: screen, bottom: 2, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868' } });
513
+ blessed.line({ parent: screen, bottom: 2, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868', bg: '#1a1b26' } });
449
514
 
450
515
  // ─── Footer ────────────────────────────────────────────────────────────
451
516
  const footer = blessed.box({
@@ -455,14 +520,15 @@ function createApp() {
455
520
 
456
521
  function updateFooter() {
457
522
  const keys = [
458
- '{#7aa2f7-fg}{bold}↵{/} {#565f89-fg}Start/Resume{/}',
459
523
  '{#7aa2f7-fg}{bold}n{/} {#565f89-fg}New{/}',
524
+ '{#7aa2f7-fg}{bold}↵{/} {#565f89-fg}Resume{/}',
525
+ '{#7aa2f7-fg}{bold}m{/} {#565f89-fg}Mode{/}',
526
+ '{#f7768e-fg}{bold}d{/} {#565f89-fg}Danger{/}',
460
527
  '{#7aa2f7-fg}{bold}/{/} {#565f89-fg}Search{/}',
461
- '{#7aa2f7-fg}{bold}f{/} {#565f89-fg}Fav{/}',
462
- '{#7aa2f7-fg}{bold}#{/} {#565f89-fg}Tag{/}',
463
528
  '{#7aa2f7-fg}{bold}p{/} {#565f89-fg}Project{/}',
464
529
  '{#7aa2f7-fg}{bold}s{/} {#565f89-fg}Sort{/}',
465
530
  '{#7aa2f7-fg}{bold}c{/} {#565f89-fg}Copy ID{/}',
531
+ '{#f7768e-fg}{bold}x{/} {#565f89-fg}Delete{/}',
466
532
  '{#7aa2f7-fg}{bold}q{/} {#565f89-fg}Quit{/}',
467
533
  ];
468
534
  footer.setContent(`\n ${keys.join(' {#414868-fg}│{/} ')}`);
@@ -486,7 +552,7 @@ function createApp() {
486
552
  const branch = session.gitBranch
487
553
  ? `{#73daca-fg}${session.gitBranch.substring(0, 25)}{/}`
488
554
  : '';
489
- const dur = session.duration ? `{#565f89-fg}⏱${session.duration}{/}` : '';
555
+ const dur = session.duration ? `{#565f89-fg}${session.duration}{/}` : '';
490
556
 
491
557
  // Compose a multi-line string for each list item.
492
558
  // blessed.list renders each item as a single row, so we pack info densely.
@@ -507,42 +573,30 @@ function createApp() {
507
573
 
508
574
  // ─── Populate list ─────────────────────────────────────────────────────
509
575
  // Index 0 = "New Session", index 1+ = sessions
510
- const NEW_SESSION_LABEL = ' {#9ece6a-fg}{bold} New Conversation{/}';
576
+ const NEW_SESSION_LABEL = ' {#9ece6a-fg}{bold}+ New Conversation{/}';
511
577
 
512
578
  function refreshList() {
513
579
  const listW = Math.floor((screen.width || 100) / 2) - 2;
514
580
 
515
581
  const sessionItems = filteredSessions.map((session) => {
516
582
  const color = getProjectColor(session.project, projectColorMap);
517
- const sm = getSessionMeta(meta, session.sessionId);
518
- const favIcon = sm.favorite ? '{#e0af68-fg}{/}' : ' ';
583
+ const eMode = getEffectivePermissionMode(meta, session);
584
+ const modeIcon = (eMode === 'bypassPermissions') ? '{#f7768e-fg}!{/}' : ' ';
519
585
  const proj = `{${color}-fg}${session.project.substring(0, 12).padEnd(12)}{/}`;
520
586
  const time = `{#e0af68-fg}${formatTimestamp(session.lastTs).padEnd(16)}{/}`;
521
- const msgs = `{#7aa2f7-fg}${String(session.estimatedMessages).padStart(4)}{/}{#565f89-fg}m{/}`;
522
587
 
523
- const fixedLen = 2 + 12 + 1 + 16 + 1 + 5 + 2 + 3;
588
+ const fixedLen = 1 + 12 + 1 + 16 + 1 + 3;
524
589
  const topicMaxLen = Math.max(10, listW - fixedLen);
525
590
  let topic = session.customTitle || session.topic;
526
591
 
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);
592
+ if (topic.length > topicMaxLen) topic = topic.substring(0, topicMaxLen) + '…';
538
593
 
539
- let label = `${favIcon}${proj} ${time} ${msgs} `;
594
+ let label = `${modeIcon}${proj} ${time} `;
540
595
  if (session.customTitle) {
541
- label += `{#73daca-fg}{bold}${esc(topicPart)}{/}`;
596
+ label += `{#73daca-fg}{bold}${esc(topic)}{/}`;
542
597
  } else {
543
- label += `{#a9b1d6-fg}${esc(topicPart)}{/}`;
598
+ label += `{#a9b1d6-fg}${esc(topic)}{/}`;
544
599
  }
545
- if (tagPart) label += `{#f7768e-fg}${esc(tagPart)}{/}`;
546
600
 
547
601
  return label;
548
602
  });
@@ -558,14 +612,19 @@ function createApp() {
558
612
  function renderDetail() {
559
613
  if (selectedIndex === -1) {
560
614
  const cli = CLI.name;
615
+ const defaultMode = meta.defaultPermissionMode || '';
616
+ const modeFlag = (defaultMode && defaultMode !== 'default') ? ` --permission-mode ${defaultMode}` : '';
561
617
  let c = '';
562
- c += `\n {#9ece6a-fg}{bold}Start a New Conversation{/}\n`;
618
+ c += `\n {#9ece6a-fg}{bold}Start a New Conversation{/}\n`;
563
619
  c += ` {#414868-fg}${'─'.repeat(44)}{/}\n\n`;
564
620
  c += ` {#a9b1d6-fg}Open a fresh Claude session and start{/}\n`;
565
621
  c += ` {#a9b1d6-fg}coding from scratch.{/}\n\n`;
566
622
  c += ` {#565f89-fg}Working Dir{/} {#7dcfff-fg}${process.cwd()}{/}\n`;
567
623
  c += ` {#565f89-fg}CLI{/} {#73daca-fg}${cli}{/}\n`;
568
- c += ` {#565f89-fg}Command{/} {#565f89-fg}${cli}{/}\n\n`;
624
+ if (defaultMode && defaultMode !== 'default') {
625
+ c += ` {#565f89-fg}Mode{/} {#f7768e-fg}${defaultMode}{/}\n`;
626
+ }
627
+ c += ` {#565f89-fg}Command{/} {#565f89-fg}${cli}${modeFlag}{/}\n\n`;
569
628
  c += ` {#414868-fg}${'─'.repeat(44)}{/}\n`;
570
629
  c += ` {#9ece6a-fg}{bold}↵ Enter{/}{#9ece6a-fg} or {/}{#9ece6a-fg}{bold}n{/}{#9ece6a-fg} to launch{/}\n`;
571
630
  detailPanel.setContent(c);
@@ -582,15 +641,13 @@ function createApp() {
582
641
  loadSessionDetail(session);
583
642
 
584
643
  const color = getProjectColor(session.project, projectColorMap);
585
- const sm = getSessionMeta(meta, session.sessionId);
586
644
  let c = '';
587
645
  const sep = ` {#414868-fg}${'─'.repeat(44)}{/}`;
588
646
 
589
- // Title with favorite indicator
590
- const favLabel = sm.favorite ? ' {#e0af68-fg}{/}' : '';
591
- c += `\n {${color}-fg}{bold}█ ${session.project}{/}${favLabel}\n`;
647
+ // Title
648
+ c += `\n {${color}-fg}{bold}█ ${session.project}{/}\n`;
592
649
  if (session.customTitle) {
593
- c += ` {#73daca-fg}{bold}📌 ${esc(session.customTitle)}{/}\n`;
650
+ c += ` {#73daca-fg}{bold}${esc(session.customTitle)}{/}\n`;
594
651
  }
595
652
  c += sep + '\n\n';
596
653
 
@@ -606,15 +663,14 @@ function createApp() {
606
663
  if (session.version) fields.push(['Claude', `{#565f89-fg}v${session.version}{/}`]);
607
664
  if (session.cwd) fields.push(['Directory', `{#565f89-fg}${session.cwd}{/}`]);
608
665
 
609
- for (const [label, value] of fields) {
610
- c += ` {#565f89-fg}${label.padEnd(12)}{/} ${value}\n`;
666
+ const effectiveMode = getEffectivePermissionMode(meta, session);
667
+ if (effectiveMode && effectiveMode !== 'default') {
668
+ const modeColor = effectiveMode === 'bypassPermissions' ? '#f7768e' : '#e0af68';
669
+ fields.push(['Mode', `{${modeColor}-fg}${effectiveMode}{/}`]);
611
670
  }
612
671
 
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`;
672
+ for (const [label, value] of fields) {
673
+ c += ` {#565f89-fg}${label.padEnd(12)}{/} ${value}\n`;
618
674
  }
619
675
 
620
676
  if (session.toolsUsed && session.toolsUsed.length > 0) {
@@ -624,7 +680,7 @@ function createApp() {
624
680
  if (session.toolsUsed.length > 10) c += ` {#565f89-fg}+${session.toolsUsed.length - 10} more{/}\n`;
625
681
  }
626
682
 
627
- c += `\n {#bb9af7-fg}{bold}💬 Conversation{/}\n`;
683
+ c += `\n {#bb9af7-fg}{bold}Conversation{/}\n`;
628
684
  c += sep + '\n';
629
685
 
630
686
  const msgs = (session.userMessages || []).slice(0, 10);
@@ -636,11 +692,11 @@ function createApp() {
636
692
  msgs.forEach((msg, i) => {
637
693
  const clean = esc(msg.replace(/\n/g, ' ').trim());
638
694
  const trunc = clean.length > 80 ? clean.substring(0, 80) + '…' : clean;
639
- c += `\n {#7aa2f7-fg}{bold}You {/} ${trunc}\n`;
695
+ c += `\n {#7aa2f7-fg}{bold}You >{/} ${trunc}\n`;
640
696
  if (assists[i]) {
641
697
  const aClean = esc(assists[i].replace(/\n/g, ' ').trim());
642
698
  const aTrunc = aClean.length > 80 ? aClean.substring(0, 80) + '…' : aClean;
643
- c += ` {#9ece6a-fg}Claude {/} {#565f89-fg}${aTrunc}{/}\n`;
699
+ c += ` {#9ece6a-fg}Claude >{/} {#565f89-fg}${aTrunc}{/}\n`;
644
700
  }
645
701
  });
646
702
  }
@@ -670,19 +726,9 @@ function createApp() {
670
726
  } else {
671
727
  const terms = filterText.toLowerCase().split(/\s+/);
672
728
  filteredSessions = allSessions.filter(s => {
673
- const sm = getSessionMeta(meta, s.sessionId);
674
729
  const haystack = [s.project, s.topic, s.customTitle || '', s.gitBranch || '', s.sessionId, ...(s.userMessages || [])].join(' ').toLowerCase();
675
730
 
676
731
  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
732
  return haystack.includes(t);
687
733
  });
688
734
  });
@@ -698,19 +744,13 @@ function createApp() {
698
744
 
699
745
  // ─── Sort ──────────────────────────────────────────────────────────────
700
746
  function cycleSort() {
701
- const modes = ['time', 'size', 'messages', 'project', 'favorites'];
747
+ const modes = ['time', 'size', 'messages', 'project'];
702
748
  sortMode = modes[(modes.indexOf(sortMode) + 1) % modes.length];
703
749
  const sorters = {
704
750
  time: (a, b) => (new Date(b.lastTs || 0).getTime()) - (new Date(a.lastTs || 0).getTime()),
705
751
  size: (a, b) => b.fileSize - a.fileSize,
706
752
  messages: (a, b) => b.estimatedMessages - a.estimatedMessages,
707
753
  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
754
  };
715
755
  allSessions.sort(sorters[sortMode]);
716
756
  selectedIndex = 0;
@@ -850,35 +890,46 @@ function createApp() {
850
890
  // ─── Resume Session ─────────────────────────────────────────────────────
851
891
  // Auto-detect: use mai-claude if available, otherwise fall back to claude
852
892
 
853
- function resumeSession(session) {
893
+ function resumeSession(session, modeOverride) {
894
+ process.stdout.write('\x1b[0m');
854
895
  screen.destroy();
855
896
 
856
897
  const label = CLI.name;
898
+ const mode = modeOverride || getEffectivePermissionMode(meta, session);
899
+ const modeFlag = (mode && mode !== 'default') ? ` --permission-mode ${mode}` : '';
857
900
 
858
901
  console.log(`\n\x1b[36m⚡ Resuming conversation with ${label}\x1b[0m`);
859
902
  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`);
903
+ console.log(`\x1b[90m Project: ${session.project} │ Branch: ${session.gitBranch || 'N/A'} │ Messages: ${session.estimatedMessages}\x1b[0m`);
904
+ if (mode && mode !== 'default') console.log(`\x1b[33m Mode: ${mode}\x1b[0m`);
905
+ console.log('');
861
906
 
862
907
  const child = spawn(
863
- `${CLI.cmd} --resume ${session.sessionId}`,
908
+ `${CLI.cmd} --resume ${session.sessionId}${modeFlag}`,
864
909
  { stdio: 'inherit', cwd: session.cwd || process.cwd(), shell: true },
865
910
  );
866
911
  child.on('error', (err) => {
867
912
  console.error(`\x1b[31mFailed to resume: ${err.message}\x1b[0m`);
868
- console.log(`\x1b[33mManual: ${label} --resume ${session.sessionId}\x1b[0m`);
913
+ console.log(`\x1b[33mManual: ${label} --resume ${session.sessionId}${modeFlag}\x1b[0m`);
869
914
  process.exit(1);
870
915
  });
871
916
  child.on('exit', (code) => process.exit(code || 0));
872
917
  }
873
918
 
874
919
  function startNewSession() {
920
+ process.stdout.write('\x1b[0m');
875
921
  screen.destroy();
876
922
 
877
923
  const label = CLI.name;
924
+ const mode = meta.defaultPermissionMode || '';
925
+ const modeFlag = (mode && mode !== 'default') ? ` --permission-mode ${mode}` : '';
878
926
 
879
- console.log(`\n\x1b[36m✨ Starting new conversation with ${label}\x1b[0m\n`);
927
+ console.log(`\n\x1b[36m✨ Starting new conversation with ${label}\x1b[0m`);
928
+ if (mode && mode !== 'default') console.log(`\x1b[33m Mode: ${mode}\x1b[0m`);
929
+ console.log('');
880
930
 
881
- const child = spawn(CLI.cmd, { stdio: 'inherit', cwd: process.cwd(), shell: true });
931
+ const cmd = modeFlag ? `${CLI.cmd}${modeFlag}` : CLI.cmd;
932
+ const child = spawn(cmd, { stdio: 'inherit', cwd: process.cwd(), shell: true });
882
933
  child.on('error', (err) => {
883
934
  console.error(`\x1b[31mFailed to start: ${err.message}\x1b[0m`);
884
935
  process.exit(1);
@@ -914,87 +965,127 @@ function createApp() {
914
965
  } catch (e) { /* silently fail */ }
915
966
  });
916
967
 
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
968
 
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
- });
969
+ // ─── Permission Mode Picker ──────────────────────────────────────────────
970
+
971
+ function showResumeConfirm(session) {
972
+ // Delay to avoid the Enter key from mode picker leaking into this popup
973
+ setTimeout(() => {
974
+ const mode = getEffectivePermissionMode(meta, session);
975
+ const modeLabel = (mode && mode !== 'default') ? `{#bb9af7-fg}${mode}{/}` : '{#565f89-fg}default{/}';
976
+ const confirmPopup = blessed.box({
977
+ parent: screen, top: 'center', left: 'center',
978
+ width: 44, height: 7,
979
+ label: ' {bold}{#9ece6a-fg}Resume?{/} ',
980
+ tags: true, border: { type: 'line' },
981
+ style: {
982
+ border: { fg: '#9ece6a' }, bg: '#24283b', fg: '#a9b1d6',
983
+ label: { fg: '#9ece6a' },
984
+ },
985
+ content: `\n Mode: ${modeLabel}\n\n {#9ece6a-fg}{bold}Enter{/}{#a9b1d6-fg} Resume {/}{#565f89-fg}Esc{/}{#a9b1d6-fg} Cancel{/}`,
986
+ });
987
+ popupOpen = true;
988
+ confirmPopup.focus();
989
+ screen.render();
937
990
 
938
- function showTagPicker(session) {
939
- const sm = getSessionMeta(meta, session.sessionId);
940
- const currentTags = new Set(sm.tags);
991
+ confirmPopup.key(['enter', 'return'], () => {
992
+ confirmPopup.destroy();
993
+ popupOpen = false;
994
+ resumeSession(session);
995
+ });
996
+ confirmPopup.key(['escape', 'q'], () => {
997
+ confirmPopup.destroy();
998
+ popupOpen = false;
999
+ renderAll();
1000
+ });
1001
+ }, 50);
1002
+ }
941
1003
 
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();
1004
+ function showPermissionModePicker(session) {
1005
+ const currentSessionMode = (meta.sessions[session.sessionId] && meta.sessions[session.sessionId].permissionMode) || '';
1006
+ const currentGlobalMode = meta.defaultPermissionMode || '';
1007
+ const effectiveMode = getEffectivePermissionMode(meta, session);
945
1008
 
946
1009
  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}{/}`;
1010
+ ' {#bb9af7-fg}{bold}── Session Override ──{/}',
1011
+ ...PERMISSION_MODES.map(m => {
1012
+ const checked = currentSessionMode === m ? '{#9ece6a-fg}✓{/}' : ' ';
1013
+ const label = m === 'default' ? 'default (none)' : m;
1014
+ return ` ${checked} {#a9b1d6-fg}${label}{/}`;
951
1015
  }),
1016
+ ' {#7aa2f7-fg}{bold}Clear session override{/}',
1017
+ '',
1018
+ ' {#bb9af7-fg}{bold}── Global Default ──{/}',
1019
+ ...PERMISSION_MODES.map(m => {
1020
+ const checked = currentGlobalMode === m ? '{#9ece6a-fg}✓{/}' : ' ';
1021
+ const label = m === 'default' ? 'default (none)' : m;
1022
+ return ` ${checked} {#a9b1d6-fg}${label}{/}`;
1023
+ }),
1024
+ ' {#7aa2f7-fg}{bold}Clear global default{/}',
952
1025
  ];
953
1026
 
954
1027
  const popup = blessed.list({
955
1028
  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{/} ',
1029
+ width: 42,
1030
+ height: Math.min(items.length + 4, 24),
1031
+ label: ' {bold}{#bb9af7-fg}Permission Mode{/} ',
959
1032
  tags: true, border: { type: 'line' },
960
1033
  style: {
961
- border: { fg: '#f7768e' }, bg: '#24283b', fg: '#a9b1d6',
1034
+ border: { fg: '#bb9af7' }, bg: '#24283b', fg: '#a9b1d6',
962
1035
  selected: { bg: '#3d59a1', fg: 'white', bold: true },
963
- label: { fg: '#f7768e' },
1036
+ label: { fg: '#bb9af7' },
964
1037
  },
965
1038
  items: items, keys: true, vi: true, mouse: true,
966
1039
  });
967
1040
  popupOpen = true;
968
1041
  popup.focus(); screen.render();
969
1042
 
1043
+ // Section header indices (0-indexed)
1044
+ const sessionHeaderIdx = 0;
1045
+ const sessionClearIdx = PERMISSION_MODES.length + 1;
1046
+ const spacerIdx = sessionClearIdx + 1;
1047
+ const globalHeaderIdx = spacerIdx + 1;
1048
+ const globalClearIdx = globalHeaderIdx + PERMISSION_MODES.length + 1;
1049
+
970
1050
  popup.on('select', (item, index) => {
971
- if (index === 0) {
972
- // New custom tag show input
973
- popup.destroy();
974
- popupOpen = false;
975
- showTagInput(session);
1051
+ // Skip headers and spacer
1052
+ if (index === sessionHeaderIdx || index === globalHeaderIdx || index === spacerIdx) return;
1053
+
1054
+ if (index === sessionClearIdx) {
1055
+ // Clear session override
1056
+ setSessionPermissionMode(meta, session.sessionId, '');
1057
+ popup.destroy(); popupOpen = false; renderAll();
1058
+ showResumeConfirm(session);
976
1059
  return;
977
1060
  }
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);
1061
+
1062
+ if (index === globalClearIdx) {
1063
+ // Clear global default
1064
+ setGlobalPermissionMode(meta, '');
1065
+ footer.setContent(`\n {#9ece6a-fg}{bold}> Global default mode cleared{/}`);
1066
+ popup.destroy(); popupOpen = false; renderAll();
1067
+ setTimeout(() => { updateFooter(); screen.render(); }, 1500);
1068
+ return;
1069
+ }
1070
+
1071
+ // Session mode selection (indices 1 to PERMISSION_MODES.length)
1072
+ if (index > sessionHeaderIdx && index <= sessionClearIdx - 1) {
1073
+ const mode = PERMISSION_MODES[index - 1];
1074
+ setSessionPermissionMode(meta, session.sessionId, mode === 'default' ? '' : mode);
1075
+ popup.destroy(); popupOpen = false; renderAll();
1076
+ showResumeConfirm(session);
1077
+ return;
1078
+ }
1079
+
1080
+ // Global mode selection
1081
+ if (index > globalHeaderIdx && index <= globalClearIdx - 1) {
1082
+ const mode = PERMISSION_MODES[index - globalHeaderIdx - 1];
1083
+ setGlobalPermissionMode(meta, mode === 'default' ? '' : mode);
1084
+ footer.setContent(`\n {#9ece6a-fg}{bold}> Global default:{/} {#bb9af7-fg}${mode}{/}`);
1085
+ popup.destroy(); popupOpen = false; renderAll();
1086
+ setTimeout(() => { updateFooter(); screen.render(); }, 1500);
1087
+ return;
984
1088
  }
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
1089
  });
999
1090
 
1000
1091
  popup.key(['escape', 'q'], () => {
@@ -1004,53 +1095,92 @@ function createApp() {
1004
1095
  });
1005
1096
  }
1006
1097
 
1007
- function showTagInput(session) {
1008
- const inputBox = blessed.textbox({
1098
+ // ─── Quick dangerous resume (d key) ────────────────────────────────────
1099
+ screen.key(['d'], () => {
1100
+ if (isSearchMode || popupOpen) return;
1101
+ if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
1102
+ resumeSession(filteredSessions[selectedIndex], 'bypassPermissions');
1103
+ });
1104
+
1105
+ // ─── Permission mode picker (m key) ───────────────────────────────────
1106
+ screen.key(['m'], () => {
1107
+ if (isSearchMode || popupOpen) return;
1108
+ if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
1109
+ showPermissionModePicker(filteredSessions[selectedIndex]);
1110
+ });
1111
+
1112
+ // ─── Delete Session ───────────────────────────────────────────────────
1113
+ function deleteSession(session) {
1114
+ try {
1115
+ // Delete the .jsonl file
1116
+ if (fs.existsSync(session.filePath)) {
1117
+ fs.unlinkSync(session.filePath);
1118
+ }
1119
+ // Clean up meta entry
1120
+ if (meta.sessions[session.sessionId]) {
1121
+ delete meta.sessions[session.sessionId];
1122
+ saveMeta(meta);
1123
+ }
1124
+ // Remove from in-memory arrays
1125
+ const allIdx = allSessions.indexOf(session);
1126
+ if (allIdx !== -1) allSessions.splice(allIdx, 1);
1127
+ const filtIdx = filteredSessions.indexOf(session);
1128
+ if (filtIdx !== -1) filteredSessions.splice(filtIdx, 1);
1129
+ // Adjust selection
1130
+ if (selectedIndex >= filteredSessions.length) {
1131
+ selectedIndex = Math.max(-1, filteredSessions.length - 1);
1132
+ }
1133
+ } catch (e) { /* silently fail */ }
1134
+ }
1135
+
1136
+ function showDeleteConfirm(session) {
1137
+ const topic = (session.customTitle || session.topic || '').substring(0, 30);
1138
+ const confirmPopup = blessed.box({
1009
1139
  parent: screen, top: 'center', left: 'center',
1010
- width: 40, height: 3,
1011
- label: ' {bold}{#f7768e-fg}New Tag{/} ',
1140
+ width: 50, height: 9,
1141
+ label: ' {bold}{#f7768e-fg}Delete Session?{/} ',
1012
1142
  tags: true, border: { type: 'line' },
1013
1143
  style: {
1014
1144
  border: { fg: '#f7768e' }, bg: '#24283b', fg: '#a9b1d6',
1015
1145
  label: { fg: '#f7768e' },
1016
1146
  },
1017
- inputOnFocus: true,
1147
+ content:
1148
+ `\n {#a9b1d6-fg}${esc(topic)}{/}\n`
1149
+ + ` {#565f89-fg}${session.sessionId}{/}\n\n`
1150
+ + ` {#f7768e-fg}{bold}y{/}{#a9b1d6-fg} Delete {/}{#565f89-fg}n / Esc{/}{#a9b1d6-fg} Cancel{/}`,
1018
1151
  });
1019
1152
  popupOpen = true;
1020
- inputBox.focus();
1153
+ confirmPopup.focus();
1021
1154
  screen.render();
1022
1155
 
1023
- inputBox.on('submit', (value) => {
1024
- inputBox.destroy();
1156
+ confirmPopup.key(['y'], () => {
1157
+ confirmPopup.destroy();
1025
1158
  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
- }
1159
+ deleteSession(session);
1160
+ footer.setContent(`\n {#f7768e-fg}{bold}✗ Deleted:{/} {#565f89-fg}${session.sessionId}{/}`);
1161
+ renderAll();
1162
+ setTimeout(() => { updateFooter(); screen.render(); }, 1500);
1038
1163
  });
1039
-
1040
- inputBox.on('cancel', () => {
1041
- inputBox.destroy();
1164
+ confirmPopup.key(['n', 'escape', 'q'], () => {
1165
+ confirmPopup.destroy();
1042
1166
  popupOpen = false;
1043
- renderAll();
1167
+ screen.render();
1044
1168
  });
1045
1169
  }
1046
1170
 
1171
+ screen.key(['x', 'delete'], () => {
1172
+ if (isSearchMode || popupOpen) return;
1173
+ if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
1174
+ showDeleteConfirm(filteredSessions[selectedIndex]);
1175
+ });
1176
+
1047
1177
  screen.key(['s'], () => { if (!isSearchMode) cycleSort(); });
1048
1178
  screen.key(['p'], () => { if (!isSearchMode) showProjectPicker(); });
1049
1179
  screen.key(['escape'], () => {
1050
1180
  if (isSearchMode) { isSearchMode = false; filterText = ''; applyFilter(); return; }
1051
1181
  filterText = ''; selectedIndex = -1; applyFilter();
1052
1182
  });
1053
- screen.key(['q', 'C-c'], () => { screen.destroy(); process.exit(0); });
1183
+ screen.key(['q', 'C-c'], () => { process.stdout.write('\x1b[0m'); screen.destroy(); process.exit(0); });
1054
1184
 
1055
1185
  // Remove blessed's built-in wheel handlers (they call select which changes selection)
1056
1186
  listPanel.removeAllListeners('element wheeldown');
@@ -1099,27 +1229,76 @@ function createApp() {
1099
1229
 
1100
1230
  // ─── Entry Point ─────────────────────────────────────────────────────────────
1101
1231
 
1232
+ const PKG = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
1233
+
1102
1234
  const args = process.argv.slice(2);
1103
1235
 
1236
+ if (args.includes('--version') || args.includes('-v') || args.includes('-V')) {
1237
+ console.log(`claude-starter v${PKG.version}`);
1238
+ process.exit(0);
1239
+ }
1240
+
1241
+ if (args.includes('--update') || args.includes('-u')) {
1242
+ const C = {
1243
+ reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
1244
+ cyan: '\x1b[36m', yellow: '\x1b[33m', green: '\x1b[32m',
1245
+ red: '\x1b[31m',
1246
+ };
1247
+ console.log(`\n${C.cyan}🔄 Checking for updates…${C.reset}\n`);
1248
+
1249
+ try {
1250
+ const latest = execSync('npm view claude-starter version 2>/dev/null', {
1251
+ stdio: ['pipe', 'pipe', 'pipe'],
1252
+ timeout: 10000,
1253
+ }).toString().trim();
1254
+
1255
+ if (latest === PKG.version) {
1256
+ console.log(`${C.green}✓ Already on the latest version (v${PKG.version})${C.reset}\n`);
1257
+ process.exit(0);
1258
+ }
1259
+
1260
+ console.log(`${C.yellow} Current: v${PKG.version}${C.reset}`);
1261
+ console.log(`${C.green} Latest: v${latest}${C.reset}\n`);
1262
+ console.log(`${C.cyan}📦 Updating…${C.reset}\n`);
1263
+
1264
+ try {
1265
+ execSync('npm install -g claude-starter@latest', { stdio: 'inherit', timeout: 60000 });
1266
+ console.log(`\n${C.green}${C.bold}✓ Updated to v${latest}${C.reset}\n`);
1267
+ } catch (e) {
1268
+ console.error(`\n${C.red}✗ Update failed. Try manually:${C.reset}`);
1269
+ console.log(`${C.yellow} npm install -g claude-starter@latest${C.reset}\n`);
1270
+ process.exit(1);
1271
+ }
1272
+ } catch (e) {
1273
+ console.error(`${C.red}✗ Could not check for updates (network error or npm not found)${C.reset}\n`);
1274
+ process.exit(1);
1275
+ }
1276
+
1277
+ process.exit(0);
1278
+ }
1279
+
1104
1280
  if (args.includes('--help') || args.includes('-h')) {
1105
1281
  console.log(`
1106
- \x1b[36m🚀 Claude Starter\x1b[0m
1282
+ \x1b[36m🚀 Claude Starter\x1b[0m \x1b[2mv${PKG.version}\x1b[0m
1107
1283
 
1108
1284
  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
1285
+ claude-starter Launch interactive TUI
1286
+ claude-starter --list [N] Print latest N sessions (default: 30)
1287
+ claude-starter --version Show version
1288
+ claude-starter --update Update to the latest version
1289
+ claude-starter --help Show this help
1112
1290
 
1113
1291
  TUI Keyboard Shortcuts:
1114
1292
  ↑/↓ Navigate sessions
1115
1293
  Enter Start new / resume selected session
1116
1294
  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
1295
+ d Resume with bypassPermissions (danger mode)
1296
+ m Permission mode picker
1297
+ / Search (fuzzy filter)
1120
1298
  p Filter by project
1121
- s Cycle sort mode (time/size/messages/project/favorites)
1299
+ s Cycle sort mode (time/size/messages/project)
1122
1300
  c Copy session ID
1301
+ x / Delete Delete selected session
1123
1302
  Home / End Jump to top / bottom
1124
1303
  Ctrl-D/U Page down / up
1125
1304
  Esc Clear filter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-starter",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A beautiful terminal UI for managing Claude Code sessions — start new or resume past conversations",
5
5
  "main": "index.js",
6
6
  "bin": {