@kud/claude-sessions-cli 1.0.3 → 2.0.0-next.3

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