@kud/claude-sessions-cli 1.0.3 → 2.0.0-next.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 (2) hide show
  1. package/dist/index.js +897 -204
  2. package/package.json +8 -6
package/dist/index.js CHANGED
@@ -1,23 +1,49 @@
1
1
  #!/usr/bin/env node
2
+ #!/usr/bin/env node
2
3
 
3
4
  // src/index.tsx
4
- import React, { useState, useEffect } from "react";
5
- import { render, Box, Text, useInput, useApp } from "ink";
5
+ import { useState, useEffect, useMemo } from "react";
6
+ import { render, Box, Text, useInput, useApp, useStdout } from "ink";
6
7
  import TextInput from "ink-text-input";
7
8
  import { readdir, stat } from "fs/promises";
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ readdirSync,
13
+ readFileSync,
14
+ writeFileSync
15
+ } from "fs";
9
16
  import { join } from "path";
10
17
  import { spawnSync, execSync } from "child_process";
11
18
  import { homedir } from "os";
12
- import { jsx, jsxs } from "react/jsx-runtime";
19
+ import { randomUUID } from "crypto";
20
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
21
  var HOME = homedir();
14
22
  var CLAUDE_PROJECTS = join(HOME, ".claude", "projects");
15
23
  var CLAUDE_JSON = join(HOME, ".claude.json");
16
- var CHATS_DIR = join(HOME, ".chats");
24
+ var CLAUDE_SESSIONS_DIR = join(HOME, ".claude-sessions");
25
+ var CHATS_DIR = join(CLAUDE_SESSIONS_DIR, "chats");
26
+ var SESSION_LABELS_FILE = join(CLAUDE_SESSIONS_DIR, "session-labels.json");
27
+ var SESSION_PINS_FILE = join(CLAUDE_SESSIONS_DIR, "session-pins.json");
28
+ var SESSION_TAGS_FILE = join(CLAUDE_SESSIONS_DIR, "session-tags.json");
17
29
  var ICON_CHAT = "\u{F0B79}";
18
30
  var ICON_CODE = "\uF121";
31
+ var ICON_SCHEDULE = "\u{F0954}";
32
+ var SEL_COLOR = "#FF8C00";
33
+ var TABS = ["code", "chat", "schedule"];
34
+ var TAB_LABEL = {
35
+ code: "Code",
36
+ chat: "Chat",
37
+ schedule: "Scheduled"
38
+ };
39
+ var TAB_ICON = {
40
+ code: ICON_CODE,
41
+ chat: ICON_CHAT,
42
+ schedule: ICON_SCHEDULE
43
+ };
44
+ var sessionPreload = null;
19
45
  var pendingAction = null;
20
- var slugify = (name) => name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
46
+ var savedState = { tab: "code", cursor: 0 };
21
47
  var humanLabel = (dir) => {
22
48
  const base = dir.split("/").pop() || dir;
23
49
  const words = base.replace(/^[._-]+/, "").replace(/[-_]+/g, " ").trim();
@@ -50,6 +76,70 @@ var removeFromClaudeJson = (dir) => {
50
76
  }
51
77
  };
52
78
  var toProjectDirName = (absPath) => absPath.replace(/[^a-zA-Z0-9]/g, "-");
79
+ var readFirstPrompt = (filePath) => {
80
+ try {
81
+ const content = readFileSync(filePath, "utf8");
82
+ const lines = content.split("\n");
83
+ let count = 0;
84
+ for (const line of lines) {
85
+ if (!line.trim()) continue;
86
+ if (++count > 200) break;
87
+ try {
88
+ const obj = JSON.parse(line);
89
+ if (obj.type === "user" && typeof obj.message?.content === "string" && !obj.message.content.startsWith("<") && !obj.message.content.includes("tool_use_id")) {
90
+ return obj.message.content.trim().replace(/\s+/g, " ").slice(0, 200);
91
+ }
92
+ } catch {
93
+ }
94
+ }
95
+ } catch {
96
+ }
97
+ return "";
98
+ };
99
+ var loadSessionLabels = () => {
100
+ try {
101
+ return JSON.parse(readFileSync(SESSION_LABELS_FILE, "utf8"));
102
+ } catch {
103
+ return {};
104
+ }
105
+ };
106
+ var saveSessionLabel = (key, label) => {
107
+ const labels = loadSessionLabels();
108
+ labels[key] = label;
109
+ writeFileSync(SESSION_LABELS_FILE, JSON.stringify(labels, null, 2));
110
+ };
111
+ var removeSessionLabel = (key) => {
112
+ const labels = loadSessionLabels();
113
+ if (!(key in labels)) return;
114
+ delete labels[key];
115
+ writeFileSync(SESSION_LABELS_FILE, JSON.stringify(labels, null, 2));
116
+ };
117
+ var loadSessionPins = () => {
118
+ try {
119
+ return new Set(JSON.parse(readFileSync(SESSION_PINS_FILE, "utf8")));
120
+ } catch {
121
+ return /* @__PURE__ */ new Set();
122
+ }
123
+ };
124
+ var toggleSessionPin = (dir) => {
125
+ const pins = loadSessionPins();
126
+ if (pins.has(dir)) pins.delete(dir);
127
+ else pins.add(dir);
128
+ writeFileSync(SESSION_PINS_FILE, JSON.stringify([...pins], null, 2));
129
+ };
130
+ var loadSessionTags = () => {
131
+ try {
132
+ return JSON.parse(readFileSync(SESSION_TAGS_FILE, "utf8"));
133
+ } catch {
134
+ return {};
135
+ }
136
+ };
137
+ var saveSessionTag = (dir, tag) => {
138
+ const tags = loadSessionTags();
139
+ if (tag.trim()) tags[dir] = tag.trim();
140
+ else delete tags[dir];
141
+ writeFileSync(SESSION_TAGS_FILE, JSON.stringify(tags, null, 2));
142
+ };
53
143
  var loadSessions = async () => {
54
144
  const sessions = [];
55
145
  if (existsSync(CLAUDE_JSON)) {
@@ -69,24 +159,32 @@ var loadSessions = async () => {
69
159
  (f) => f.endsWith(".jsonl")
70
160
  );
71
161
  if (!jsonlFiles.length) return;
72
- const mtime = Math.max(
73
- ...await Promise.all(
74
- jsonlFiles.map(
75
- (f) => stat(join(claudeProjectDir, f)).then((s) => s.mtimeMs / 1e3)
76
- )
77
- )
78
- );
79
162
  const shortPath = cwd.replace(HOME, "~");
80
163
  const type = cwd === HOME || cwd.startsWith(CHATS_DIR) ? "chat" : "code";
81
- sessions.push({
82
- dir: cwd,
83
- label: type === "chat" ? humanLabel(cwd) : kebabLabel(cwd),
84
- path: shortPath,
85
- type,
86
- mtime,
87
- ago: timeAgo(mtime),
88
- claudeProjectDir
89
- });
164
+ const projectLabel = type === "chat" ? humanLabel(cwd) : kebabLabel(cwd);
165
+ await Promise.all(
166
+ jsonlFiles.map(async (f) => {
167
+ try {
168
+ const jsonlPath = join(claudeProjectDir, f);
169
+ const mtime = (await stat(jsonlPath)).mtimeMs / 1e3;
170
+ const sessionId = f.replace(".jsonl", "");
171
+ const firstPrompt = type === "code" ? readFirstPrompt(jsonlPath) : "";
172
+ sessions.push({
173
+ dir: cwd,
174
+ label: firstPrompt || projectLabel,
175
+ path: shortPath,
176
+ type,
177
+ mtime,
178
+ ago: timeAgo(mtime),
179
+ claudeProjectDir,
180
+ sessionId,
181
+ projectLabel,
182
+ hasClaudeMd: type === "chat" && existsSync(join(cwd, "CLAUDE.md"))
183
+ });
184
+ } catch {
185
+ }
186
+ })
187
+ );
90
188
  } catch {
91
189
  }
92
190
  })
