claude-starter 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +52 -9
  2. package/index.js +273 -16
  3. 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>git clone</code>&nbsp;&nbsp;→&nbsp;&nbsp;<code>npm link</code>&nbsp;&nbsp;→&nbsp;&nbsp;<code>claude-starter</code>
19
+ <code>npm install -g claude-starter</code>&nbsp;&nbsp;→&nbsp;&nbsp;<code>claude-starter</code>
19
20
  </p>
20
21
 
21
22
  <p align="center">
@@ -58,26 +59,45 @@ claude-starter
58
59
  - `auth` → 所有认证相关的对话
59
60
  - `refactor` → 上周的代码重构
60
61
  - `web-app fix` → 某个项目的 bug 修复
62
+ - `#bug-fix` → 所有打了 bug-fix 标签的对话
63
+ - `fav` → 所有收藏的对话
61
64
 
62
65
  **不需要管理模式,不需要确认。输入即搜,方向键即走。**
63
66
 
67
+ ## ⭐ 收藏 & 🏷️ 标签 — 组织你的对话
68
+
69
+ 按 `f` 收藏/取消收藏,按 `#` 打标签。
70
+
71
+ - **收藏**:重要的对话一键标星,排序切到 `favorites` 模式收藏置顶
72
+ - **标签**:预设 8 种标签(`bug-fix`、`feature`、`refactor` 等),支持自定义
73
+ - **搜索联动**:`/` 搜索支持 `#tag` 语法和 `fav` 关键字
74
+ - **持久化**:数据存在 `~/.claude/claude-starter-meta.json`,重启不丢失
75
+
64
76
  ## 核心能力
65
77
 
66
78
  | | 功能 | 说明 |
67
79
  |---|---|---|
68
80
  | 🎨 | **精美 TUI** | Tokyo Night 配色,分屏布局,终端里的 App |
69
81
  | ✨ | **一键新建** | 列表顶部直接新建对话 |
70
- | 🔍 | **即时搜索** | `/` 全文搜索,无需回车 |
82
+ | 🔍 | **即时搜索** | `/` 全文搜索,无需回车,支持 `#tag` 和 `fav` |
83
+ | ⭐ | **收藏** | `f` 收藏重要对话,排序置顶 |
84
+ | 🏷️ | **标签** | `#` 分类管理,预设 + 自定义标签 |
71
85
  | 📂 | **项目过滤** | `p` 按项目筛选 |
72
86
  | ⚡ | **秒级恢复** | 选中 → Enter → 回到对话 |
73
87
  | 📋 | **对话预览** | 右侧面板展示完整元数据和对话历史 |
74
- | 🔀 | **多种排序** | 时间 / 大小 / 消息数 / 项目 |
88
+ | 🔀 | **多种排序** | 时间 / 大小 / 消息数 / 项目 / 收藏 |
75
89
  | 📎 | **复制 ID** | `c` 一键复制到剪贴板 |
76
90
  | 🧠 | **智能 CLI** | 自动检测 `mai-claude` / `claude` |
77
91
  | 🔒 | **完全本地** | 不联网,不上传,不追踪 |
78
92
 
79
93
  ## 安装
80
94
 
95
+ ```bash
96
+ npm install -g claude-starter
97
+ ```
98
+
99
+ 或者从源码安装:
100
+
81
101
  ```bash
82
102
  git clone https://github.com/Bojun-Vvibe/claude-starter.git
83
103
  cd claude-starter
@@ -94,11 +114,13 @@ npm link
94
114
  | `↑` `↓` | 上下导航 |
95
115
  | `Enter` | 新建 / 恢复对话 |
96
116
  | `n` | 直接新建 |
97
- | `/` | 搜索 |
117
+ | `/` | 搜索(支持 `#tag` 和 `fav`) |
118
+ | `f` | 收藏 / 取消收藏 ⭐ |
119
+ | `#` | 添加 / 管理标签 🏷️ |
98
120
  | `Backspace` | 删除搜索字符,删空自动退出 |
99
121
  | `Esc` | 清空搜索 |
100
122
  | `p` | 按项目过滤 |
101
- | `s` | 切换排序 |
123
+ | `s` | 切换排序(时间/大小/消息数/项目/收藏) |
102
124
  | `c` | 复制 Session ID |
103
125
  | `Home` / `End` | 跳到顶 / 底 |
