claude-starter 1.1.1 → 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.
- package/README.md +49 -6
- package/index.js +497 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<img src="https://img.shields.io/badge/%F0%9F%9A%80-Claude_Starter-7aa2f7?style=for-the-badge&labelColor=1a1b26" alt="Claude Starter" />
|
|
3
3
|
<br/>
|
|
4
|
+
<img src="https://img.shields.io/npm/v/claude-starter?style=flat-square&color=f7768e&logo=npm" alt="npm" />
|
|
4
5
|
<img src="https://img.shields.io/badge/node-%3E%3D18-9ece6a?style=flat-square&logo=node.js&logoColor=white" alt="Node.js" />
|
|
5
6
|
<img src="https://img.shields.io/badge/license-MIT-bb9af7?style=flat-square" alt="MIT License" />
|
|
6
7
|
<img src="https://img.shields.io/github/v/release/Bojun-Vvibe/claude-starter?style=flat-square&color=7dcfff" alt="Release" />
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
</p>
|
|
16
17
|
|
|
17
18
|
<p align="center">
|
|
18
|
-
<code>
|
|
19
|
+
<code>npm install -g claude-starter</code> → <code>claude-starter</code>
|
|
19
20
|
</p>
|
|
20
21
|
|
|
21
22
|
<p align="center">
|
|
@@ -73,11 +74,19 @@ claude-starter
|
|
|
73
74
|
| 📋 | **对话预览** | 右侧面板展示完整元数据和对话历史 |
|
|
74
75
|
| 🔀 | **多种排序** | 时间 / 大小 / 消息数 / 项目 |
|
|
75
76
|
| 📎 | **复制 ID** | `c` 一键复制到剪贴板 |
|
|
77
|
+
| 🔒 | **权限模式** | `m` 设置权限模式,`d` 一键 danger 模式恢复 |
|
|
78
|
+
| 🗑️ | **删除会话** | `x` 删除不需要的会话 |
|
|
76
79
|
| 🧠 | **智能 CLI** | 自动检测 `mai-claude` / `claude` |
|
|
77
|
-
|
|
|
80
|
+
| 🔐 | **完全本地** | 不联网,不上传,不追踪 |
|
|
78
81
|
|
|
79
82
|
## 安装
|
|
80
83
|
|
|
84
|
+
```bash
|
|
85
|
+
npm install -g claude-starter
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
或者从源码安装:
|
|
89
|
+
|
|
81
90
|
```bash
|
|
82
91
|
git clone https://github.com/Bojun-Vvibe/claude-starter.git
|
|
83
92
|
cd claude-starter
|
|
@@ -87,6 +96,16 @@ npm link
|
|
|
87
96
|
|
|
88
97
|
然后运行 `claude-starter`,就这么简单。
|
|
89
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
|
+
|
|
90
109
|
## 快捷键
|
|
91
110
|
|
|
92
111
|
| 按键 | 功能 |
|
|
@@ -94,12 +113,15 @@ npm link
|
|
|
94
113
|
| `↑` `↓` | 上下导航 |
|
|
95
114
|
| `Enter` | 新建 / 恢复对话 |
|
|
96
115
|
| `n` | 直接新建 |
|
|
116
|
+
| `d` | Danger 模式恢复(bypassPermissions) |
|
|
117
|
+
| `m` | 权限模式选择器 |
|
|
97
118
|
| `/` | 搜索 |
|
|
98
119
|
| `Backspace` | 删除搜索字符,删空自动退出 |
|
|
99
120
|
| `Esc` | 清空搜索 |
|
|
100
121
|
| `p` | 按项目过滤 |
|
|
101
|
-
| `s` |
|
|
122
|
+
| `s` | 切换排序(时间/大小/消息数/项目) |
|
|
102
123
|
| `c` | 复制 Session ID |
|
|
124
|
+
| `x` / `Delete` | 删除会话 |
|
|
103
125
|
| `Home` / `End` | 跳到顶 / 底 |
|
|
104
126
|
| `Ctrl-D` / `Ctrl-U` | 翻页 |
|
|
105
127
|
| `q` / `Ctrl-C` | 退出 |
|
|
@@ -151,17 +173,25 @@ Searches across **everything** — project names, Git branches, conversation con
|
|
|
151
173
|
|---|---|---|
|
|
152
174
|
| 🎨 | **Beautiful TUI** | Tokyo Night color scheme, split-pane layout, feels native in your terminal |
|
|
153
175
|
| ✨ | **New Session** | Launch a fresh conversation in one keystroke |
|
|
154
|
-
| 🔍 | **Instant Search** | Fuzzy search across everything
|
|
176
|
+
| 🔍 | **Instant Search** | Fuzzy search across everything |
|
|
155
177
|
| 📂 | **Project Filter** | Press `p` to filter by project |
|
|
156
178
|
| ⚡ | **One-Key Resume** | Arrow, Enter, you're back in the conversation |
|
|
157
179
|
| 📋 | **Session Preview** | Full metadata + conversation history in the right panel |
|
|
158
180
|
| 🔀 | **Sort Modes** | Sort by time, size, messages, or project |
|
|
159
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 |
|
|
160
184
|
| 🧠 | **Smart CLI** | Auto-detects `mai-claude` vs `claude` |
|
|
161
|
-
|
|
|
185
|
+
| 🔐 | **100% Local** | No network, no telemetry, no data leaves your machine |
|
|
162
186
|
|
|
163
187
|
## Install
|
|
164
188
|
|
|
189
|
+
```bash
|
|
190
|
+
npm install -g claude-starter
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Or install from source:
|
|
194
|
+
|
|
165
195
|
```bash
|
|
166
196
|
git clone https://github.com/Bojun-Vvibe/claude-starter.git
|
|
167
197
|
cd claude-starter
|
|
@@ -175,6 +205,16 @@ Then run:
|
|
|
175
205
|
claude-starter
|
|
176
206
|
```
|
|
177
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
|
+
|
|
178
218
|
## Keyboard Shortcuts
|
|
179
219
|
|
|
180
220
|
| Key | Action |
|
|
@@ -182,12 +222,15 @@ claude-starter
|
|
|
182
222
|
| `↑` `↓` | Navigate sessions |
|
|
183
223
|
| `Enter` | Start new / resume selected session |
|
|
184
224
|
| `n` | New session |
|
|
225
|
+
| `d` | Resume with bypassPermissions (danger mode) |
|
|
226
|
+
| `m` | Permission mode picker |
|
|
185
227
|
| `/` | Search |
|
|
186
228
|
| `Backspace` | Edit search, auto-exit when empty |
|
|
187
229
|
| `Esc` | Clear filter |
|
|
188
230
|
| `p` | Filter by project |
|
|
189
|
-
| `s` | Cycle sort mode |
|
|
231
|
+
| `s` | Cycle sort mode (time/size/messages/project) |
|
|
190
232
|
| `c` | Copy session ID |
|
|
233
|
+
| `x` / `Delete` | Delete session |
|
|
191
234
|
| `Home` / `End` | Jump to first / last |
|
|
192
235
|
| `Ctrl-D` / `Ctrl-U` | Page down / up |
|
|
193
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
|
|
@@ -18,9 +20,12 @@
|
|
|
18
20
|
* p Filter by project (popup)
|
|
19
21
|
* s Cycle sort: time → size → messages → project
|
|
20
22
|
* n Start new session
|
|
23
|
+
* d Resume with bypassPermissions (danger mode)
|
|
24
|
+
* m Permission mode picker
|
|
21
25
|
* Home / End Jump to top / bottom
|
|
22
26
|
* Ctrl-D/U Page down / up
|
|
23
27
|
* c Copy session ID to clipboard
|
|
28
|
+
* x / Delete Delete selected session
|
|
24
29
|
* q / Ctrl-C Quit
|
|
25
30
|
*/
|
|
26
31
|
|
|
@@ -32,37 +37,72 @@ const os = require('os');
|
|
|
32
37
|
|
|
33
38
|
// ─── CLI Detection ──────────────────────────────────────────────────────────
|
|
34
39
|
// Detect whether `mai-claude` is available (binary, alias, or function).
|
|
35
|
-
//
|
|
36
|
-
//
|
|
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)`.
|
|
37
46
|
//
|
|
38
47
|
// Returns { name, cmd } where:
|
|
39
48
|
// name = display label ("mai-claude" or "claude")
|
|
40
49
|
// cmd = the actual command string to spawn (resolves aliases)
|
|
41
50
|
|
|
42
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
|
+
|
|
43
60
|
const shell = process.env.SHELL || '/bin/sh';
|
|
61
|
+
|
|
62
|
+
// 1) Non-interactive: check if mai-claude exists as a binary on PATH
|
|
44
63
|
try {
|
|
45
|
-
const
|
|
64
|
+
const binPath = execSync('command -v mai-claude 2>/dev/null', {
|
|
46
65
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
66
|
timeout: 3000,
|
|
67
|
+
shell: true,
|
|
48
68
|
}).toString().trim();
|
|
69
|
+
if (binPath) {
|
|
70
|
+
return { name: 'mai-claude', cmd: 'mai-claude' };
|
|
71
|
+
}
|
|
72
|
+
} catch { /* not found as binary, continue */ }
|
|
49
73
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
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
|
+
}
|
|
60
102
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return { name: 'claude', cmd: 'claude' };
|
|
65
|
-
}
|
|
103
|
+
} catch { /* alias resolution failed, fall back to claude */ }
|
|
104
|
+
|
|
105
|
+
return { name: 'claude', cmd: 'claude' };
|
|
66
106
|
}
|
|
67
107
|
|
|
68
108
|
const CLI = detectCLI();
|
|
@@ -76,15 +116,86 @@ const PROJECT_COLORS = [
|
|
|
76
116
|
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
77
117
|
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
78
118
|
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
119
|
+
const META_FILE = path.join(CLAUDE_DIR, 'claude-starter-meta.json');
|
|
120
|
+
|
|
121
|
+
// ─── Session Meta ────────────────────────────────────────────────────
|
|
122
|
+
// Stores user-defined metadata for sessions in a simple JSON file.
|
|
123
|
+
|
|
124
|
+
function loadMeta() {
|
|
125
|
+
try {
|
|
126
|
+
if (fs.existsSync(META_FILE)) {
|
|
127
|
+
return JSON.parse(fs.readFileSync(META_FILE, 'utf-8'));
|
|
128
|
+
}
|
|
129
|
+
} catch (e) { /* corrupt file, start fresh */ }
|
|
130
|
+
return { sessions: {} };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const PERMISSION_MODES = ['default', 'bypassPermissions', 'acceptEdits', 'dontAsk', 'plan', 'auto'];
|
|
134
|
+
|
|
135
|
+
function saveMeta(meta) {
|
|
136
|
+
try {
|
|
137
|
+
fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2), 'utf-8');
|
|
138
|
+
} catch (e) { /* silently fail */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getSessionMeta(meta, sessionId) {
|
|
142
|
+
return meta.sessions[sessionId] || {};
|
|
143
|
+
}
|
|
144
|
+
|
|
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 '';
|
|
152
|
+
}
|
|
153
|
+
|
|
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;
|
|
158
|
+
saveMeta(meta);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function setGlobalPermissionMode(meta, mode) {
|
|
162
|
+
meta.defaultPermissionMode = mode || undefined;
|
|
163
|
+
if (!mode) delete meta.defaultPermissionMode;
|
|
164
|
+
saveMeta(meta);
|
|
165
|
+
}
|
|
166
|
+
|
|
79
167
|
|
|
80
168
|
// ─── Data Layer ──────────────────────────────────────────────────────────────
|
|
81
169
|
|
|
82
170
|
function getProjectDisplayName(dirName) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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() || '~';
|
|
88
199
|
}
|
|
89
200
|
|
|
90
201
|
function loadSessionQuick(filePath, projectName) {
|
|
@@ -107,9 +218,10 @@ function loadSessionQuick(filePath, projectName) {
|
|
|
107
218
|
const tailStr = tailBuf.toString('utf-8');
|
|
108
219
|
|
|
109
220
|
let firstTs = null, lastTs = null;
|
|
110
|
-
let version = '', gitBranch = '', cwd = '';
|
|
221
|
+
let version = '', gitBranch = '', cwd = '', permissionMode = '';
|
|
111
222
|
let firstUserMsg = '';
|
|
112
223
|
let userMsgCount = 0;
|
|
224
|
+
let customTitle = '';
|
|
113
225
|
|
|
114
226
|
const headLines = headStr.split('\n').filter(Boolean);
|
|
115
227
|
for (const line of headLines) {
|
|
@@ -121,6 +233,8 @@ function loadSessionQuick(filePath, projectName) {
|
|
|
121
233
|
if (!version && d.version) version = d.version;
|
|
122
234
|
if (!gitBranch && d.gitBranch) gitBranch = d.gitBranch;
|
|
123
235
|
if (!cwd && d.cwd) cwd = d.cwd;
|
|
236
|
+
if (!permissionMode && d.permissionMode) permissionMode = d.permissionMode;
|
|
237
|
+
if (d.type === 'custom-title' && d.customTitle) customTitle = d.customTitle;
|
|
124
238
|
if (d.type === 'user') {
|
|
125
239
|
userMsgCount++;
|
|
126
240
|
if (!firstUserMsg) firstUserMsg = extractUserText(d);
|
|
@@ -135,6 +249,7 @@ function loadSessionQuick(filePath, projectName) {
|
|
|
135
249
|
const d = JSON.parse(line);
|
|
136
250
|
if (d.timestamp) lastTs = d.timestamp;
|
|
137
251
|
if (d.type === 'user') userMsgCount++;
|
|
252
|
+
if (d.type === 'custom-title' && d.customTitle) customTitle = d.customTitle;
|
|
138
253
|
} catch (e) { /* partial line */ }
|
|
139
254
|
}
|
|
140
255
|
}
|
|
@@ -157,6 +272,7 @@ function loadSessionQuick(filePath, projectName) {
|
|
|
157
272
|
return {
|
|
158
273
|
sessionId, project: projectName,
|
|
159
274
|
topic: topic || '(no user messages)',
|
|
275
|
+
customTitle, permissionMode,
|
|
160
276
|
firstTs, lastTs, version, gitBranch, cwd,
|
|
161
277
|
fileSize: stat.size, duration: durationStr,
|
|
162
278
|
estimatedMessages, filePath, _detailLoaded: false,
|
|
@@ -189,6 +305,7 @@ function loadSessionDetail(session) {
|
|
|
189
305
|
for (const line of lines) {
|
|
190
306
|
try {
|
|
191
307
|
const d = JSON.parse(line);
|
|
308
|
+
if (d.type === 'custom-title' && d.customTitle) session.customTitle = d.customTitle;
|
|
192
309
|
if (d.type === 'user') {
|
|
193
310
|
totalMessages++;
|
|
194
311
|
const text = extractUserText(d);
|
|
@@ -242,7 +359,11 @@ function loadAllSessions() {
|
|
|
242
359
|
for (const file of files) {
|
|
243
360
|
try {
|
|
244
361
|
const session = loadSessionQuick(path.join(projPath, file), projectName);
|
|
245
|
-
|
|
362
|
+
// Skip sessions without timestamps, without real user messages, or warmup sessions
|
|
363
|
+
if (session.firstTs
|
|
364
|
+
&& session.topic !== '(no user messages)'
|
|
365
|
+
&& !/^warmup$/i.test(session.topic.trim())
|
|
366
|
+
) sessions.push(session);
|
|
246
367
|
} catch (e) { /* skip */ }
|
|
247
368
|
}
|
|
248
369
|
}
|
|
@@ -312,6 +433,7 @@ function runListMode(limit) {
|
|
|
312
433
|
|
|
313
434
|
function createApp() {
|
|
314
435
|
const allSessions = loadAllSessions();
|
|
436
|
+
const meta = loadMeta();
|
|
315
437
|
let filteredSessions = [...allSessions];
|
|
316
438
|
let selectedIndex = -1; // -1 = "New Session", 0+ = session index
|
|
317
439
|
let filterText = '';
|
|
@@ -324,12 +446,17 @@ function createApp() {
|
|
|
324
446
|
|
|
325
447
|
// ─── Screen ────────────────────────────────────────────────────────────
|
|
326
448
|
const screen = blessed.screen({
|
|
327
|
-
smartCSR:
|
|
449
|
+
smartCSR: false,
|
|
450
|
+
fastCSR: false,
|
|
328
451
|
title: 'Claude Starter',
|
|
329
452
|
fullUnicode: true,
|
|
330
453
|
autoPadding: true,
|
|
454
|
+
dockBorders: true,
|
|
331
455
|
});
|
|
332
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
|
+
|
|
333
460
|
// ─── Header ────────────────────────────────────────────────────────────
|
|
334
461
|
const header = blessed.box({
|
|
335
462
|
parent: screen, top: 0, left: 0, width: '100%', height: 3,
|
|
@@ -337,17 +464,20 @@ function createApp() {
|
|
|
337
464
|
});
|
|
338
465
|
|
|
339
466
|
function updateHeader() {
|
|
340
|
-
const title = '{bold}{#7aa2f7-fg}
|
|
467
|
+
const title = '{bold}{#7aa2f7-fg}Claude Starter{/}';
|
|
341
468
|
const count = `{#9ece6a-fg}${filteredSessions.length}{/}{#565f89-fg}/${allSessions.length} sessions{/}`;
|
|
342
469
|
const proj = `{#bb9af7-fg}${uniqueProjects.length}{/}{#565f89-fg} projects{/}`;
|
|
343
|
-
const sort = `{#73daca-fg}
|
|
470
|
+
const sort = `{#73daca-fg}[${sortMode}]{/}`;
|
|
344
471
|
const search = isSearchMode
|
|
345
472
|
? `{#e0af68-fg}/ ${filterText}▌{/}`
|
|
346
473
|
: (filterText ? `{#e0af68-fg}/ ${filterText}{/}` : '');
|
|
347
|
-
|
|
474
|
+
let parts = [title, count, proj];
|
|
475
|
+
parts.push(sort);
|
|
476
|
+
if (search) parts.push(search);
|
|
477
|
+
header.setContent(`\n ${parts.join(' {#414868-fg}│{/} ')}`);
|
|
348
478
|
}
|
|
349
479
|
|
|
350
|
-
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' } });
|
|
351
481
|
|
|
352
482
|
// ─── Left Panel: blessed.list for correct scroll tracking ──────────────
|
|
353
483
|
const listPanel = blessed.list({
|
|
@@ -368,7 +498,7 @@ function createApp() {
|
|
|
368
498
|
interactive: true,
|
|
369
499
|
});
|
|
370
500
|
|
|
371
|
-
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' } });
|
|
372
502
|
|
|
373
503
|
// ─── Right Panel ───────────────────────────────────────────────────────
|
|
374
504
|
const detailPanel = blessed.box({
|
|
@@ -380,7 +510,7 @@ function createApp() {
|
|
|
380
510
|
mouse: true,
|
|
381
511
|
});
|
|
382
512
|
|
|
383
|
-
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' } });
|
|
384
514
|
|
|
385
515
|
// ─── Footer ────────────────────────────────────────────────────────────
|
|
386
516
|
const footer = blessed.box({
|
|
@@ -390,13 +520,15 @@ function createApp() {
|
|
|
390
520
|
|
|
391
521
|
function updateFooter() {
|
|
392
522
|
const keys = [
|
|
393
|
-
'{#7aa2f7-fg}{bold}↵{/} {#565f89-fg}Start/Resume{/}',
|
|
394
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{/}',
|
|
395
527
|
'{#7aa2f7-fg}{bold}/{/} {#565f89-fg}Search{/}',
|
|
396
|
-
'{#7aa2f7-fg}{bold}↑/↓{/} {#565f89-fg}Nav{/}',
|
|
397
528
|
'{#7aa2f7-fg}{bold}p{/} {#565f89-fg}Project{/}',
|
|
398
529
|
'{#7aa2f7-fg}{bold}s{/} {#565f89-fg}Sort{/}',
|
|
399
530
|
'{#7aa2f7-fg}{bold}c{/} {#565f89-fg}Copy ID{/}',
|
|
531
|
+
'{#f7768e-fg}{bold}x{/} {#565f89-fg}Delete{/}',
|
|
400
532
|
'{#7aa2f7-fg}{bold}q{/} {#565f89-fg}Quit{/}',
|
|
401
533
|
];
|
|
402
534
|
footer.setContent(`\n ${keys.join(' {#414868-fg}│{/} ')}`);
|
|
@@ -420,7 +552,7 @@ function createApp() {
|
|
|
420
552
|
const branch = session.gitBranch
|
|
421
553
|
? `{#73daca-fg}${session.gitBranch.substring(0, 25)}{/}`
|
|
422
554
|
: '';
|
|
423
|
-
const dur = session.duration ? `{#565f89-fg}
|
|
555
|
+
const dur = session.duration ? `{#565f89-fg}${session.duration}{/}` : '';
|
|
424
556
|
|
|
425
557
|
// Compose a multi-line string for each list item.
|
|
426
558
|
// blessed.list renders each item as a single row, so we pack info densely.
|
|
@@ -441,23 +573,32 @@ function createApp() {
|
|
|
441
573
|
|
|
442
574
|
// ─── Populate list ─────────────────────────────────────────────────────
|
|
443
575
|
// Index 0 = "New Session", index 1+ = sessions
|
|
444
|
-
const NEW_SESSION_LABEL = ' {#9ece6a-fg}{bold}
|
|
576
|
+
const NEW_SESSION_LABEL = ' {#9ece6a-fg}{bold}+ New Conversation{/}';
|
|
445
577
|
|
|
446
578
|
function refreshList() {
|
|
447
579
|
const listW = Math.floor((screen.width || 100) / 2) - 2;
|
|
448
580
|
|
|
449
581
|
const sessionItems = filteredSessions.map((session) => {
|
|
450
582
|
const color = getProjectColor(session.project, projectColorMap);
|
|
451
|
-
const
|
|
583
|
+
const eMode = getEffectivePermissionMode(meta, session);
|
|
584
|
+
const modeIcon = (eMode === 'bypassPermissions') ? '{#f7768e-fg}!{/}' : ' ';
|
|
585
|
+
const proj = `{${color}-fg}${session.project.substring(0, 12).padEnd(12)}{/}`;
|
|
452
586
|
const time = `{#e0af68-fg}${formatTimestamp(session.lastTs).padEnd(16)}{/}`;
|
|
453
|
-
const msgs = `{#7aa2f7-fg}${String(session.estimatedMessages).padStart(4)}{/}{#565f89-fg}m{/}`;
|
|
454
587
|
|
|
455
|
-
const fixedLen =
|
|
456
|
-
const topicMaxLen = Math.max(
|
|
457
|
-
let topic = session.topic;
|
|
588
|
+
const fixedLen = 1 + 12 + 1 + 16 + 1 + 3;
|
|
589
|
+
const topicMaxLen = Math.max(10, listW - fixedLen);
|
|
590
|
+
let topic = session.customTitle || session.topic;
|
|
591
|
+
|
|
458
592
|
if (topic.length > topicMaxLen) topic = topic.substring(0, topicMaxLen) + '…';
|
|
459
593
|
|
|
460
|
-
|
|
594
|
+
let label = `${modeIcon}${proj} ${time} `;
|
|
595
|
+
if (session.customTitle) {
|
|
596
|
+
label += `{#73daca-fg}{bold}${esc(topic)}{/}`;
|
|
597
|
+
} else {
|
|
598
|
+
label += `{#a9b1d6-fg}${esc(topic)}{/}`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return label;
|
|
461
602
|
});
|
|
462
603
|
|
|
463
604
|
const items = [NEW_SESSION_LABEL, ...sessionItems];
|
|
@@ -471,14 +612,19 @@ function createApp() {
|
|
|
471
612
|
function renderDetail() {
|
|
472
613
|
if (selectedIndex === -1) {
|
|
473
614
|
const cli = CLI.name;
|
|
615
|
+
const defaultMode = meta.defaultPermissionMode || '';
|
|
616
|
+
const modeFlag = (defaultMode && defaultMode !== 'default') ? ` --permission-mode ${defaultMode}` : '';
|
|
474
617
|
let c = '';
|
|
475
|
-
c += `\n {#9ece6a-fg}{bold}
|
|
618
|
+
c += `\n {#9ece6a-fg}{bold}Start a New Conversation{/}\n`;
|
|
476
619
|
c += ` {#414868-fg}${'─'.repeat(44)}{/}\n\n`;
|
|
477
620
|
c += ` {#a9b1d6-fg}Open a fresh Claude session and start{/}\n`;
|
|
478
621
|
c += ` {#a9b1d6-fg}coding from scratch.{/}\n\n`;
|
|
479
622
|
c += ` {#565f89-fg}Working Dir{/} {#7dcfff-fg}${process.cwd()}{/}\n`;
|
|
480
623
|
c += ` {#565f89-fg}CLI{/} {#73daca-fg}${cli}{/}\n`;
|
|
481
|
-
|
|
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`;
|
|
482
628
|
c += ` {#414868-fg}${'─'.repeat(44)}{/}\n`;
|
|
483
629
|
c += ` {#9ece6a-fg}{bold}↵ Enter{/}{#9ece6a-fg} or {/}{#9ece6a-fg}{bold}n{/}{#9ece6a-fg} to launch{/}\n`;
|
|
484
630
|
detailPanel.setContent(c);
|
|
@@ -498,7 +644,11 @@ function createApp() {
|
|
|
498
644
|
let c = '';
|
|
499
645
|
const sep = ` {#414868-fg}${'─'.repeat(44)}{/}`;
|
|
500
646
|
|
|
647
|
+
// Title
|
|
501
648
|
c += `\n {${color}-fg}{bold}█ ${session.project}{/}\n`;
|
|
649
|
+
if (session.customTitle) {
|
|
650
|
+
c += ` {#73daca-fg}{bold}${esc(session.customTitle)}{/}\n`;
|
|
651
|
+
}
|
|
502
652
|
c += sep + '\n\n';
|
|
503
653
|
|
|
504
654
|
const fields = [
|
|
@@ -513,6 +663,12 @@ function createApp() {
|
|
|
513
663
|
if (session.version) fields.push(['Claude', `{#565f89-fg}v${session.version}{/}`]);
|
|
514
664
|
if (session.cwd) fields.push(['Directory', `{#565f89-fg}${session.cwd}{/}`]);
|
|
515
665
|
|
|
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}{/}`]);
|
|
670
|
+
}
|
|
671
|
+
|
|
516
672
|
for (const [label, value] of fields) {
|
|
517
673
|
c += ` {#565f89-fg}${label.padEnd(12)}{/} ${value}\n`;
|
|
518
674
|
}
|
|
@@ -524,7 +680,7 @@ function createApp() {
|
|
|
524
680
|
if (session.toolsUsed.length > 10) c += ` {#565f89-fg}+${session.toolsUsed.length - 10} more{/}\n`;
|
|
525
681
|
}
|
|
526
682
|
|
|
527
|
-
c += `\n {#bb9af7-fg}{bold}
|
|
683
|
+
c += `\n {#bb9af7-fg}{bold}Conversation{/}\n`;
|
|
528
684
|
c += sep + '\n';
|
|
529
685
|
|
|
530
686
|
const msgs = (session.userMessages || []).slice(0, 10);
|
|
@@ -536,11 +692,11 @@ function createApp() {
|
|
|
536
692
|
msgs.forEach((msg, i) => {
|
|
537
693
|
const clean = esc(msg.replace(/\n/g, ' ').trim());
|
|
538
694
|
const trunc = clean.length > 80 ? clean.substring(0, 80) + '…' : clean;
|
|
539
|
-
c += `\n {#7aa2f7-fg}{bold}You
|
|
695
|
+
c += `\n {#7aa2f7-fg}{bold}You >{/} ${trunc}\n`;
|
|
540
696
|
if (assists[i]) {
|
|
541
697
|
const aClean = esc(assists[i].replace(/\n/g, ' ').trim());
|
|
542
698
|
const aTrunc = aClean.length > 80 ? aClean.substring(0, 80) + '…' : aClean;
|
|
543
|
-
c += ` {#9ece6a-fg}Claude
|
|
699
|
+
c += ` {#9ece6a-fg}Claude >{/} {#565f89-fg}${aTrunc}{/}\n`;
|
|
544
700
|
}
|
|
545
701
|
});
|
|
546
702
|
}
|
|
@@ -570,8 +726,11 @@ function createApp() {
|
|
|
570
726
|
} else {
|
|
571
727
|
const terms = filterText.toLowerCase().split(/\s+/);
|
|
572
728
|
filteredSessions = allSessions.filter(s => {
|
|
573
|
-
const haystack = [s.project, s.topic, s.gitBranch || '', s.sessionId, ...(s.userMessages || [])].join(' ').toLowerCase();
|
|
574
|
-
|
|
729
|
+
const haystack = [s.project, s.topic, s.customTitle || '', s.gitBranch || '', s.sessionId, ...(s.userMessages || [])].join(' ').toLowerCase();
|
|
730
|
+
|
|
731
|
+
return terms.every(t => {
|
|
732
|
+
return haystack.includes(t);
|
|
733
|
+
});
|
|
575
734
|
});
|
|
576
735
|
}
|
|
577
736
|
selectedIndex = Math.min(selectedIndex, Math.max(-1, filteredSessions.length - 1));
|
|
@@ -670,14 +829,17 @@ function createApp() {
|
|
|
670
829
|
}
|
|
671
830
|
|
|
672
831
|
screen.key(['down'], () => {
|
|
832
|
+
if (popupOpen) return;
|
|
673
833
|
if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
|
|
674
834
|
moveSelection(1);
|
|
675
835
|
});
|
|
676
836
|
screen.key(['up'], () => {
|
|
837
|
+
if (popupOpen) return;
|
|
677
838
|
if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
|
|
678
839
|
moveSelection(-1);
|
|
679
840
|
});
|
|
680
841
|
screen.key(['home'], () => {
|
|
842
|
+
if (popupOpen) return;
|
|
681
843
|
if (isSearchMode) { isSearchMode = false; }
|
|
682
844
|
selectedIndex = -1;
|
|
683
845
|
suppressSelectEvent = true; listPanel.select(0); suppressSelectEvent = false;
|
|
@@ -685,6 +847,7 @@ function createApp() {
|
|
|
685
847
|
renderDetail(); updateHeader(); screen.render();
|
|
686
848
|
});
|
|
687
849
|
screen.key(['end'], () => {
|
|
850
|
+
if (popupOpen) return;
|
|
688
851
|
if (isSearchMode) { isSearchMode = false; }
|
|
689
852
|
selectedIndex = Math.max(0, filteredSessions.length - 1);
|
|
690
853
|
suppressSelectEvent = true; listPanel.select(selectedIndex + 1); suppressSelectEvent = false;
|
|
@@ -692,10 +855,12 @@ function createApp() {
|
|
|
692
855
|
renderDetail(); updateHeader(); screen.render();
|
|
693
856
|
});
|
|
694
857
|
screen.key(['pagedown', 'C-d'], () => {
|
|
858
|
+
if (popupOpen) return;
|
|
695
859
|
if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
|
|
696
860
|
moveSelection(Math.floor((listPanel.height || 20) / 2));
|
|
697
861
|
});
|
|
698
862
|
screen.key(['pageup', 'C-u'], () => {
|
|
863
|
+
if (popupOpen) return;
|
|
699
864
|
if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
|
|
700
865
|
moveSelection(-Math.floor((listPanel.height || 20) / 2));
|
|
701
866
|
});
|
|
@@ -725,35 +890,46 @@ function createApp() {
|
|
|
725
890
|
// ─── Resume Session ─────────────────────────────────────────────────────
|
|
726
891
|
// Auto-detect: use mai-claude if available, otherwise fall back to claude
|
|
727
892
|
|
|
728
|
-
function resumeSession(session) {
|
|
893
|
+
function resumeSession(session, modeOverride) {
|
|
894
|
+
process.stdout.write('\x1b[0m');
|
|
729
895
|
screen.destroy();
|
|
730
896
|
|
|
731
897
|
const label = CLI.name;
|
|
898
|
+
const mode = modeOverride || getEffectivePermissionMode(meta, session);
|
|
899
|
+
const modeFlag = (mode && mode !== 'default') ? ` --permission-mode ${mode}` : '';
|
|
732
900
|
|
|
733
901
|
console.log(`\n\x1b[36m⚡ Resuming conversation with ${label}\x1b[0m`);
|
|
734
902
|
console.log(`\x1b[90m Session: ${session.sessionId}\x1b[0m`);
|
|
735
|
-
console.log(`\x1b[90m Project: ${session.project} │ Branch: ${session.gitBranch || 'N/A'} │ Messages: ${session.estimatedMessages}\x1b[0m
|
|
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('');
|
|
736
906
|
|
|
737
907
|
const child = spawn(
|
|
738
|
-
`${CLI.cmd} --resume ${session.sessionId}`,
|
|
908
|
+
`${CLI.cmd} --resume ${session.sessionId}${modeFlag}`,
|
|
739
909
|
{ stdio: 'inherit', cwd: session.cwd || process.cwd(), shell: true },
|
|
740
910
|
);
|
|
741
911
|
child.on('error', (err) => {
|
|
742
912
|
console.error(`\x1b[31mFailed to resume: ${err.message}\x1b[0m`);
|
|
743
|
-
console.log(`\x1b[33mManual: ${label} --resume ${session.sessionId}\x1b[0m`);
|
|
913
|
+
console.log(`\x1b[33mManual: ${label} --resume ${session.sessionId}${modeFlag}\x1b[0m`);
|
|
744
914
|
process.exit(1);
|
|
745
915
|
});
|
|
746
916
|
child.on('exit', (code) => process.exit(code || 0));
|
|
747
917
|
}
|
|
748
918
|
|
|
749
919
|
function startNewSession() {
|
|
920
|
+
process.stdout.write('\x1b[0m');
|
|
750
921
|
screen.destroy();
|
|
751
922
|
|
|
752
923
|
const label = CLI.name;
|
|
924
|
+
const mode = meta.defaultPermissionMode || '';
|
|
925
|
+
const modeFlag = (mode && mode !== 'default') ? ` --permission-mode ${mode}` : '';
|
|
753
926
|
|
|
754
|
-
console.log(`\n\x1b[36m✨ Starting new conversation with ${label}\x1b[0m
|
|
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('');
|
|
755
930
|
|
|
756
|
-
const
|
|
931
|
+
const cmd = modeFlag ? `${CLI.cmd}${modeFlag}` : CLI.cmd;
|
|
932
|
+
const child = spawn(cmd, { stdio: 'inherit', cwd: process.cwd(), shell: true });
|
|
757
933
|
child.on('error', (err) => {
|
|
758
934
|
console.error(`\x1b[31mFailed to start: ${err.message}\x1b[0m`);
|
|
759
935
|
process.exit(1);
|
|
@@ -789,13 +965,222 @@ function createApp() {
|
|
|
789
965
|
} catch (e) { /* silently fail */ }
|
|
790
966
|
});
|
|
791
967
|
|
|
968
|
+
|
|
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();
|
|
990
|
+
|
|
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
|
+
}
|
|
1003
|
+
|
|
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);
|
|
1008
|
+
|
|
1009
|
+
const items = [
|
|
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}{/}`;
|
|
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{/}',
|
|
1025
|
+
];
|
|
1026
|
+
|
|
1027
|
+
const popup = blessed.list({
|
|
1028
|
+
parent: screen, top: 'center', left: 'center',
|
|
1029
|
+
width: 42,
|
|
1030
|
+
height: Math.min(items.length + 4, 24),
|
|
1031
|
+
label: ' {bold}{#bb9af7-fg}Permission Mode{/} ',
|
|
1032
|
+
tags: true, border: { type: 'line' },
|
|
1033
|
+
style: {
|
|
1034
|
+
border: { fg: '#bb9af7' }, bg: '#24283b', fg: '#a9b1d6',
|
|
1035
|
+
selected: { bg: '#3d59a1', fg: 'white', bold: true },
|
|
1036
|
+
label: { fg: '#bb9af7' },
|
|
1037
|
+
},
|
|
1038
|
+
items: items, keys: true, vi: true, mouse: true,
|
|
1039
|
+
});
|
|
1040
|
+
popupOpen = true;
|
|
1041
|
+
popup.focus(); screen.render();
|
|
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
|
+
|
|
1050
|
+
popup.on('select', (item, index) => {
|
|
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);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
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;
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
popup.key(['escape', 'q'], () => {
|
|
1092
|
+
popup.destroy();
|
|
1093
|
+
popupOpen = false;
|
|
1094
|
+
renderAll();
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
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({
|
|
1139
|
+
parent: screen, top: 'center', left: 'center',
|
|
1140
|
+
width: 50, height: 9,
|
|
1141
|
+
label: ' {bold}{#f7768e-fg}Delete Session?{/} ',
|
|
1142
|
+
tags: true, border: { type: 'line' },
|
|
1143
|
+
style: {
|
|
1144
|
+
border: { fg: '#f7768e' }, bg: '#24283b', fg: '#a9b1d6',
|
|
1145
|
+
label: { fg: '#f7768e' },
|
|
1146
|
+
},
|
|
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{/}`,
|
|
1151
|
+
});
|
|
1152
|
+
popupOpen = true;
|
|
1153
|
+
confirmPopup.focus();
|
|
1154
|
+
screen.render();
|
|
1155
|
+
|
|
1156
|
+
confirmPopup.key(['y'], () => {
|
|
1157
|
+
confirmPopup.destroy();
|
|
1158
|
+
popupOpen = false;
|
|
1159
|
+
deleteSession(session);
|
|
1160
|
+
footer.setContent(`\n {#f7768e-fg}{bold}✗ Deleted:{/} {#565f89-fg}${session.sessionId}{/}`);
|
|
1161
|
+
renderAll();
|
|
1162
|
+
setTimeout(() => { updateFooter(); screen.render(); }, 1500);
|
|
1163
|
+
});
|
|
1164
|
+
confirmPopup.key(['n', 'escape', 'q'], () => {
|
|
1165
|
+
confirmPopup.destroy();
|
|
1166
|
+
popupOpen = false;
|
|
1167
|
+
screen.render();
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
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
|
+
|
|
792
1177
|
screen.key(['s'], () => { if (!isSearchMode) cycleSort(); });
|
|
793
1178
|
screen.key(['p'], () => { if (!isSearchMode) showProjectPicker(); });
|
|
794
1179
|
screen.key(['escape'], () => {
|
|
795
1180
|
if (isSearchMode) { isSearchMode = false; filterText = ''; applyFilter(); return; }
|
|
796
1181
|
filterText = ''; selectedIndex = -1; applyFilter();
|
|
797
1182
|
});
|
|
798
|
-
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); });
|
|
799
1184
|
|
|
800
1185
|
// Remove blessed's built-in wheel handlers (they call select which changes selection)
|
|
801
1186
|
listPanel.removeAllListeners('element wheeldown');
|
|
@@ -844,25 +1229,76 @@ function createApp() {
|
|
|
844
1229
|
|
|
845
1230
|
// ─── Entry Point ─────────────────────────────────────────────────────────────
|
|
846
1231
|
|
|
1232
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
1233
|
+
|
|
847
1234
|
const args = process.argv.slice(2);
|
|
848
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
|
+
|
|
849
1280
|
if (args.includes('--help') || args.includes('-h')) {
|
|
850
1281
|
console.log(`
|
|
851
|
-
\x1b[36m🚀 Claude Starter\x1b[0m
|
|
1282
|
+
\x1b[36m🚀 Claude Starter\x1b[0m \x1b[2mv${PKG.version}\x1b[0m
|
|
852
1283
|
|
|
853
1284
|
Usage:
|
|
854
|
-
claude-starter
|
|
855
|
-
claude-starter --list [N]
|
|
856
|
-
claude-starter --
|
|
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
|
|
857
1290
|
|
|
858
1291
|
TUI Keyboard Shortcuts:
|
|
859
1292
|
↑/↓ Navigate sessions
|
|
860
1293
|
Enter Start new / resume selected session
|
|
861
1294
|
n Start new session
|
|
1295
|
+
d Resume with bypassPermissions (danger mode)
|
|
1296
|
+
m Permission mode picker
|
|
862
1297
|
/ Search (fuzzy filter)
|
|
863
1298
|
p Filter by project
|
|
864
|
-
s Cycle sort mode
|
|
1299
|
+
s Cycle sort mode (time/size/messages/project)
|
|
865
1300
|
c Copy session ID
|
|
1301
|
+
x / Delete Delete selected session
|
|
866
1302
|
Home / End Jump to top / bottom
|
|
867
1303
|
Ctrl-D/U Page down / up
|
|
868
1304
|
Esc Clear filter
|