@@ -94,23 +192,25 @@ var loadSessions = async () => {
94
192
  } catch {
95
193
  }
96
194
  }
97
- const seen = new Set(sessions.map((s) => s.dir));
98
195
  if (existsSync(CHATS_DIR)) {
99
196
  try {
197
+ const existingDirs = new Set(sessions.map((s) => s.dir));
100
198
  for (const dir of await readdir(CHATS_DIR)) {
101
199
  try {
102
200
  const fullPath = join(CHATS_DIR, dir);
103
- if (!(await stat(fullPath)).isDirectory() || seen.has(fullPath))
104
- continue;
105
- const shortPath = fullPath.replace(HOME, "~");
201
+ const dirStat = await stat(fullPath);
202
+ if (!dirStat.isDirectory() || existingDirs.has(fullPath)) continue;
203
+ const mtime = dirStat.mtimeMs / 1e3;
106
204
  sessions.push({
107
205
  dir: fullPath,
108
206
  label: humanLabel(fullPath),
109
- path: shortPath,
207
+ path: fullPath.replace(HOME, "~"),
110
208
  type: "chat",
111
- mtime: 0,
112
- ago: "new",
113
- claudeProjectDir: ""
209
+ mtime,
210
+ ago: timeAgo(mtime),
211
+ claudeProjectDir: "",
212
+ projectLabel: humanLabel(fullPath),
213
+ hasClaudeMd: existsSync(join(fullPath, "CLAUDE.md"))
114
214
  });
115
215
  } catch {
116
216
  }
@@ -118,8 +218,20 @@ var loadSessions = async () => {
118
218
  } catch {
119
219
  }
120
220
  }
121
- const unique = [...new Map(sessions.map((s) => [s.dir, s])).values()];
122
- return unique.sort((a, b) => b.mtime - a.mtime);
221
+ const labelOverrides = loadSessionLabels();
222
+ for (const s of sessions) {
223
+ const override = s.sessionId && labelOverrides[s.sessionId] || labelOverrides[s.dir];
224
+ if (override) s.label = override;
225
+ }
226
+ const pins = loadSessionPins();
227
+ const tagOverrides = loadSessionTags();
228
+ for (const s of sessions) {
229
+ if (s.type === "chat") {
230
+ s.pinned = pins.has(s.dir);
231
+ s.tag = tagOverrides[s.dir];
232
+ }
233
+ }
234
+ return sessions.sort((a, b) => b.mtime - a.mtime);
123
235
  };
124
236
  var findCleanItems = async () => {
125
237
  const items = [];
@@ -187,44 +299,122 @@ var findCleanItems = async () => {
187
299
  }
188
300
  return items;
189
301
  };
190
- var deleteSession = (item) => {
191
- if (item.claudeProjectDir) {
302
+ var deleteSession = (session) => {
303
+ if (session.sessionId) {
304
+ const jsonlPath = join(
305
+ session.claudeProjectDir,
306
+ `${session.sessionId}.jsonl`
307
+ );
192
308
  try {
193
- execSync(`trash "${item.claudeProjectDir}"`);
309
+ execSync(`trash "${jsonlPath}"`);
194
310
  } catch {
195
311
  }
196
- removeFromClaudeJson(item.dir);
197
- }
198
- if (item.type === "chat" && item.dir !== HOME) {
312
+ } else if (session.type === "chat") {
199
313
  try {
200
- execSync(`trash "${item.dir}"`);
314
+ execSync(`trash "${session.dir}"`);
315
+ removeSessionLabel(session.dir);
201
316
  } catch {
202
317
  }
203
318
  }
204
319
  };
205
- var SectionHeader = ({ label }) => {
206
- return /* @__PURE__ */ jsx(Box, { paddingX: 2, marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
207
- "\u2500\u2500 ",
208
- label,
209
- " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
210
- ] }) });
320
+ var buildDisplayItems = (tab, sessions, search, expandedProjects, expandedTags) => {
321
+ const match = (s) => !search || s.label.toLowerCase().includes(search.toLowerCase()) || s.path.toLowerCase().includes(search.toLowerCase());
322
+ if (tab === "chat") {
323
+ const filtered2 = sessions.filter((s) => s.type === "chat").filter(match);
324
+ const pinned = filtered2.filter((s) => s.pinned);
325
+ const tagged = filtered2.filter((s) => !s.pinned && s.tag);
326
+ const untagged = filtered2.filter((s) => !s.pinned && !s.tag);
327
+ const items2 = [{ kind: "new" }];
328
+ for (const s of pinned) items2.push({ kind: "session", session: s });
329
+ const tagGroups = /* @__PURE__ */ new Map();
330
+ for (const s of tagged) {
331
+ if (!tagGroups.has(s.tag)) tagGroups.set(s.tag, []);
332
+ tagGroups.get(s.tag).push(s);
333
+ }
334
+ for (const [tag, group] of tagGroups) {
335
+ const expanded = expandedTags.has(tag);
336
+ items2.push({
337
+ kind: "tag-header",
338
+ label: tag,
339
+ expanded,
340
+ count: group.length
341
+ });
342
+ if (expanded)
343
+ for (const s of group) items2.push({ kind: "session", session: s });
344
+ }
345
+ for (const s of untagged) items2.push({ kind: "session", session: s });
346
+ return items2;
347
+ }
348
+ if (tab === "schedule") {
349
+ return [];
350
+ }
351
+ const filtered = sessions.filter((s) => s.type === "code").filter(match);
352
+ const groups = /* @__PURE__ */ new Map();
353
+ for (const s of filtered) {
354
+ if (!groups.has(s.dir)) groups.set(s.dir, []);
355
+ groups.get(s.dir).push(s);
356
+ }
357
+ const items = [];
358
+ for (const [dir, group] of groups) {
359
+ const expanded = expandedProjects.has(dir);
360
+ items.push({
361
+ kind: "header",
362
+ label: group[0].projectLabel ?? group[0].path,
363
+ dir,
364
+ expanded,
365
+ count: group.length,
366
+ recentSession: group[0]
367
+ });
368
+ if (expanded) {
369
+ for (const s of group) items.push({ kind: "session", session: s });
370
+ }
371
+ }
372
+ return items;
373
+ };
374
+ var contextHints = (item) => {
375
+ const nav = [
376
+ ["\u2191\u2193", "nav"],
377
+ ["\u2190\u2192", "tab"]
378
+ ];
379
+ if (item?.kind === "new")
380
+ return [...nav, ["enter", "new chat"], ["/", "search"], ["q", "quit"]];
381
+ if (item?.kind === "header")
382
+ return [
383
+ ...nav,
384
+ ["enter", "open"],
385
+ ["space", item.expanded ? "collapse" : "expand"],
386
+ ["d", "delete all"],
387
+ ["q", "quit"]
388
+ ];
389
+ if (item?.kind === "tag-header")
390
+ return [
391
+ ...nav,
392
+ ["space", item.expanded ? "collapse" : "expand"],
393
+ ["q", "quit"]
394
+ ];
395
+ if (item?.kind === "session") {
396
+ const s = item.session;
397
+ const pairs = [
398
+ ...nav,
399
+ ["enter", "open"],
400
+ ["d", "delete"]
401
+ ];
402
+ if (s.type === "chat") {
403
+ pairs.push(["p", s.pinned ? "unpin" : "pin"]);
404
+ pairs.push(["t", "tag"]);
405
+ } else if (s.sessionId) {
406
+ pairs.push(["r", "rename"]);
407
+ }
408
+ if (s.hasClaudeMd) pairs.push(["m", "md"]);
409
+ pairs.push(["q", "quit"]);
410
+ return pairs;
411
+ }
412
+ return [...nav, ["/", "search"], ["q", "quit"]];
211
413
  };
212
414
  var Hint = ({ pairs }) => /* @__PURE__ */ jsx(Box, { gap: 2, children: pairs.map(([key, desc]) => /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
213
415
  /* @__PURE__ */ jsx(Text, { color: "white", children: key }),
214
416
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: desc })
215
417
  ] }, key)) });