104
126
  | `Ctrl-D` / `Ctrl-U` | 翻页 |
@@ -142,26 +164,45 @@ Searches across **everything** — project names, Git branches, conversation con
142
164
  - `auth` → all auth-related sessions
143
165
  - `refactor` → that cleanup from last week
144
166
  - `web-app fix` → bug fixes in a specific project
167
+ - `#bug-fix` → all sessions tagged with bug-fix
168
+ - `fav` → all favorited sessions
145
169
 
146
170
  **No modes. No confirmation. Just type and go.**
147
171
 
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
+
148
181
  ## Features
149
182
 
150
183
  | | Feature | Description |
151
184
  |---|---|---|
152
185
  | 🎨 | **Beautiful TUI** | Tokyo Night color scheme, split-pane layout, feels native in your terminal |
153
186
  | ✨ | **New Session** | Launch a fresh conversation in one keystroke |
154
- | 🔍 | **Instant Search** | Fuzzy search across everything, no Enter needed |
187
+ | 🔍 | **Instant Search** | Fuzzy search across everything, supports `#tag` and `fav` |
188
+ | ⭐ | **Favorites** | Press `f` to star important sessions |
189
+ | 🏷️ | **Tags** | Press `#` to categorize with built-in + custom tags |
155
190
  | 📂 | **Project Filter** | Press `p` to filter by project |
156
191
  | ⚡ | **One-Key Resume** | Arrow, Enter, you're back in the conversation |
157
192
  | 📋 | **Session Preview** | Full metadata + conversation history in the right panel |
158
- | 🔀 | **Sort Modes** | Sort by time, size, messages, or project |
193
+ | 🔀 | **Sort Modes** | Sort by time, size, messages, project, or favorites |
159
194
  | 📎 | **Copy ID** | Press `c` to copy session ID |
160
195
  | 🧠 | **Smart CLI** | Auto-detects `mai-claude` vs `claude` |
161
196
  | 🔒 | **100% Local** | No network, no telemetry, no data leaves your machine |
162
197
 
163
198
  ## Install
164
199
 
200
+ ```bash
201
+ npm install -g claude-starter
202
+ ```
203
+
204
+ Or install from source:
205
+
165
206
  ```bash
166
207
  git clone https://github.com/Bojun-Vvibe/claude-starter.git
167
208
  cd claude-starter
@@ -182,11 +223,13 @@ claude-starter
182
223
  | `↑` `↓` | Navigate sessions |
183
224
  | `Enter` | Start new / resume selected session |
184
225
  | `n` | New session |
185
- | `/` | Search |
226
+ | `/` | Search (supports `#tag` and `fav`) |
227
+ | `f` | Toggle favorite ⭐ |
228
+ | `#` | Add/manage tags 🏷️ |
186
229
  | `Backspace` | Edit search, auto-exit when empty |
187
230
  | `Esc` | Clear filter |
188
231
  | `p` | Filter by project |
189
- | `s` | Cycle sort mode |
232
+ | `s` | Cycle sort mode (time/size/messages/project/favorites) |
190
233
  | `c` | Copy session ID |
191
234
  | `Home` / `End` | Jump to first / last |
192
235
  | `Ctrl-D` / `Ctrl-U` | Page down / up |
package/index.js CHANGED
@@ -16,8 +16,10 @@
16
16
  * / Start search (fuzzy filter)
17
17
  * Esc Clear search / cancel
18
18
  * p Filter by project (popup)
19
- * s Cycle sort: time → size → messages → project
19
+ * s Cycle sort: time → size → messages → project → favorites
20
20
  * n Start new session
21
+ * f Toggle favorite on selected session
22
+ * # Add/remove tags on selected session
21
23
  * Home / End Jump to top / bottom
22
24
  * Ctrl-D/U Page down / up
23
25
  * c Copy session ID to clipboard
