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.
- package/README.md +40 -40
- package/index.js +390 -211
- 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
|
-
| 🔍 | **即时搜索** | `/`
|
|
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
|
-
|
|
|
118
|
-
| `
|
|
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
|
|
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,
|
|
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
|
-
|
|
|
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
|
-
|
|
|
227
|
-
| `
|
|
228
|
-
|
|
|
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
|
|
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
|
|
21
|
+
* s Cycle sort: time → size → messages → project
|
|
20
22
|
* n Start new session
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* d Resume with bypassPermissions (danger mode)
|
|
24
|
+
* m Permission mode picker
|
|
23
25
|
* Home / End Jump to top / bottom
|
|
24
26
|
* Ctrl-D/U Page down / up
|
|
25
27
|
* c Copy session ID to clipboard
|
|
28
|
+
* x / Delete Delete selected session
|
|
26
29
|
* q / Ctrl-C Quit
|
|
27
30
|
*/
|
|
28
31
|
|
|
@@ -34,37 +37,72 @@ const os = require('os');
|
|
|
34
37
|
|
|
35
38
|
// ─── CLI Detection ──────────────────────────────────────────────────────────
|
|
36
39
|
// Detect whether `mai-claude` is available (binary, alias, or function).
|
|
37
|
-
//
|
|
38
|
-
//
|
|
40
|
+
// First checks PATH directly, then sources shell config non-interactively
|
|
41
|
+
// to resolve aliases. Falls back to plain `claude`.
|
|
42
|
+
//
|
|
43
|
+
// NOTE: We deliberately avoid `shell -i` (interactive mode) because it
|
|
44
|
+
// triggers SIGTTOU in terminals like Warp that strictly manage TTY process
|
|
45
|
+
// groups, causing `suspended (tty output)`.
|
|
39
46
|
//
|
|
40
47
|
// Returns { name, cmd } where:
|
|
41
48
|
// name = display label ("mai-claude" or "claude")
|
|
42
49
|
// cmd = the actual command string to spawn (resolves aliases)
|
|
43
50
|
|
|
44
51
|
function detectCLI() {
|
|
52
|
+
// Strategy:
|
|
53
|
+
// 1. First try non-interactive lookup (safe for all terminals including Warp)
|
|
54
|
+
// 2. Only fall back to interactive shell if needed for alias resolution
|
|
55
|
+
//
|
|
56
|
+
// IMPORTANT: avoid `shell -i` (interactive mode) — it can trigger SIGTTOU
|
|
57
|
+
// in terminals like Warp that strictly manage TTY process groups, causing
|
|
58
|
+
// the process to be suspended with "suspended (tty output)".
|
|
59
|
+
|
|
45
60
|
const shell = process.env.SHELL || '/bin/sh';
|
|
61
|
+
|
|
62
|
+
// 1) Non-interactive: check if mai-claude exists as a binary on PATH
|
|
46
63
|
try {
|
|
47
|
-
const
|
|
64
|
+
const binPath = execSync('command -v mai-claude 2>/dev/null', {
|
|
48
65
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
66
|
timeout: 3000,
|
|
67
|
+
shell: true,
|
|
50
68
|
}).toString().trim();
|
|
69
|
+
if (binPath) {
|
|
70
|
+
return { name: 'mai-claude', cmd: 'mai-claude' };
|
|
71
|
+
}
|
|
72
|
+
} catch { /* not found as binary, continue */ }
|
|
51
73
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
74
|
+
// 2) Source shell config non-interactively to resolve aliases/functions.
|
|
75
|
+
// This avoids `-i` which would try to claim the TTY and risk SIGTTOU.
|
|
76
|
+
try {
|
|
77
|
+
const isZsh = shell.endsWith('/zsh');
|
|
78
|
+
const rcFile = isZsh
|
|
79
|
+
? path.join(os.homedir(), '.zshrc')
|
|
80
|
+
: path.join(os.homedir(), '.bashrc');
|
|
81
|
+
|
|
82
|
+
if (fs.existsSync(rcFile)) {
|
|
83
|
+
const raw = execSync(
|
|
84
|
+
`${shell} -c 'source "${rcFile}" 2>/dev/null; command -v mai-claude 2>/dev/null'`,
|
|
85
|
+
{
|
|
86
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
87
|
+
timeout: 3000,
|
|
88
|
+
env: { ...process.env, PS1: '', PROMPT: '', NO_TTY: '1' },
|
|
89
|
+
},
|
|
90
|
+
).toString().trim();
|
|
91
|
+
|
|
92
|
+
if (raw) {
|
|
93
|
+
const lines = raw.split('\n');
|
|
94
|
+
const aliasLine = lines.find(l => l.startsWith('alias ')) || lines[lines.length - 1];
|
|
95
|
+
|
|
96
|
+
const aliasMatch = aliasLine.match(/^alias [^=]+=(?:'(.+)'|"(.+)")$/s);
|
|
97
|
+
if (aliasMatch) {
|
|
98
|
+
return { name: 'mai-claude', cmd: aliasMatch[1] || aliasMatch[2] };
|
|
99
|
+
}
|
|
100
|
+
return { name: 'mai-claude', cmd: 'mai-claude' };
|
|
101
|
+
}
|
|
62
102
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return { name: 'claude', cmd: 'claude' };
|
|
67
|
-
}
|
|
103
|
+
} catch { /* alias resolution failed, fall back to claude */ }
|
|
104
|
+
|
|
105
|
+
return { name: 'claude', cmd: 'claude' };
|
|
68
106
|
}
|
|
69
107
|
|
|
70
108
|
const CLI = detectCLI();
|
|
@@ -80,11 +118,8 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
|
80
118
|
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
81
119
|
const META_FILE = path.join(CLAUDE_DIR, 'claude-starter-meta.json');
|
|
82
120
|
|
|
83
|
-
// ─── Session Meta
|
|
121
|
+
// ─── Session Meta ────────────────────────────────────────────────────
|
|
84
122
|
// Stores user-defined metadata for sessions in a simple JSON file.
|
|
85
|
-
// Schema: { "sessions": { "<sessionId>": { "favorite": bool, "tags": string[] } } }
|
|
86
|
-
|
|
87
|
-
const DEFAULT_TAGS = ['bug-fix', 'feature', 'refactor', 'debug', 'review', 'config', 'docs', 'experiment'];
|
|
88
123
|
|
|
89
124
|
function loadMeta() {
|
|
90
125
|
try {
|
|
@@ -95,6 +130,8 @@ function loadMeta() {
|
|
|
95
130
|
return { sessions: {} };
|
|
96
131
|
}
|
|
97
132
|
|
|
133
|
+
const PERMISSION_MODES = ['default', 'bypassPermissions', 'acceptEdits', 'dontAsk', 'plan', 'auto'];
|
|
134
|
+
|
|
98
135
|
function saveMeta(meta) {
|
|
99
136
|
try {
|
|
100
137
|
fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2), 'utf-8');
|
|
@@ -102,38 +139,63 @@ function saveMeta(meta) {
|
|
|
102
139
|
}
|
|
103
140
|
|
|
104
141
|
function getSessionMeta(meta, sessionId) {
|
|
105
|
-
return meta.sessions[sessionId] || {
|
|
142
|
+
return meta.sessions[sessionId] || {};
|
|
106
143
|
}
|
|
107
144
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return
|
|
145
|
+
function getEffectivePermissionMode(meta, session) {
|
|
146
|
+
// Priority: per-session override > session's original mode from JSONL > global default
|
|
147
|
+
const sm = meta.sessions[session.sessionId];
|
|
148
|
+
if (sm && sm.permissionMode) return sm.permissionMode;
|
|
149
|
+
if (session.permissionMode) return session.permissionMode;
|
|
150
|
+
if (meta.defaultPermissionMode) return meta.defaultPermissionMode;
|
|
151
|
+
return '';
|
|
113
152
|
}
|
|
114
153
|
|
|
115
|
-
function
|
|
116
|
-
if (!meta.sessions[sessionId]) meta.sessions[sessionId] = {
|
|
117
|
-
meta.sessions[sessionId].
|
|
154
|
+
function setSessionPermissionMode(meta, sessionId, mode) {
|
|
155
|
+
if (!meta.sessions[sessionId]) meta.sessions[sessionId] = {};
|
|
156
|
+
meta.sessions[sessionId].permissionMode = mode || undefined;
|
|
157
|
+
if (!mode) delete meta.sessions[sessionId].permissionMode;
|
|
118
158
|
saveMeta(meta);
|
|
119
159
|
}
|
|
120
160
|
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
return [...tags];
|
|
161
|
+
function setGlobalPermissionMode(meta, mode) {
|
|
162
|
+
meta.defaultPermissionMode = mode || undefined;
|
|
163
|
+
if (!mode) delete meta.defaultPermissionMode;
|
|
164
|
+
saveMeta(meta);
|
|
127
165
|
}
|
|
128
166
|
|
|
167
|
+
|
|
129
168
|
// ─── Data Layer ──────────────────────────────────────────────────────────────
|
|
130
169
|
|
|
131
170
|
function getProjectDisplayName(dirName) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
171
|
+
// Claude stores projects as path with `-` separators, e.g.:
|
|
172
|
+
// -Users-bob-Desktop-MSProject-my-app
|
|
173
|
+
// -Users-bob-Projects-Router-Maestro
|
|
174
|
+
// -Users-bob-Desktop-GraphConnector
|
|
175
|
+
// -Users-bob
|
|
176
|
+
//
|
|
177
|
+
// Strategy: strip the user home prefix, then take the last meaningful path segment.
|
|
178
|
+
// This gives clean names like "my-app", "Router-Maestro", "GraphConnector".
|
|
179
|
+
|
|
180
|
+
// Remove leading -Users-<username> prefix
|
|
181
|
+
let name = dirName.replace(/^-Users-[^-]+/, '');
|
|
182
|
+
|
|
183
|
+
// If nothing left, it was just the home dir
|
|
184
|
+
if (!name || name === '-') return '~';
|
|
185
|
+
|
|
186
|
+
// Remove leading dash
|
|
187
|
+
name = name.replace(/^-/, '');
|
|
188
|
+
|
|
189
|
+
// Get the last path segment (split by common directory markers)
|
|
190
|
+
// e.g. "Desktop-MSProject-my-app" → "my-app"
|
|
191
|
+
// "Desktop-GraphConnector" → "GraphConnector"
|
|
192
|
+
// "Projects-Router-Maestro" → "Router-Maestro"
|
|
193
|
+
const knownPrefixes = /^(Desktop|Documents|Projects|Downloads|dev|src|code|repos|work|home)(?:-|$)/i;
|
|
194
|
+
while (knownPrefixes.test(name)) {
|
|
195
|
+
name = name.replace(/^[^-]+-?/, '');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return name || dirName.split('-').pop() || '~';
|
|
137
199
|
}
|
|
138
200
|
|
|
139
201
|
function loadSessionQuick(filePath, projectName) {
|
|
@@ -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:
|
|
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}
|
|
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
|
|
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}
|
|
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}
|
|
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
|
|
518
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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 = `${
|
|
594
|
+
let label = `${modeIcon}${proj} ${time} `;
|
|
540
595
|
if (session.customTitle) {
|
|
541
|
-
label += `{#73daca-fg}{bold}${esc(
|
|
596
|
+
label += `{#73daca-fg}{bold}${esc(topic)}{/}`;
|
|
542
597
|
} else {
|
|
543
|
-
label += `{#a9b1d6-fg}${esc(
|
|
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}
|
|
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
|
-
|
|
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
|
|
590
|
-
|
|
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}
|
|
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
|
-
|
|
610
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
const tagChips = sm.tags.map(t => `{#414868-fg}[{/}{#f7768e-fg}#${t}{/}{#414868-fg}]{/}`).join(' ');
|
|
616
|
-
c += `\n {#f7768e-fg}{bold}🏷️ Tags{/}\n`;
|
|
617
|
-
c += ` ${tagChips}\n`;
|
|
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}
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
943
|
-
const
|
|
944
|
-
const
|
|
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
|
-
' {#
|
|
948
|
-
...
|
|
949
|
-
const checked =
|
|
950
|
-
|
|
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:
|
|
957
|
-
height: Math.min(items.length + 4,
|
|
958
|
-
label: ' {bold}{#
|
|
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: '#
|
|
1034
|
+
border: { fg: '#bb9af7' }, bg: '#24283b', fg: '#a9b1d6',
|
|
962
1035
|
selected: { bg: '#3d59a1', fg: 'white', bold: true },
|
|
963
|
-
label: { fg: '#
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
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:
|
|
1011
|
-
label: ' {bold}{#f7768e-fg}
|
|
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
|
-
|
|
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
|
-
|
|
1153
|
+
confirmPopup.focus();
|
|
1021
1154
|
screen.render();
|
|
1022
1155
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1156
|
+
confirmPopup.key(['y'], () => {
|
|
1157
|
+
confirmPopup.destroy();
|
|
1025
1158
|
popupOpen = false;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
tags.add(tagName);
|
|
1031
|
-
setSessionTags(meta, session.sessionId, [...tags]);
|
|
1032
|
-
footer.setContent(`\n {#9ece6a-fg}{bold}✓ Tagged:{/} {#f7768e-fg}#${tagName}{/}`);
|
|
1033
|
-
renderAll();
|
|
1034
|
-
setTimeout(() => { updateFooter(); screen.render(); }, 1500);
|
|
1035
|
-
} else {
|
|
1036
|
-
renderAll();
|
|
1037
|
-
}
|
|
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
|
-
|
|
1041
|
-
inputBox.destroy();
|
|
1164
|
+
confirmPopup.key(['n', 'escape', 'q'], () => {
|
|
1165
|
+
confirmPopup.destroy();
|
|
1042
1166
|
popupOpen = false;
|
|
1043
|
-
|
|
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
|
|
1110
|
-
claude-starter --list [N]
|
|
1111
|
-
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
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
|
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
|