216
- var FilterBar = ({ filter }) => {
217
- const opts = ["all", "chat", "code"];
218
- return /* @__PURE__ */ jsx(Box, { gap: 1, children: opts.map((o) => /* @__PURE__ */ jsx(
219
- Text,
220
- {
221
- color: filter === o ? "cyan" : "gray",
222
- bold: filter === o,
223
- children: filter === o ? `[${o}]` : o
224
- },
225
- o
226
- )) });
227
- };
228
418
  var CleanConfirm = ({
229
419
  items,
230
420
  onConfirm,
@@ -290,7 +480,7 @@ var CleanConfirm = ({
290
480
  const color = REASON_COLOR[reason] ?? "white";
291
481
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: i > 0 ? 1 : 0, children: [
292
482
  /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
293
- /* @__PURE__ */ jsx(Text, { color: sel ? "cyan" : "gray", children: sel ? "\u25B6" : " " }),
483
+ /* @__PURE__ */ jsx(Text, { color: sel ? "cyan" : "gray", children: sel ? "\u203A" : " " }),
294
484
  /* @__PURE__ */ jsx(Text, { color: checked ? color : "gray", children: checked ? "[x]" : "[ ]" }),
295
485
  /* @__PURE__ */ jsx(Text, { color: checked ? color : "gray", bold: checked, children: reason }),
296
486
  /* @__PURE__ */ jsx(Text, { color: checked ? color : "gray", dimColor: true, children: countByReason[reason] })
@@ -338,51 +528,186 @@ var CleanApp = () => {
338
528
  };
339
529
  var App = () => {
340
530
  const { exit } = useApp();
531
+ const { stdout } = useStdout();
341
532
  const [sessions, setSessions] = useState(null);
342
- const [cursor, setCursor] = useState(0);
533
+ const [loadingIndex, setLoadingIndex] = useState(0);
534
+ const [tab, setTab] = useState(savedState.tab);
535
+ const [cursor, setCursor] = useState(savedState.cursor);
343
536
  const [mode, setMode] = useState("list");
344
537
  const [newName, setNewName] = useState("");
345
- const [filter, setFilter] = useState("all");
538
+ const [renameValue, setRenameValue] = useState("");
346
539
  const [search, setSearch] = useState("");
347
540
  const [cleanItems, setCleanItems] = useState(null);
541
+ const [deleteAllTarget, setDeleteAllTarget] = useState(null);
542
+ const [previewContent, setPreviewContent] = useState([]);
543
+ const [previewScroll, setPreviewScroll] = useState(0);
544
+ const [expandedProjects, setExpandedProjects] = useState(
545
+ /* @__PURE__ */ new Set()
546
+ );
547
+ const [expandedTags, setExpandedTags] = useState(/* @__PURE__ */ new Set());
548
+ const [tagValue, setTagValue] = useState("");
549
+ const [scrollOffset, setScrollOffset] = useState(0);
550
+ const LOADING_MESSAGES = [
551
+ "Summoning your sessions\u2026",
552
+ "Reading the scrolls\u2026",
553
+ "Warming up the neurons\u2026",
554
+ "Herding your sessions\u2026",
555
+ "Consulting the oracle\u2026",
556
+ "Charging up\u2026",
557
+ "Loading memories\u2026",
558
+ "Almost there\u2026"
559
+ ];
348
560
  useEffect(() => {
349
- loadSessions().then(setSessions);
561
+ const p = sessionPreload ?? loadSessions();
562
+ sessionPreload = null;
563
+ p.then(setSessions);
350
564
  }, []);
351
- const chatSessions = sessions?.filter((s) => s.type === "chat") ?? [];
352
- const codeSessions = sessions?.filter((s) => s.type === "code") ?? [];
353
- const grouped = filter === "chat" ? chatSessions : filter === "code" ? codeSessions : [...chatSessions, ...codeSessions];
354
- const filtered = search ? grouped.filter(
355
- (s) => s.label.toLowerCase().includes(search.toLowerCase()) || s.path.toLowerCase().includes(search.toLowerCase())
356
- ) : grouped;
357
- const items = sessions ? [null, ...filtered] : [];
358
- const doExit = (action) => {
359
- pendingAction = action;
565
+ useEffect(() => {
566
+ if (sessions) return;
567
+ const id = setInterval(
568
+ () => setLoadingIndex((i) => (i + 1) % LOADING_MESSAGES.length),
569
+ 600
570
+ );
571
+ return () => clearInterval(id);
572
+ }, [sessions]);
573
+ const CHROME_ROWS = 10;
574
+ const listHeight = Math.max(1, (stdout.rows ?? 24) - CHROME_ROWS);
575
+ const displayItems = useMemo(
576
+ () => sessions ? buildDisplayItems(
577
+ tab,
578
+ sessions,
579
+ search,
580
+ expandedProjects,
581
+ expandedTags
582
+ ) : [],
583
+ [sessions, tab, search, expandedProjects, expandedTags]
584
+ );
585
+ useEffect(() => {
586
+ if (cursor < scrollOffset) setScrollOffset(cursor);
587
+ else if (cursor >= scrollOffset + listHeight)
588
+ setScrollOffset(cursor - listHeight + 1);
589
+ }, [cursor, listHeight]);
590
+ useEffect(() => {
591
+ setScrollOffset(0);
592
+ }, [tab, search]);
593
+ const moveCursor = (dir) => setCursor((c) => Math.max(0, Math.min(displayItems.length - 1, c + dir)));
594
+ const toggleExpand = (dir) => setExpandedProjects((prev) => {
595
+ if (prev.has(dir)) return /* @__PURE__ */ new Set();
596
+ return /* @__PURE__ */ new Set([dir]);
597
+ });
598
+ const toggleExpandTag = (tag) => setExpandedTags((prev) => {
599
+ if (prev.has(tag)) {
600
+ const n = new Set(prev);
601
+ n.delete(tag);
602
+ return n;
603
+ }
604
+ return /* @__PURE__ */ new Set([...prev, tag]);
605
+ });
606
+ const cycleTab = (dir) => {
607
+ const next = TABS[(TABS.indexOf(tab) + dir + TABS.length) % TABS.length];
608
+ setTab(next);
609
+ setSearch("");
610
+ };
611
+ const doOpen = (session) => {
612
+ savedState = { tab, cursor };
613
+ const isChat = session.type === "chat";
614
+ pendingAction = {
615
+ type: isChat ? session.claudeProjectDir ? "open" : "new" : session.sessionId ? "resume" : session.claudeProjectDir ? "open" : "new",
616
+ dir: session.dir,
617
+ sessionId: !isChat ? session.sessionId : void 0
618
+ };
360
619
  exit();
361
620
  };
362
- const cycleFilter = () => {
363
- setFilter((f) => f === "all" ? "chat" : f === "chat" ? "code" : "all");
621
+ useEffect(() => {
364
622
  setCursor(0);
365
- };
623
+ }, [tab]);
366
624
  useInput(
367
625
  (input, key) => {
368
- if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
369
- if (key.downArrow) setCursor((c) => Math.min(items.length - 1, c + 1));
370
- if (key.tab) cycleFilter();
626
+ if (key.upArrow) moveCursor(-1);
627
+ if (key.downArrow) moveCursor(1);
628
+ if (key.leftArrow) cycleTab(-1);
629
+ if (key.rightArrow) cycleTab(1);
630
+ if (key.tab) cycleTab(1);
371
631
  if (input === "/") {
372
632
  setMode("search");
373
633
  setSearch("");
374
634
  setCursor(0);
375
635
  }
376
636
  if (key.return) {
377
- if (cursor === 0) {
637
+ const item = displayItems[cursor];
638
+ if (!item) return;
639
+ if (item.kind === "new") {
378
640
  setMode("new");
379
641
  setNewName("");
380
- } else if (items[cursor]) {
381
- const s = items[cursor];
382
- doExit({ type: s.claudeProjectDir ? "open" : "new", dir: s.dir });
642
+ } else if (item.kind === "header") {
643
+ doOpen(item.recentSession);
644
+ } else if (item.kind === "session") {
645
+ doOpen(item.session);
646
+ }
647
+ }
648
+ if (input === " ") {
649
+ const item = displayItems[cursor];
650
+ if (item?.kind === "header") toggleExpand(item.dir);
651
+ if (item?.kind === "tag-header") toggleExpandTag(item.label);
652
+ }
653
+ if (input === "d") {
654
+ const item = displayItems[cursor];
655
+ if (item?.kind === "session") setMode("confirm-delete");
656
+ if (item?.kind === "header") {
657
+ const groupSessions = sessions.filter((s) => s.dir === item.dir);
658
+ setDeleteAllTarget({
659
+ dir: item.dir,
660
+ label: item.label,
661
+ sessions: groupSessions
662
+ });
663
+ setMode("confirm-delete-all");
664
+ }
665
+ }
666
+ if (input === "r") {
667
+ const item = displayItems[cursor];
668
+ if (item?.kind === "session" && (item.session.type === "chat" || item.session.sessionId)) {
669
+ setRenameValue(item.session.label);
670
+ setMode("rename");
671
+ }
672
+ }
673
+ if (input === "f") {
674
+ const item = displayItems[cursor];
675
+ const dir = item?.kind === "session" && item.session.type === "chat" ? item.session.dir : null;
676
+ if (dir) spawnSync("open", [dir]);
677
+ }
678
+ if (input === "m") {
679
+ const item = displayItems[cursor];
680
+ if (item?.kind === "session" && item.session.hasClaudeMd) {
681
+ try {
682
+ const content = readFileSync(
683
+ join(item.session.dir, "CLAUDE.md"),
684
+ "utf8"
685
+ );
686
+ setPreviewContent(content.split("\n"));
687
+ setPreviewScroll(0);
688
+ setMode("preview-claude-md");
689
+ } catch {
690
+ }
691
+ }
692
+ }
693
+ if (input === "p") {
694
+ const item = displayItems[cursor];
695
+ if (item?.kind === "session" && item.session.type === "chat") {
696
+ toggleSessionPin(item.session.dir);
697
+ setSessions(
698
+ (prev) => prev ? prev.map(
699
+ (s) => s.dir === item.session.dir ? { ...s, pinned: !s.pinned } : s
700
+ ) : prev
701
+ );
702
+ }
703
+ }
704
+ if (input === "t") {
705
+ const item = displayItems[cursor];
706
+ if (item?.kind === "session" && item.session.type === "chat") {
707
+ setTagValue(item.session.tag ?? "");
708
+ setMode("tag");
383
709
  }
384
710
  }
385
- if (input === "d" && cursor > 0) setMode("confirm-delete");
386
711
  if (input === "C") {
387
712
  setCleanItems(null);
388
713
  setMode("clean-confirm");
@@ -394,16 +719,17 @@ var App = () => {
394
719
  );
395
720
  useInput(
396
721
  (input, key) => {
397
- if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
398
- if (key.downArrow) setCursor((c) => Math.min(items.length - 1, c + 1));
722
+ if (key.upArrow) moveCursor(-1);
723
+ if (key.downArrow) moveCursor(1);
399
724
  if (key.return) {
400
- if (cursor === 0) {
725
+ const item = displayItems[cursor];
726
+ if (!item) return;
727
+ if (item.kind === "new") {
401
728
  setSearch("");
402
729
  setMode("new");
403
730
  setNewName("");
404
- } else if (items[cursor]) {
405
- const s = items[cursor];
406
- doExit({ type: s.claudeProjectDir ? "open" : "new", dir: s.dir });
731
+ } else if (item.kind === "session") {
732
+ doOpen(item.session);
407
733
  }
408
734
  }
409
735
  if (key.escape) {
@@ -417,88 +743,365 @@ var App = () => {
417
743
  useInput(
418
744
  (input, key) => {
419
745
  if (input === "y") {
420
- const item = items[cursor];
421
- deleteSession(item);
422
- setSessions((s) => s.filter((x) => x.dir !== item.dir));
423
- setCursor((c) => Math.min(c, items.length - 2));
746
+ const item = displayItems[cursor];
747
+ if (item?.kind === "session") {
748
+ deleteSession(item.session);
749
+ setSessions(
750
+ (s) => s.filter(
751
+ (x) => !(x.dir === item.session.dir && x.sessionId === item.session.sessionId)
752
+ )
753
+ );
754
+ setCursor((c) => Math.max(0, c - 1));
755
+ }
424
756
  setMode("list");
425
757
  }
426
758
  if (input === "n" || key.escape) setMode("list");
427
759
  },
428
760
  { isActive: mode === "confirm-delete" }
429
761
  );
762
+ useInput(
763
+ (input, key) => {
764
+ if (input === "y" && deleteAllTarget) {
765
+ const claudeProjectDir = deleteAllTarget.sessions[0]?.claudeProjectDir;
766
+ for (const s of deleteAllTarget.sessions) deleteSession(s);
767
+ if (claudeProjectDir && existsSync(claudeProjectDir))
768
+ try {
769
+ execSync(`trash "${claudeProjectDir}"`);
770
+ } catch {
771
+ }
772
+ removeFromClaudeJson(deleteAllTarget.dir);
773
+ removeSessionLabel(deleteAllTarget.dir);
774
+ setSessions((s) => s.filter((x) => x.dir !== deleteAllTarget.dir));
775
+ setCursor((c) => Math.max(0, c - 1));
776
+ setDeleteAllTarget(null);
777
+ setMode("list");
778
+ }
779
+ if (input === "n" || key.escape) {
780
+ setDeleteAllTarget(null);
781
+ setMode("list");
782
+ }
783
+ },
784
+ { isActive: mode === "confirm-delete-all" }
785
+ );
430
786
  useInput(
431
787
  (_, key) => {
432
788
  if (key.escape) setMode("list");
433
789
  },
434
- { isActive: mode === "new" }
790
+ { isActive: mode === "new" || mode === "rename" || mode === "tag" }
435
791
  );
436
- if (!sessions) return /* @__PURE__ */ jsx(Text, { dimColor: true, children: " loading\u2026" });
437
- if (mode === "new")
438
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
439
- /* @__PURE__ */ jsx(Text, { bold: true, children: "New chat" }),
440
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, gap: 1, children: [
441
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u203A" }),
442
- /* @__PURE__ */ jsx(
443
- TextInput,
444
- {
445
- value: newName,
446
- onChange: setNewName,
447
- onSubmit: (val) => {
448
- if (!val.trim()) {
792
+ useInput(
793
+ (_, key) => {
794
+ if (key.upArrow) setPreviewScroll((s) => Math.max(0, s - 1));
795
+ if (key.downArrow)
796
+ setPreviewScroll(
797
+ (s) => Math.min(Math.max(0, previewContent.length - listHeight), s + 1)
798
+ );
799
+ if (key.escape) setMode("list");
800
+ },
801
+ { isActive: mode === "preview-claude-md" }
802
+ );
803
+ const isSearching = mode === "search";
804
+ const visibleItems = mode === "list" || mode === "search" ? displayItems.slice(scrollOffset, scrollOffset + listHeight) : [];
805
+ const renderContent = () => {
806
+ if (!sessions)
807
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 2, gap: 1, children: [
808
+ /* @__PURE__ */ jsx(Text, { color: SEL_COLOR, children: "\u273B" }),
809
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: LOADING_MESSAGES[loadingIndex] })
810
+ ] });
811
+ if (mode === "new")
812
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
813
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "New chat" }),
814
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, gap: 1, children: [
815
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u203A" }),
816
+ /* @__PURE__ */ jsx(
817
+ TextInput,
818
+ {
819
+ value: newName,
820
+ onChange: setNewName,
821
+ onSubmit: (val) => {
822
+ if (!val.trim()) {
823
+ setMode("list");
824
+ return;
825
+ }
826
+ const dir = join(CHATS_DIR, randomUUID());
827
+ saveSessionLabel(dir, val.trim());
828
+ savedState = { tab, cursor };
829
+ pendingAction = { type: "new", dir, name: val.trim() };
830
+ exit();
831
+ }
832
+ }
833
+ )
834
+ ] }),
835
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Hint, { pairs: [["esc", "cancel"]] }) })
836
+ ] });
837
+ if (mode === "rename") {
838
+ const item = displayItems[cursor];
839
+ const session = item?.kind === "session" ? item.session : null;
840
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
841
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Rename" }),
842
+ session && /* @__PURE__ */ jsx(Text, { dimColor: true, children: session.path }),
843
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, gap: 1, children: [
844
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u203A" }),
845
+ /* @__PURE__ */ jsx(
846
+ TextInput,
847
+ {
848
+ value: renameValue,
849
+ onChange: setRenameValue,
850
+ onSubmit: (val) => {
851
+ if (!val.trim() || !session) {
852
+ setMode("list");
853
+ return;
854
+ }
855
+ const key = session.type === "chat" ? session.dir : session.sessionId;
856
+ if (!key) {
857
+ setMode("list");
858
+ return;
859
+ }
860
+ saveSessionLabel(key, val.trim());
861
+ setSessions(
862
+ (prev) => prev ? prev.map(
863
+ (s) => (session.type === "chat" ? s.dir === session.dir : s.sessionId === session.sessionId) ? { ...s, label: val.trim() } : s
864
+ ) : prev
865
+ );
449
866
  setMode("list");
450
- return;
451
867
  }
452
- doExit({ type: "new", dir: join(CHATS_DIR, slugify(val.trim())) });
453
868
  }
869
+ )
870
+ ] }),
871
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "enter to save \xB7 esc to cancel" }) })
872
+ ] });
873
+ }
874
+ if (mode === "tag") {
875
+ const item = displayItems[cursor];
876
+ const session = item?.kind === "session" ? item.session : null;
877
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
878
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Tag" }),
879
+ session && /* @__PURE__ */ jsx(Text, { dimColor: true, children: session.label }),
880
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, gap: 1, children: [
881
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u203A" }),
882
+ /* @__PURE__ */ jsx(
883
+ TextInput,
884
+ {
885
+ value: tagValue,
886
+ onChange: setTagValue,
887
+ onSubmit: (val) => {
888
+ if (!session) {
889
+ setMode("list");
890
+ return;
891
+ }
892
+ saveSessionTag(session.dir, val);
893
+ setSessions(
894
+ (prev) => prev ? prev.map(
895
+ (s) => s.dir === session.dir ? { ...s, tag: val.trim() || void 0 } : s
896
+ ) : prev
897
+ );
898
+ setMode("list");
899
+ }
900
+ }
901
+ )
902
+ ] }),
903
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "enter to save \xB7 empty to remove \xB7 esc to cancel" }) })
904
+ ] });
905
+ }
906
+ if (mode === "confirm-delete") {
907
+ const item = displayItems[cursor];
908
+ const session = item?.kind === "session" ? item.session : null;
909
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
910
+ /* @__PURE__ */ jsxs(Text, { children: [
911
+ "Remove",
912
+ " ",
913
+ /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: session?.label ?? "" }),
914
+ "?"
915
+ ] }),
916
+ session && /* @__PURE__ */ jsx(Text, { dimColor: true, children: session.path }),
917
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
918
+ Hint,
919
+ {
920
+ pairs: [
921
+ ["y", "confirm"],
922
+ ["n / esc", "cancel"]
923
+ ]
454
924
  }