@@ -76,6 +78,53 @@ const PROJECT_COLORS = [
76
78
  // ─── Paths ───────────────────────────────────────────────────────────────────
77
79
  const CLAUDE_DIR = path.join(os.homedir(), '.claude');
78
80
  const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
81
+ const META_FILE = path.join(CLAUDE_DIR, 'claude-starter-meta.json');
82
+
83
+ // ─── Session Meta (favorites & tags) ────────────────────────────────────────
84
+ // 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
+
89
+ function loadMeta() {
90
+ try {
91
+ if (fs.existsSync(META_FILE)) {
92
+ return JSON.parse(fs.readFileSync(META_FILE, 'utf-8'));
93
+ }
94
+ } catch (e) { /* corrupt file, start fresh */ }
95
+ return { sessions: {} };
96
+ }
97
+
98
+ function saveMeta(meta) {
99
+ try {
100
+ fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2), 'utf-8');
101
+ } catch (e) { /* silently fail */ }
102
+ }
103
+
104
+ function getSessionMeta(meta, sessionId) {
105
+ return meta.sessions[sessionId] || { favorite: false, tags: [] };
106
+ }
107
+
108
+ function toggleFavorite(meta, sessionId) {
109
+ if (!meta.sessions[sessionId]) meta.sessions[sessionId] = { favorite: false, tags: [] };
110
+ meta.sessions[sessionId].favorite = !meta.sessions[sessionId].favorite;
111
+ saveMeta(meta);
112
+ return meta.sessions[sessionId].favorite;
113
+ }
114
+
115
+ function setSessionTags(meta, sessionId, tags) {
116
+ if (!meta.sessions[sessionId]) meta.sessions[sessionId] = { favorite: false, tags: [] };
117
+ meta.sessions[sessionId].tags = tags;
118
+ saveMeta(meta);
119
+ }
120
+
121
+ function getAllUsedTags(meta) {
122
+ const tags = new Set();
123
+ for (const s of Object.values(meta.sessions)) {
124
+ if (s.tags) s.tags.forEach(t => tags.add(t));
125
+ }
126
+ return [...tags];
127
+ }
79
128
 
80
129
  // ─── Data Layer ──────────────────────────────────────────────────────────────
81
130
 