455
- )
925
+ ) })
926
+ ] });
927
+ }
928
+ if (mode === "confirm-delete-all" && deleteAllTarget)
929
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
930
+ /* @__PURE__ */ jsxs(Text, { children: [
931
+ "Delete all",
932
+ " ",
933
+ /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: deleteAllTarget.sessions.length }),
934
+ " ",
935
+ "sessions for",
936
+ " ",
937
+ /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: deleteAllTarget.label }),
938
+ "?"
939
+ ] }),
940
+ deleteAllTarget.sessions[0]?.sessionId && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "session history only \u2014 project folder is untouched" }),
941
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: deleteAllTarget.sessions.map((s) => /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
942
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "\xB7" }),
943
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: s.label }) }),
944
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, minWidth: 9, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: s.ago }) })
945
+ ] }, s.sessionId ?? s.label)) }),
946
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
947
+ Hint,
948
+ {
949
+ pairs: [
950
+ ["y", "confirm"],
951
+ ["n / esc", "cancel"]
952
+ ]
953
+ }
954
+ ) })
955
+ ] });
956
+ if (mode === "preview-claude-md") {
957
+ const visibleLines = previewContent.slice(
958
+ previewScroll,
959
+ previewScroll + listHeight
960
+ );
961
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
962
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "CLAUDE.md" }),
963
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: visibleLines.map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: line || " " }, previewScroll + i)) }),
964
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
965
+ Hint,
966
+ {
967
+ pairs: [
968
+ ["\u2191\u2193", "scroll"],
969
+ ["esc", "close"]
970
+ ]
971
+ }
972
+ ) })
973
+ ] });
974
+ }
975
+ if (mode === "clean-confirm")
976
+ return /* @__PURE__ */ jsx(
977
+ CleanConfirm,
978
+ {
979
+ items: cleanItems,
980
+ onConfirm: (selected) => {
981
+ for (const item of selected) item.execute();
982
+ setMode("list");
983
+ loadSessions().then(setSessions);
984
+ },
985
+ onCancel: () => setMode("list")
986
+ }
987
+ );
988
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
989
+ tab === "schedule" && displayItems.length === 0 && /* @__PURE__ */ jsxs(Box, { paddingX: 2, gap: 1, children: [
990
+ /* @__PURE__ */ jsx(Text, { color: "blue", children: "i" }),
991
+ /* @__PURE__ */ jsx(Text, { children: "no scheduled tasks yet" })
456
992
  ] }),
457
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Hint, { pairs: [["esc", "cancel"]] }) })
993
+ visibleItems.map((item, vi) => {
994
+ const i = scrollOffset + vi;
995
+ const sel = i === cursor;
996
+ if (item.kind === "new") {
997
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
998
+ /* @__PURE__ */ jsxs(Box, { paddingX: 2, gap: 1, children: [
999
+ /* @__PURE__ */ jsx(Text, { color: sel ? "green" : "gray", children: sel ? "\u203A" : " " }),
1000
+ /* @__PURE__ */ jsx(Text, { color: sel ? SEL_COLOR : "white", bold: sel, children: "+ New chat" })
1001
+ ] }),
1002
+ /* @__PURE__ */ jsx(Text, { children: " " })
1003
+ ] }, "new");
1004
+ }
1005
+ if (item.kind === "header") {
1006
+ return /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, gap: 1, children: [
1007
+ /* @__PURE__ */ jsx(Text, { color: sel ? "green" : "gray", children: sel ? "\u203A" : " " }),
1008
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: item.expanded ? "-" : "+" }),
1009
+ /* @__PURE__ */ jsx(Text, { color: sel ? SEL_COLOR : "green", children: ICON_CODE }),
1010
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsxs(
1011
+ Text,
1012
+ {
1013
+ color: sel ? SEL_COLOR : "white",
1014
+ bold: true,
1015
+ wrap: "truncate-end",
1016
+ children: [
1017
+ item.label,
1018
+ item.count > 1 && /* @__PURE__ */ jsx(
1019
+ Text,
1020
+ {
1021
+ color: sel ? SEL_COLOR : "gray",
1022
+ children: ` (${item.count})`
1023
+ }
1024
+ )
1025
+ ]
1026
+ }
1027
+ ) }),
1028
+ !item.expanded && /* @__PURE__ */ jsx(Box, { flexShrink: 0, minWidth: 9, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: item.recentSession.ago }) })
1029
+ ] }, `h-${item.dir}`);
1030
+ }
1031
+ if (item.kind === "tag-header") {
1032
+ return /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, gap: 1, children: [
1033
+ /* @__PURE__ */ jsx(Text, { color: sel ? "green" : "gray", children: sel ? "\u203A" : " " }),
1034
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: item.expanded ? "-" : "+" }),
1035
+ /* @__PURE__ */ jsx(Text, { color: sel ? SEL_COLOR : "blue", children: "#" }),
1036
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsxs(
1037
+ Text,
1038
+ {
1039
+ color: sel ? SEL_COLOR : "white",
1040
+ bold: true,
1041
+ wrap: "truncate-end",
1042
+ children: [
1043
+ item.label,
1044
+ item.count > 1 && /* @__PURE__ */ jsx(
1045
+ Text,
1046
+ {
1047
+ color: sel ? SEL_COLOR : "gray",
1048
+ children: ` (${item.count})`
1049
+ }
1050
+ )
1051
+ ]
1052
+ }
1053
+ ) })
1054
+ ] }, `tag-${item.label}`);
1055
+ }
1056
+ const s = item.session;
1057
+ const indent = tab === "code" ? 6 : !s.pinned && s.tag ? 4 : 2;
1058
+ return /* @__PURE__ */ jsxs(
1059
+ Box,
1060
+ {
1061
+ paddingLeft: indent,
1062
+ gap: 1,
1063
+ children: [
1064
+ s.type === "chat" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1065
+ /* @__PURE__ */ jsx(Text, { color: sel ? "green" : "gray", children: sel ? "\u203A" : " " }),
1066
+ /* @__PURE__ */ jsx(Text, { color: sel ? SEL_COLOR : "magenta", children: ICON_CHAT })
1067
+ ] }) : /* @__PURE__ */ jsx(Text, { color: sel ? "green" : "gray", children: sel ? "\u203A" : "\xB7" }),
1068
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx(
1069
+ Text,
1070
+ {
1071
+ color: sel ? SEL_COLOR : "white",
1072
+ bold: s.type === "chat",
1073
+ wrap: "truncate-end",
1074
+ children: s.label
1075
+ }
1076
+ ) }),
1077
+ s.pinned && /* @__PURE__ */ jsx(Box, { flexShrink: 0, marginRight: 1, children: /* @__PURE__ */ jsx(Text, { color: sel ? "yellow" : "gray", dimColor: !sel, children: "\u2605" }) }),
1078
+ s.hasClaudeMd && /* @__PURE__ */ jsx(Box, { flexShrink: 0, marginRight: 1, children: /* @__PURE__ */ jsx(Text, { color: sel ? "cyan" : "gray", dimColor: !sel, children: "md" }) }),
1079
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, minWidth: 9, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: s.ago }) })
1080
+ ]
1081
+ },
1082
+ `${s.dir}-${s.sessionId ?? s.label}`
1083
+ );
1084
+ }),
1085
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, paddingX: 2, children: /* @__PURE__ */ jsx(Hint, { pairs: contextHints(displayItems[cursor]) }) })
458
1086
  ] });