@@ -110,6 +159,7 @@ function loadSessionQuick(filePath, projectName) {
110
159
  let version = '', gitBranch = '', cwd = '';
111
160
  let firstUserMsg = '';
112
161
  let userMsgCount = 0;
162
+ let customTitle = '';
113
163
 
114
164
  const headLines = headStr.split('\n').filter(Boolean);
115
165
  for (const line of headLines) {
@@ -121,6 +171,7 @@ function loadSessionQuick(filePath, projectName) {
121
171
  if (!version && d.version) version = d.version;
122
172
  if (!gitBranch && d.gitBranch) gitBranch = d.gitBranch;
123
173
  if (!cwd && d.cwd) cwd = d.cwd;
174
+ if (d.type === 'custom-title' && d.customTitle) customTitle = d.customTitle;
124
175
  if (d.type === 'user') {
125
176
  userMsgCount++;
126
177
  if (!firstUserMsg) firstUserMsg = extractUserText(d);
@@ -135,6 +186,7 @@ function loadSessionQuick(filePath, projectName) {
135
186
  const d = JSON.parse(line);
136
187
  if (d.timestamp) lastTs = d.timestamp;
137
188
  if (d.type === 'user') userMsgCount++;
189
+ if (d.type === 'custom-title' && d.customTitle) customTitle = d.customTitle;
138
190
  } catch (e) { /* partial line */ }
139
191
  }
140
192
  }
@@ -157,6 +209,7 @@ function loadSessionQuick(filePath, projectName) {
157
209
  return {
158
210
  sessionId, project: projectName,
159
211
  topic: topic || '(no user messages)',
212
+ customTitle,
160
213
  firstTs, lastTs, version, gitBranch, cwd,
161
214
  fileSize: stat.size, duration: durationStr,
162
215
  estimatedMessages, filePath, _detailLoaded: false,
@@ -189,6 +242,7 @@ function loadSessionDetail(session) {
189
242
  for (const line of lines) {
190
243
  try {
191
244
  const d = JSON.parse(line);
245
+ if (d.type === 'custom-title' && d.customTitle) session.customTitle = d.customTitle;
192
246
  if (d.type === 'user') {
193
247
  totalMessages++;
194
248
  const text = extractUserText(d);
@@ -242,7 +296,11 @@ function loadAllSessions() {
242
296
  for (const file of files) {
243
297
  try {
244
298
  const session = loadSessionQuick(path.join(projPath, file), projectName);
245
- if (session.firstTs) sessions.push(session);
299
+ // Skip sessions without timestamps, without real user messages, or warmup sessions
300
+ if (session.firstTs
301
+ && session.topic !== '(no user messages)'
302
+ && !/^warmup$/i.test(session.topic.trim())
303
+ ) sessions.push(session);
246
304
  } catch (e) { /* skip */ }
247
305
  }
248
306
  }
@@ -312,6 +370,7 @@ function runListMode(limit) {
312
370
 
313
371
  function createApp() {
314
372
  const allSessions = loadAllSessions();
373
+ const meta = loadMeta();
315
374
  let filteredSessions = [...allSessions];
316
375
  let selectedIndex = -1; // -1 = "New Session", 0+ = session index
317
376
  let filterText = '';
@@ -340,11 +399,17 @@ function createApp() {
340
399
  const title = '{bold}{#7aa2f7-fg}🚀 Claude Starter{/}';
341
400
  const count = `{#9ece6a-fg}${filteredSessions.length}{/}{#565f89-fg}/${allSessions.length} sessions{/}`;
342
401
  const proj = `{#bb9af7-fg}${uniqueProjects.length}{/}{#565f89-fg} projects{/}`;
402
+ const favCount = allSessions.filter(s => getSessionMeta(meta, s.sessionId).favorite).length;
403
+ const fav = favCount > 0 ? `{#e0af68-fg}⭐${favCount}{/}` : '';
343
404
  const sort = `{#73daca-fg}↕${sortMode}{/}`;
344
405
  const search = isSearchMode
345
406
  ? `{#e0af68-fg}/ ${filterText}▌{/}`
346
407
  : (filterText ? `{#e0af68-fg}/ ${filterText}{/}` : '');
347
- header.setContent(`\n ${title} {#414868-fg}│{/} ${count} {#414868-fg}│{/} ${proj} {#414868-fg}│{/} ${sort}${search ? ` {#414868-fg}│{/} ${search}` : ''}`);
408
+ let parts = [title, count, proj];
409
+ if (fav) parts.push(fav);
410
+ parts.push(sort);
411
+ if (search) parts.push(search);
412
+ header.setContent(`\n ${parts.join(' {#414868-fg}│{/} ')}`);
348
413
  }
349
414
 
350
415
  blessed.line({ parent: screen, top: 3, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868' } });
@@ -393,7 +458,8 @@ function createApp() {
393
458
  '{#7aa2f7-fg}{bold}↵{/} {#565f89-fg}Start/Resume{/}',
394
459
  '{#7aa2f7-fg}{bold}n{/} {#565f89-fg}New{/}',
395
460
  '{#7aa2f7-fg}{bold}/{/} {#565f89-fg}Search{/}',
396
- '{#7aa2f7-fg}{bold}↑/↓{/} {#565f89-fg}Nav{/}',
461
+ '{#7aa2f7-fg}{bold}f{/} {#565f89-fg}Fav{/}',
462
+ '{#7aa2f7-fg}{bold}#{/} {#565f89-fg}Tag{/}',
397
463
  '{#7aa2f7-fg}{bold}p{/} {#565f89-fg}Project{/}',
398
464
  '{#7aa2f7-fg}{bold}s{/} {#565f89-fg}Sort{/}',
399
465
  '{#7aa2f7-fg}{bold}c{/} {#565f89-fg}Copy ID{/}',
@@ -448,16 +514,37 @@ function createApp() {
448
514
 
449
515
  const sessionItems = filteredSessions.map((session) => {
450
516
  const color = getProjectColor(session.project, projectColorMap);
451
- const proj = `{${color}-fg}${session.project.substring(0, 13).padEnd(13)}{/}`;
517
+ const sm = getSessionMeta(meta, session.sessionId);
518
+ const favIcon = sm.favorite ? '{#e0af68-fg}⭐{/}' : ' ';
519
+ const proj = `{${color}-fg}${session.project.substring(0, 12).padEnd(12)}{/}`;
452
520
  const time = `{#e0af68-fg}${formatTimestamp(session.lastTs).padEnd(16)}{/}`;
453
521
  const msgs = `{#7aa2f7-fg}${String(session.estimatedMessages).padStart(4)}{/}{#565f89-fg}m{/}`;
454
522
 
455
- const fixedLen = 13 + 1 + 16 + 1 + 5 + 2 + 3;
456
- const topicMaxLen = Math.max(15, listW - fixedLen);
457
- let topic = session.topic;
458
- if (topic.length > topicMaxLen) topic = topic.substring(0, topicMaxLen) + '…';
523
+ const fixedLen = 2 + 12 + 1 + 16 + 1 + 5 + 2 + 3;
524
+ const topicMaxLen = Math.max(10, listW - fixedLen);
525
+ let topic = session.customTitle || session.topic;
526
+
527
+ // Append tags inline after topic
528
+ const tagStr = sm.tags.length > 0
529
+ ? ' ' + sm.tags.map(t => `#${t}`).join(' ')
530
+ : '';
531
+
532
+ let display = topic + tagStr;
533
+ if (display.length > topicMaxLen) display = display.substring(0, topicMaxLen) + '…';
534
+
535
+ // Split display back into topic part and tag part for coloring
536
+ const topicPart = display.substring(0, Math.min(topic.length, topicMaxLen));
537
+ const tagPart = display.substring(topicPart.length);
459
538
 
460
- return ` ${proj} ${time} ${msgs} {#a9b1d6-fg}${esc(topic)}{/}`;
539
+ let label = `${favIcon}${proj} ${time} ${msgs} `;
540
+ if (session.customTitle) {
541
+ label += `{#73daca-fg}{bold}${esc(topicPart)}{/}`;
542
+ } else {
543
+ label += `{#a9b1d6-fg}${esc(topicPart)}{/}`;
544
+ }
545
+ if (tagPart) label += `{#f7768e-fg}${esc(tagPart)}{/}`;
546
+
547
+ return label;
461
548
  });
462
549
 
463
550
  const items = [NEW_SESSION_LABEL, ...sessionItems];
@@ -495,10 +582,16 @@ function createApp() {
495
582
  loadSessionDetail(session);
496
583
 
497
584
  const color = getProjectColor(session.project, projectColorMap);
585
+ const sm = getSessionMeta(meta, session.sessionId);
498
586
  let c = '';
499
587
  const sep = ` {#414868-fg}${'─'.repeat(44)}{/}`;
500
588
 
501
- c += `\n {${color}-fg}{bold}█ ${session.project}{/}\n`;
589
+ // Title with favorite indicator
590
+ const favLabel = sm.favorite ? ' {#e0af68-fg}⭐{/}' : '';
591
+ c += `\n {${color}-fg}{bold}█ ${session.project}{/}${favLabel}\n`;
592
+ if (session.customTitle) {
593
+ c += ` {#73daca-fg}{bold}📌 ${esc(session.customTitle)}{/}\n`;
594
+ }
502
595
  c += sep + '\n\n';
503
596
 
504
597
  const fields = [
@@ -517,6 +610,13 @@ function createApp() {
517
610
  c += ` {#565f89-fg}${label.padEnd(12)}{/} ${value}\n`;
518
611
  }
519
612
 
613
+ // Tags section
614
+ if (sm.tags.length > 0) {
615
+ const tagChips = sm.tags.map(t => `{#414868-fg}[{/}{#f7768e-fg}#${t}{/}{#414868-fg}]{/}`).join(' ');
616
+ c += `\n {#f7768e-fg}{bold}🏷️ Tags{/}\n`;
617
+ c += ` ${tagChips}\n`;
618
+ }
619
+
520
620
  if (session.toolsUsed && session.toolsUsed.length > 0) {
521
621
  c += `\n {#7dcfff-fg}{bold}Tools Used{/}\n`;
522
622
  const chips = session.toolsUsed.slice(0, 10).map(t => `{#414868-fg}[{/}{#7dcfff-fg}${t}{/}{#414868-fg}]{/}`).join(' ');
@@ -570,8 +670,21 @@ function createApp() {
570
670
  } else {
571
671
  const terms = filterText.toLowerCase().split(/\s+/);
572
672
  filteredSessions = allSessions.filter(s => {
573
- const haystack = [s.project, s.topic, s.gitBranch || '', s.sessionId, ...(s.userMessages || [])].join(' ').toLowerCase();
574
- return terms.every(t => haystack.includes(t));
673
+ const sm = getSessionMeta(meta, s.sessionId);
674
+ const haystack = [s.project, s.topic, s.customTitle || '', s.gitBranch || '', s.sessionId, ...(s.userMessages || [])].join(' ').toLowerCase();
675
+
676
+ 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
+ return haystack.includes(t);
687
+ });
575
688
  });
576
689
  }
577
690
  selectedIndex = Math.min(selectedIndex, Math.max(-1, filteredSessions.length - 1));
@@ -585,13 +698,19 @@ function createApp() {
585
698
 
586
699
  // ─── Sort ──────────────────────────────────────────────────────────────
587
700
  function cycleSort() {
588
- const modes = ['time', 'size', 'messages', 'project'];
701
+ const modes = ['time', 'size', 'messages', 'project', 'favorites'];
589
702
  sortMode = modes[(modes.indexOf(sortMode) + 1) % modes.length];
590
703
  const sorters = {
591
704
  time: (a, b) => (new Date(b.lastTs || 0).getTime()) - (new Date(a.lastTs || 0).getTime()),
592
705
  size: (a, b) => b.fileSize - a.fileSize,
593
706
  messages: (a, b) => b.estimatedMessages - a.estimatedMessages,
594
707
  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
+ },
595
714
  };
596
715
  allSessions.sort(sorters[sortMode]);
597
716
  selectedIndex = 0;
@@ -670,14 +789,17 @@ function createApp() {
670
789
  }
671
790
 
672
791
  screen.key(['down'], () => {
792
+ if (popupOpen) return;
673
793
  if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
674
794
  moveSelection(1);
675
795
  });
676
796
  screen.key(['up'], () => {
797
+ if (popupOpen) return;
677
798
  if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
678
799
  moveSelection(-1);
679
800
  });
680
801
  screen.key(['home'], () => {
802
+ if (popupOpen) return;
681
803
  if (isSearchMode) { isSearchMode = false; }
682
804
  selectedIndex = -1;
683
805
  suppressSelectEvent = true; listPanel.select(0); suppressSelectEvent = false;
@@ -685,6 +807,7 @@ function createApp() {
685
807
  renderDetail(); updateHeader(); screen.render();
686
808
  });
687
809
  screen.key(['end'], () => {
810
+ if (popupOpen) return;
688
811
  if (isSearchMode) { isSearchMode = false; }
689
812
  selectedIndex = Math.max(0, filteredSessions.length - 1);
690
813
  suppressSelectEvent = true; listPanel.select(selectedIndex + 1); suppressSelectEvent = false;
@@ -692,10 +815,12 @@ function createApp() {
692
815
  renderDetail(); updateHeader(); screen.render();
693
816
  });
694
817
  screen.key(['pagedown', 'C-d'], () => {
818
+ if (popupOpen) return;
695
819
  if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
696
820
  moveSelection(Math.floor((listPanel.height || 20) / 2));
697
821
  });
698
822
  screen.key(['pageup', 'C-u'], () => {
823
+ if (popupOpen) return;
699
824
  if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
700
825
  moveSelection(-Math.floor((listPanel.height || 20) / 2));
701
826
  });
@@ -789,6 +914,136 @@ function createApp() {
789
914
  } catch (e) { /* silently fail */ }
790
915
  });
791
916
 
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
+
929
+ // Tag management — handled via keypress since '#' is a shifted character
930
+ // that some terminal/blessed combos may not route through screen.key
931
+ screen.on('keypress', (ch, key) => {
932
+ if (ch === '#' && !isSearchMode && !popupOpen) {
933
+ if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
934
+ showTagPicker(filteredSessions[selectedIndex]);
935
+ }
936
+ });
937
+
938
+ function showTagPicker(session) {
939
+ const sm = getSessionMeta(meta, session.sessionId);
940
+ const currentTags = new Set(sm.tags);
941
+
942
+ // Build tag list: all known tags (defaults + used), with checkmarks for active ones
943
+ const usedTags = getAllUsedTags(meta);
944
+ const allTags = [...new Set([...DEFAULT_TAGS, ...usedTags])].sort();
945
+
946
+ const items = [
947
+ ' {#9ece6a-fg}{bold}+ New custom tag…{/}',
948
+ ...allTags.map(t => {
949
+ const checked = currentTags.has(t) ? '{#9ece6a-fg}✓{/}' : ' ';
950
+ return ` ${checked} {#f7768e-fg}#${t}{/}`;
951
+ }),
952
+ ];
953
+
954
+ const popup = blessed.list({
955
+ parent: screen, top: 'center', left: 'center',
956
+ width: Math.min(45, Math.max(...items.map(i => i.replace(/\{[^}]*\}/g, '').length)) + 8),
957
+ height: Math.min(items.length + 4, 20),
958
+ label: ' {bold}{#f7768e-fg}🏷️ Tags{/} ',
959
+ tags: true, border: { type: 'line' },
960
+ style: {
961
+ border: { fg: '#f7768e' }, bg: '#24283b', fg: '#a9b1d6',
962
+ selected: { bg: '#3d59a1', fg: 'white', bold: true },
963
+ label: { fg: '#f7768e' },
964
+ },
965
+ items: items, keys: true, vi: true, mouse: true,
966
+ });
967
+ popupOpen = true;
968
+ popup.focus(); screen.render();
969
+
970
+ popup.on('select', (item, index) => {
971
+ if (index === 0) {
972
+ // New custom tag — show input
973
+ popup.destroy();
974
+ popupOpen = false;
975
+ showTagInput(session);
976
+ return;
977
+ }
978
+ // Toggle the selected tag
979
+ const tagName = allTags[index - 1];
980
+ if (currentTags.has(tagName)) {
981
+ currentTags.delete(tagName);
982
+ } else {
983
+ currentTags.add(tagName);
984
+ }
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
+ });
999
+
1000
+ popup.key(['escape', 'q'], () => {
1001
+ popup.destroy();
1002
+ popupOpen = false;
1003
+ renderAll();
1004
+ });
1005
+ }
1006
+
1007
+ function showTagInput(session) {
1008
+ const inputBox = blessed.textbox({
1009
+ parent: screen, top: 'center', left: 'center',
1010
+ width: 40, height: 3,
1011
+ label: ' {bold}{#f7768e-fg}New Tag{/} ',
1012
+ tags: true, border: { type: 'line' },
1013
+ style: {
1014
+ border: { fg: '#f7768e' }, bg: '#24283b', fg: '#a9b1d6',
1015
+ label: { fg: '#f7768e' },
1016
+ },
1017
+ inputOnFocus: true,
1018
+ });
1019
+ popupOpen = true;
1020
+ inputBox.focus();
1021
+ screen.render();
1022
+
1023
+ inputBox.on('submit', (value) => {
1024
+ inputBox.destroy();
1025
+ popupOpen = false;
1026
+ const tagName = value.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
1027
+ if (tagName) {
1028
+ const sm = getSessionMeta(meta, session.sessionId);
1029
+ const tags = new Set(sm.tags);
1030
+ tags.add(tagName);
1031
+ setSessionTags(meta, session.sessionId, [...tags]);
1032
+ footer.setContent(`\n {#9ece6a-fg}{bold}✓ Tagged:{/} {#f7768e-fg}#${tagName}{/}`);
1033
+ renderAll();
1034
+ setTimeout(() => { updateFooter(); screen.render(); }, 1500);
1035
+ } else {
1036
+ renderAll();
1037
+ }
1038
+ });
1039
+
1040
+ inputBox.on('cancel', () => {
1041
+ inputBox.destroy();
1042
+ popupOpen = false;
1043
+ renderAll();
1044
+ });
1045
+ }
1046
+
792
1047
  screen.key(['s'], () => { if (!isSearchMode) cycleSort(); });
793
1048
  screen.key(['p'], () => { if (!isSearchMode) showProjectPicker(); });
794
1049
  screen.key(['escape'], () => {
@@ -859,9 +1114,11 @@ TUI Keyboard Shortcuts:
859
1114
  ↑/↓ Navigate sessions
860
1115
  Enter Start new / resume selected session
861
1116
  n Start new session
862
- / Search (fuzzy filter)
1117
+ / Search (fuzzy filter, supports #tag and fav)
1118
+ f Toggle favorite ⭐ on selected session
1119
+ # Add/remove tags on selected session
863
1120
  p Filter by project
864
- s Cycle sort mode
1121
+ s Cycle sort mode (time/size/messages/project/favorites)
865
1122
  c Copy session ID
866
1123
  Home / End Jump to top / bottom
867
1124
  Ctrl-D/U Page down / up
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-starter",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A beautiful terminal UI for managing Claude Code sessions — start new or resume past conversations",
5
5
  "main": "index.js",
6
6
  "bin": {