459
- if (mode === "confirm-delete") {
460
- const item = items[cursor];
461
- const toDelete = [
462
- ...item.type === "chat" ? [item.dir] : [],
463
- ...item.claudeProjectDir ? [item.claudeProjectDir] : []
464
- ];
465
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
466
- /* @__PURE__ */ jsxs(Text, { children: [
467
- "Remove",
468
- " ",
469
- /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: item.label }),
470
- "?"
471
- ] }),
472
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: toDelete.map((p) => /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
473
- " ",
474
- p.replace(HOME, "~")
475
- ] }, p)) }),
476
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
477
- Hint,
1087
+ };
1088
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 1, width: stdout.columns, children: [
1089
+ /* @__PURE__ */ jsxs(Box, { paddingX: 2, gap: 1, marginBottom: 1, children: [
1090
+ /* @__PURE__ */ jsx(Text, { color: SEL_COLOR, children: "\u273B" }),
1091
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Claude" })
1092
+ ] }),
1093
+ /* @__PURE__ */ jsx(Box, { paddingX: 2, gap: 3, marginBottom: 1, children: TABS.map((t) => /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
1094
+ /* @__PURE__ */ jsx(Text, { color: tab === t ? SEL_COLOR : "gray", children: TAB_ICON[t] }),
1095
+ /* @__PURE__ */ jsx(
1096
+ Text,
478
1097
  {
479
- pairs: [
480
- ["y", "confirm"],
481
- ["n / esc", "cancel"]
482
- ]
1098
+ color: tab === t ? SEL_COLOR : "gray",
1099
+ bold: tab === t,
1100
+ underline: tab === t,
1101
+ children: TAB_LABEL[t]
483
1102
  }
484
- ) })
485
- ] });
486
- }
487
- if (mode === "clean-confirm")
488
- return /* @__PURE__ */ jsx(
489
- CleanConfirm,
490
- {
491
- items: cleanItems,
492
- onConfirm: (selected) => {
493
- for (const item of selected) item.execute();
494
- setMode("list");
495
- loadSessions().then(setSessions);
496
- },
497
- onCancel: () => setMode("list")
498
- }
499
- );
500
- const isSearching = mode === "search";
501
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 1, children: [
1103
+ )
1104
+ ] }, t)) }),
502
1105
  /* @__PURE__ */ jsxs(Box, { paddingX: 2, marginBottom: 1, gap: 1, children: [
503
1106
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/" }),
504
1107
  isSearching ? /* @__PURE__ */ jsx(
@@ -514,68 +1117,158 @@ var App = () => {
514
1117
  }
515
1118
  ) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: search || "search\u2026" })
516
1119
  ] }),
517
- items.map((item, i) => {
518
- const sel = i === cursor;
519
- const prev = items[i - 1];
520
- const showChatHeader = !search && filter === "all" && item?.type === "chat" && prev?.type !== "chat";
521
- const showCodeHeader = !search && filter === "all" && item?.type === "code" && prev?.type !== "code";
522
- if (!item)
523
- return /* @__PURE__ */ jsxs(Box, { paddingX: 2, children: [
524
- /* @__PURE__ */ jsx(Text, { color: sel ? "cyan" : "gray", children: sel ? "\u25B6 " : " " }),
525
- /* @__PURE__ */ jsx(Text, { color: "white", bold: sel, children: "+ New chat" })
526
- ] }, "new");
527
- return /* @__PURE__ */ jsxs(React.Fragment, { children: [
528
- showChatHeader && /* @__PURE__ */ jsx(SectionHeader, { label: "chat" }),
529
- showCodeHeader && /* @__PURE__ */ jsx(SectionHeader, { label: "code" }),
530
- /* @__PURE__ */ jsxs(Box, { paddingX: 2, gap: 2, children: [
531
- /* @__PURE__ */ jsx(Text, { color: sel ? "cyan" : "gray", children: sel ? "\u25B6" : " " }),
532
- /* @__PURE__ */ jsx(
533
- Text,
534
- {
535
- color: item.type === "chat" ? "magenta" : "green",
536
- bold: sel,
537
- children: item.type === "chat" ? ICON_CHAT : ICON_CODE
538
- }
539
- ),
540
- /* @__PURE__ */ jsx(Text, { color: "white", bold: sel, children: item.label }),
541
- /* @__PURE__ */ jsx(Text, { color: "gray", bold: sel, children: item.path }),
542
- /* @__PURE__ */ jsx(Text, { color: "yellow", bold: sel, dimColor: !sel, children: item.ago }),
543
- item.dir === HOME && /* @__PURE__ */ jsx(Text, { color: "red", dimColor: !sel, children: "\u26A0 not recommended" })
544
- ] })
545
- ] }, item.dir);
546
- }),
547
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, paddingX: 2, gap: 3, children: [
548
- /* @__PURE__ */ jsx(
549
- Hint,
550
- {
551
- pairs: [
552
- ["\u2191\u2193", "nav"],
553
- ["enter", "open"],
554
- ["d", "remove"],
555
- ["/", "search"],
556
- ["tab", "filter"],
557
- ["C", "clean"],
558
- ["q", "quit"]
559
- ]
560
- }
561
- ),
562
- /* @__PURE__ */ jsx(FilterBar, { filter })
563
- ] })
1120
+ renderContent()
564
1121
  ] });
565
1122
  };
566
1123
  mkdirSync(CHATS_DIR, { recursive: true });
1124
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1125
+ var runBanner = async () => {
1126
+ const cols = process.stdout.columns ?? 80;
1127
+ const O = "\x1B[38;5;208m";
1128
+ const G = "\x1B[90m";
1129
+ const D = "\x1B[2m";
1130
+ const R = "\x1B[0m";
1131
+ const c = (text, vis) => {
1132
+ const pad = Math.max(0, Math.floor((cols - vis) / 2));
1133
+ return " ".repeat(pad) + text;
1134
+ };
1135
+ const sparkle = (bright) => [
1136
+ "",
1137
+ c(bright ? `${G}\xB7 ${O}\u2726${G} \xB7${R}` : `${G}\xB7 \u2726 \xB7${R}`, 5),
1138
+ c(bright ? `${O}\u2726 \xB7 \u273B \xB7 \u2726${R}` : `${G}\u2726 \xB7 ${O}\u273B${G} \xB7 \u2726${R}`, 9),
1139
+ c(bright ? `${G}\xB7 ${O}\u2726${G} \xB7${R}` : `${G}\xB7 \u2726 \xB7${R}`, 5),
1140
+ "",
1141
+ c(`${O}claude sessions${R}`, 15)
1142
+ ];
1143
+ const txt = (bright) => c(bright ? `${O}claude sessions${R}` : `${D}claude sessions${R}`, 15);
1144
+ const frames = [
1145
+ [["", "", "", "", "", txt(false)], 150],
1146
+ [["", "", c(`${D}\xB7${R}`, 1), "", "", txt(false)], 120],
1147
+ [["", "", c(`${D}${O}\u273B${R}`, 1), "", "", txt(false)], 100],
1148
+ [
1149
+ [
1150
+ "",
1151
+ c(`${G}\xB7 \xB7 \xB7${R}`, 5),
1152
+ c(`${G}\xB7 \u273B \xB7${R}`, 5),
1153
+ c(`${G}\xB7 \xB7 \xB7${R}`, 5),
1154
+ "",
1155
+ txt(false)
1156
+ ],
1157
+ 70
1158
+ ],
1159
+ [
1160
+ [
1161
+ "",
1162
+ c(`${G}\u2726 \xB7 \u2726${R}`, 5),
1163
+ c(`${G}\xB7 ${O}\u273B${G} \xB7${R}`, 5),
1164
+ c(`${G}\u2726 \xB7 \u2726${R}`, 5),
1165
+ "",
1166
+ txt(false)
1167
+ ],
1168
+ 70
1169
+ ],
1170
+ [
1171
+ [
1172
+ "",
1173
+ c(`${G}\xB7 ${O}\u2726${G} \xB7${R}`, 5),
1174
+ c(`${O}\u2726 \xB7 \u273B \xB7 \u2726${R}`, 9),
1175
+ c(`${G}\xB7 ${O}\u2726${G} \xB7${R}`, 5),
1176
+ "",
1177
+ txt(false)
1178
+ ],
1179
+ 70
1180
+ ],
1181
+ [
1182
+ [
1183
+ "",
1184
+ c(`${G}\xB7 ${O}\u2726${G} \xB7${R}`, 5),
1185
+ c(`${O}\u2726 \xB7 \u273B \xB7 \u2726${R}`, 9),
1186
+ c(`${G}\xB7 ${O}\u2726${G} \xB7${R}`, 5),
1187
+ "",
1188
+ txt(true)
1189
+ ],
1190
+ 90
1191
+ ],
1192
+ [
1193
+ [
1194
+ "",
1195
+ c(`${G}\xB7 ${O}\u2726${G} \xB7${R}`, 5),
1196
+ c(`${O}\u2726 \xB7 \u273B \xB7 \u2726${R}`, 9),
1197
+ c(`${G}\xB7 ${O}\u2726${G} \xB7${R}`, 5),
1198
+ "",
1199
+ txt(true)
1200
+ ],
1201
+ 130
1202
+ ],
1203
+ [sparkle(true), 200],
1204
+ [sparkle(false), 180],
1205
+ [sparkle(true), 180],
1206
+ [sparkle(false), 180],
1207
+ [sparkle(true), 180],
1208
+ [sparkle(false), 200],
1209
+ [
1210
+ [
1211
+ "",
1212
+ "",
1213
+ c(`${G}\xB7 ${O}\u273B${G} \xB7${R}`, 5),
1214
+ "",
1215
+ "",
1216
+ c(`${O}claude sessions${R}`, 15)
1217
+ ],
1218
+ 170
1219
+ ],
1220
+ [
1221
+ ["", "", c(`${O}\u273B${R}`, 1), "", "", c(`${O}claude sessions${R}`, 15)],
1222
+ 160
1223
+ ],
1224
+ [
1225
+ ["", "", c(`${O}\u273B${R}`, 1), "", "", c(`${O}claude sessions${R}`, 15)],
1226
+ 200
1227
+ ]
1228
+ ];
1229
+ for (const [lines, ms] of frames) {
1230
+ process.stdout.write("\x1B[H\x1B[J" + lines.join("\n"));
1231
+ await sleep(ms);
1232
+ }
1233
+ process.stdout.write("\x1B[H\x1B[J");
1234
+ };
567
1235
  if (process.argv[2] === "clean") {
568
1236
  const { waitUntilExit } = render(/* @__PURE__ */ jsx(CleanApp, {}), { exitOnCtrlC: true });
569
1237
  await waitUntilExit();
570
1238
  } else {
571
- const { waitUntilExit } = render(/* @__PURE__ */ jsx(App, {}), { exitOnCtrlC: true });
572
- await waitUntilExit();
573
- if (pendingAction) {
574
- const { type, dir } = pendingAction;
1239
+ let firstLaunch = true;
1240
+ while (true) {
1241
+ process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H\x1B[?25l");
1242
+ pendingAction = null;
1243
+ if (firstLaunch && !process.argv.includes("--no-banner")) {
1244
+ sessionPreload = loadSessions();
1245
+ await runBanner();
1246
+ firstLaunch = false;
1247
+ }
1248
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx(App, {}), { exitOnCtrlC: true });
1249
+ await waitUntilExit();
1250
+ process.stdout.write("\x1B[2J\x1B[H\x1B[?1049l\x1B[2J\x1B[H\x1B[?25h");
1251
+ if (!pendingAction) break;
1252
+ const { type, dir, sessionId, name } = pendingAction;
575
1253
  mkdirSync(dir, { recursive: true });
1254
+ if (type === "new" && name) {
1255
+ const claudeMdPath = join(dir, "CLAUDE.md");
1256
+ if (!existsSync(claudeMdPath)) writeFileSync(claudeMdPath, `# ${name}
1257
+ `);
1258
+ }
576
1259
  process.chdir(dir);
577
- spawnSync("claude", type === "open" ? ["--continue"] : [], {
578
- stdio: "inherit"
579
- });
1260
+ const args = type === "resume" && sessionId ? ["--resume", sessionId] : type === "open" ? ["--continue"] : name ? ["--name", name] : [];
1261
+ spawnSync("claude", args, { stdio: "inherit" });
1262
+ if (type === "new") {
1263
+ const claudeProjectDir = join(CLAUDE_PROJECTS, toProjectDirName(dir));
1264
+ const hasConversation = existsSync(claudeProjectDir) && readdirSync(claudeProjectDir).filter((f) => f.endsWith(".jsonl")).some((f) => readFirstPrompt(join(claudeProjectDir, f)) !== "");
1265
+ if (!hasConversation) {
1266
+ try {
1267
+ execSync(`trash "${dir}"`);
1268
+ } catch {
1269
+ }
1270
+ removeSessionLabel(dir);
1271
+ }
1272
+ }
580
1273
  }
581
1274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/claude-sessions-cli",
3
- "version": "1.0.3",
3
+ "version": "2.0.0-next.0",
4
4
  "description": "TUI session manager for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,19 +19,21 @@
19
19
  "build:watch": "tsup --watch",
20
20
  "clean": "rm -rf dist",
21
21
  "typecheck": "tsc --noEmit",
22
- "prepublishOnly": "npm run build"
22
+ "prepublishOnly": "npm run build",
23
+ "publish:next": "npm publish --tag next"
23
24
  },
24
25
  "dependencies": {
25
- "ink": "6.8.0",
26
+ "ink": "7.0.1",
27
+ "ink-spinner": "5.0.0",
26
28
  "ink-text-input": "6.0.0",
27
- "react": "19.2.4"
29
+ "react": "19.2.5"
28
30
  },
29
31
  "devDependencies": {
30
- "@types/node": "^25.5.0",
32
+ "@types/node": "^25.6.0",
31
33
  "@types/react": "^19.2.14",
32
34
  "tsup": "^8.5.1",
33
35
  "tsx": "4.21.0",
34
- "typescript": "^6.0.2"
36
+ "typescript": "^6.0.3"
35
37
  },
36
38
  "engines": {
37
39
  "node": ">=24.0.0"