@kud/claude-sessions-cli 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -139
  2. package/dist/index.js +418 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,165 +1,46 @@
1
1
  <div align="center">
2
2
 
3
- &nbsp;
4
-
5
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
4
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D24-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org/)
7
- [![npm](https://img.shields.io/badge/npm-%40kud%2Fclaude--sessions--cli-CB3837?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@kud/claude-sessions-cli)
5
+ [![npm](https://img.shields.io/npm/v/%40kud%2Fclaude-sessions-cli?style=flat-square&color=CB3837)](https://www.npmjs.com/package/@kud/claude-sessions-cli)
8
6
  [![MIT](https://img.shields.io/badge/licence-MIT-22C55E?style=flat-square)](LICENSE)
9
7
 
10
- **TUI session manager for Claude Code — browse, resume, organise, and clean up all your sessions from one interactive interface.**
8
+ **TUI session manager for Claude Code**
11
9
 
12
- [Features](#-features) • [Quick Start](#-quick-start) [CLI Reference](#-cli-reference) • [Development](#-development)
10
+ <a href="https://kud.io/projects/claude-sessions-cli">Website</a> · <a href="https://kud.io/projects/claude-sessions-cli/docs">Documentation</a>
13
11
 
14
12
  </div>
15
13
 
16
- ---
17
-
18
- ## 🌟 Features
19
-
20
- - 🗂 **Three-tab interface** — Code sessions grouped by project, Chat sessions with pins and tag folders, and a Scheduled tab
21
- - ⭐ **Pin & tag chat sessions** — star important chats to the top, group others into collapsible `#tag` folders
22
- - 🔁 **Instant resume** — press `enter` on any session and Claude Code opens right where you left off, using the correct `--resume`, `--continue`, or `--name` flag automatically
23
- - 🪄 **Auto CLAUDE.md creation** — new chat sessions get a `CLAUDE.md` bootstrapped automatically; preview any session's `CLAUDE.md` in-place with `m`
24
- - 🧹 **Clean mode** — interactive cleanup of ghost entries, history-less projects, and orphaned history folders; available as both a key binding (`C`) and a subcommand
25
- - ✨ **Animated banner** — a sparkle ASCII animation plays on first launch while sessions load in the background; skip it with `--no-banner`
26
- - 🔍 **Live search** — filter sessions by name or path as you type with `/`
27
-
28
- ---
29
-
30
- ## 🚀 Quick Start
14
+ ## Features
31
15
 
32
- Install the prerelease:
16
+ - **Three-tab interface** — Code sessions grouped by project, Chat sessions with pins and tag folders, and a Scheduled tab.
17
+ - **Instant resume** — press `enter` on any session and Claude Code opens right where you left off, using the correct flag automatically.
18
+ - **Pin and tag chat sessions** — star important chats to the top, group others into collapsible `#tag` folders.
19
+ - **Auto CLAUDE.md creation** — new chat sessions get a `CLAUDE.md` bootstrapped automatically; preview any session's file in-place with `m`.
20
+ - **Clean mode** — interactive cleanup of ghost entries, history-less projects, and orphaned history folders.
21
+ - **Live search** — filter sessions by name or path as you type with `/`.
33
22
 
34
- ```sh
35
- npm install -g @kud/claude-sessions-cli@next
36
- ```
37
-
38
- Launch the TUI:
23
+ ## Install
39
24
 
40
25
  ```sh
41
- claude-sessions
26
+ npm install -g @kud/claude-sessions-cli
42
27
  ```
43
28
 
44
- You will see a brief animated banner followed by the session browser:
45
-
46
- ```
47
- ✻ Claude
48
-
49
- Code Chat Scheduled
50
-
51
- / search…
52
-
53
- › + New chat
54
-
55
- › + my-api (3) 2h
56
- + my-site (1) yesterday
57
- + dotfiles (2) 5d
58
- ```
59
-
60
- Pick a session with `↑` / `↓` and press `enter`. Claude Code opens immediately, resuming that exact conversation.
61
-
62
- ---
63
-
64
- ## 📖 CLI Reference
65
-
66
- ### Subcommands
29
+ ## Usage
67
30
 
68
- | Command | Description |
69
- | ----------------------------- | ------------------------------------------ |
70
- | `claude-sessions` | Open the TUI session browser |
71
- | `claude-sessions clean` | Run the standalone clean-up wizard |
72
- | `claude-sessions --no-banner` | Open the TUI, skipping the intro animation |
73
-
74
- ### Tab overview
75
-
76
- | Tab | Contents |
77
- | ------------- | -------------------------------------------------------------------------------- |
78
- | **Code** | Sessions from project directories, grouped by project, expand/collapse per group |
79
- | **Chat** | Sessions from `~/.claude-sessions/chats/`, with pin stars and `#tag` folders |
80
- | **Scheduled** | Placeholder for scheduled tasks (coming soon) |
81
-
82
- ### Key bindings
83
-
84
- | Key | Context | Action |
85
- | ----------- | --------------- | ------------------------------------------ |
86
- | `↑` `↓` | Anywhere | Navigate the list |
87
- | `←` `→` | Anywhere | Switch tabs |
88
- | `tab` | Anywhere | Cycle to the next tab |
89
- | `enter` | Session / group | Open or resume the session in Claude Code |
90
- | `space` | Group header | Expand or collapse the project / tag group |
91
- | `d` | Session | Delete session (with confirmation) |
92
- | `d` | Project header | Delete all sessions for that project |
93
- | `r` | Code session | Rename the session label |
94
- | `p` | Chat session | Pin or unpin (★) the session |
95
- | `t` | Chat session | Set or remove a `#tag` for the session |
96
- | `m` | Chat session | Preview the session's `CLAUDE.md` inline |
97
- | `f` | Chat session | Open the session folder in Finder |
98
- | `/` | List | Enter search mode — filter by name or path |
99
- | `C` | List | Open clean mode |
100
- | `q` / `esc` | Anywhere | Quit |
101
-
102
- ### Clean mode
103
-
104
- Scans `~/.claude.json` and `~/.claude/projects/` for stale data and groups issues by type. Select which categories to remove before confirming — nothing is deleted without an explicit `y`.
105
-
106
- | Type | Meaning | Action |
107
- | ---------------- | ------------------------------------------------------------------------ | ---------------------------- |
108
- | ghost | In `~/.claude.json` but the project directory no longer exists | Remove from `~/.claude.json` |
109
- | no history | In `~/.claude.json` with no conversation history | Remove from `~/.claude.json` |
110
- | orphaned history | History in `~/.claude/projects/` with no matching `~/.claude.json` entry | Trash the history folder |
111
-
112
- Clean mode is available as the `C` key inside the TUI and as the standalone `claude-sessions clean` subcommand.
113
-
114
- ---
115
-
116
- ## 🔧 Development
117
-
118
- ### Project structure
119
-
120
- ```
121
- claude-sessions-cli/
122
- ├── src/
123
- │ └── index.tsx # entire application — TUI, banner, session logic
124
- ├── dist/ # compiled output (generated)
125
- ├── package.json
126
- └── tsup.config.ts
31
+ ```console
32
+ $ claude-sessions
33
+ $ claude-sessions clean
34
+ $ claude-sessions --no-banner
127
35
  ```
128
36
 
129
- ### Scripts
130
-
131
- | Script | Description |
132
- | ---------------------- | ---------------------------------- |
133
- | `npm run dev` | Run directly from source via `tsx` |
134
- | `npm run build` | Compile to `dist/` with `tsup` |
135
- | `npm run build:watch` | Watch mode compilation |
136
- | `npm run typecheck` | Type-check without emitting |
137
- | `npm run clean` | Remove `dist/` |
138
- | `npm run publish:next` | Publish to the `next` tag on npm |
139
-
140
- ### Clone and run locally
37
+ ## Development
141
38
 
142
39
  ```sh
143
- git clone git@github.com:kud/claude-sessions-cli.git
40
+ git clone https://github.com/kud/claude-sessions-cli.git
144
41
  cd claude-sessions-cli
145
42
  npm install
146
43
  npm run dev
147
44
  ```
148
45
 
149
- ---
150
-
151
- ## 🏗 Tech Stack
152
-
153
- | Package | Role |
154
- | ---------------- | --------------------------------------- |
155
- | `ink` | React renderer for the terminal |
156
- | `ink-text-input` | Controlled text input component for Ink |
157
- | `ink-spinner` | Spinner component for loading states |
158
- | `react` | UI component model |
159
- | `tsup` | TypeScript bundler |
160
- | `tsx` | Direct TypeScript execution for dev |
161
- | `typescript` | Static typing |
162
-
163
- ---
164
-
165
- MIT © [kud](https://github.com/kud) — Made with ❤️
46
+ 📚 **Full documentation → [claude-sessions-cli/docs](https://kud.io/projects/claude-sessions-cli/docs)**
package/dist/index.js CHANGED
@@ -43,12 +43,20 @@ var TAB_ICON = {
43
43
  var sessionPreload = null;
44
44
  var pendingAction = null;
45
45
  var savedState = { tab: "code", cursor: 0 };
46
+ var slugify = (name) => name.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
46
47
  var humanLabel = (dir) => {
47
48
  const base = dir.split("/").pop() || dir;
48
49
  const words = base.replace(/^[._-]+/, "").replace(/[-_]+/g, " ").trim();
49
50
  return (words || base).replace(/\b\w/g, (c) => c.toUpperCase());
50
51
  };
51
52
  var kebabLabel = (dir) => dir.split("/").pop() || dir;
53
+ var windowed = (arr, cursor, height) => {
54
+ const start = Math.max(
55
+ 0,
56
+ Math.min(cursor - Math.floor(height / 2), Math.max(0, arr.length - height))
57
+ );
58
+ return { start, items: arr.slice(start, start + height) };
59
+ };
52
60
  var timeAgo = (mtime) => {
53
61
  const diff = Date.now() / 1e3 - mtime;
54
62
  const m = Math.floor(diff / 60);
@@ -316,6 +324,85 @@ var deleteSession = (session) => {
316
324
  }
317
325
  }
318
326
  };
327
+ var ensureClaudeJsonProject = (dir) => {
328
+ try {
329
+ const json = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
330
+ if (!json.projects) json.projects = {};
331
+ if (!json.projects[dir]) {
332
+ json.projects[dir] = {};
333
+ writeFileSync(CLAUDE_JSON, JSON.stringify(json, null, 2));
334
+ }
335
+ } catch {
336
+ }
337
+ };
338
+ var analyzeSessionMove = (sessionId, fromDir) => {
339
+ try {
340
+ const srcPath = join(
341
+ CLAUDE_PROJECTS,
342
+ toProjectDirName(fromDir),
343
+ `${sessionId}.jsonl`
344
+ );
345
+ const lines = readFileSync(srcPath, "utf8").split("\n").filter((l) => l.trim());
346
+ let embeddedRefs = 0;
347
+ for (const line of lines) {
348
+ const occurrences = line.split(fromDir).length - 1;
349
+ let cwd;
350
+ try {
351
+ cwd = JSON.parse(line).cwd;
352
+ } catch {
353
+ }
354
+ embeddedRefs += Math.max(0, occurrences - (cwd === fromDir ? 1 : 0));
355
+ }
356
+ return { lineCount: lines.length, embeddedRefs };
357
+ } catch {
358
+ return { lineCount: 0, embeddedRefs: 0 };
359
+ }
360
+ };
361
+ var moveSession = (sessionId, fromDir, toDir) => {
362
+ if (toDir === fromDir)
363
+ return { ok: false, error: "destination is the same as the source" };
364
+ try {
365
+ const fromProjectDir = join(CLAUDE_PROJECTS, toProjectDirName(fromDir));
366
+ const toProjectDir = join(CLAUDE_PROJECTS, toProjectDirName(toDir));
367
+ const srcPath = join(fromProjectDir, `${sessionId}.jsonl`);
368
+ const destPath = join(toProjectDir, `${sessionId}.jsonl`);
369
+ if (!existsSync(srcPath))
370
+ return { ok: false, error: "source session not found" };
371
+ if (existsSync(destPath))
372
+ return {
373
+ ok: false,
374
+ error: "a session with this id already exists at the destination"
375
+ };
376
+ const rewritten = readFileSync(srcPath, "utf8").split("\n").map((line) => {
377
+ if (!line.trim()) return line;
378
+ try {
379
+ const obj = JSON.parse(line);
380
+ if (obj.cwd === fromDir) obj.cwd = toDir;
381
+ return JSON.stringify(obj);
382
+ } catch {
383
+ return line;
384
+ }
385
+ }).join("\n");
386
+ mkdirSync(toDir, { recursive: true });
387
+ mkdirSync(toProjectDir, { recursive: true });
388
+ writeFileSync(destPath, rewritten);
389
+ execSync(`trash "${srcPath}"`);
390
+ ensureClaudeJsonProject(toDir);
391
+ const remaining = existsSync(fromProjectDir) ? readdirSync(fromProjectDir).filter((f) => f.endsWith(".jsonl")) : [];
392
+ if (!remaining.length) {
393
+ removeFromClaudeJson(fromDir);
394
+ if (existsSync(fromProjectDir)) {
395
+ try {
396
+ execSync(`trash "${fromProjectDir}"`);
397
+ } catch {
398
+ }
399
+ }
400
+ }
401
+ return { ok: true };
402
+ } catch (e) {
403
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
404
+ }
405
+ };
319
406
  var buildDisplayItems = (tab, sessions, search, expandedProjects, expandedTags) => {
320
407
  const match = (s) => !search || s.label.toLowerCase().includes(search.toLowerCase()) || s.path.toLowerCase().includes(search.toLowerCase());
321
408
  if (tab === "chat") {
@@ -403,6 +490,7 @@ var contextHints = (item) => {
403
490
  pairs.push(["t", "tag"]);
404
491
  } else if (s.sessionId) {
405
492
  pairs.push(["r", "rename"]);
493
+ pairs.push(["M", "move"]);
406
494
  }
407
495
  if (s.hasClaudeMd) pairs.push(["m", "md"]);
408
496
  pairs.push(["q", "quit"]);
@@ -546,6 +634,16 @@ var App = () => {
546
634
  const [expandedTags, setExpandedTags] = useState(/* @__PURE__ */ new Set());
547
635
  const [tagValue, setTagValue] = useState("");
548
636
  const [scrollOffset, setScrollOffset] = useState(0);
637
+ const [wizCursor, setWizCursor] = useState(0);
638
+ const [moveFromDir, setMoveFromDir] = useState(null);
639
+ const [moveSessionSel, setMoveSessionSel] = useState(null);
640
+ const [moveToDir, setMoveToDir] = useState(null);
641
+ const [moveDestKind, setMoveDestKind] = useState(
642
+ "new-subfolder"
643
+ );
644
+ const [moveDestInput, setMoveDestInput] = useState("");
645
+ const [moveAnalysis, setMoveAnalysis] = useState(null);
646
+ const [moveResult, setMoveResult] = useState(null);
549
647
  const LOADING_MESSAGES = [
550
648
  "Summoning your sessions\u2026",
551
649
  "Reading the scrolls\u2026",
@@ -581,6 +679,24 @@ var App = () => {
581
679
  ) : [],
582
680
  [sessions, tab, search, expandedProjects, expandedTags]
583
681
  );
682
+ const moveFolders = useMemo(
683
+ () => sessions ? [
684
+ ...new Set(
685
+ sessions.filter((s) => s.type === "code").map((s) => s.dir)
686
+ )
687
+ ] : [],
688
+ [sessions]
689
+ );
690
+ const moveSessionsList = useMemo(
691
+ () => sessions && moveFromDir ? sessions.filter(
692
+ (s) => s.type === "code" && s.dir === moveFromDir && s.sessionId
693
+ ) : [],
694
+ [sessions, moveFromDir]
695
+ );
696
+ const moveDestFolders = useMemo(
697
+ () => moveFolders.filter((d) => d !== moveFromDir),
698
+ [moveFolders, moveFromDir]
699
+ );
584
700
  useEffect(() => {
585
701
  if (cursor < scrollOffset) setScrollOffset(cursor);
586
702
  else if (cursor >= scrollOffset + listHeight)
@@ -712,6 +828,14 @@ var App = () => {
712
828
  setMode("clean-confirm");
713
829
  findCleanItems().then(setCleanItems);
714
830
  }
831
+ if (input === "M") {
832
+ setMoveFromDir(null);
833
+ setMoveSessionSel(null);
834
+ setMoveToDir(null);
835
+ setMoveAnalysis(null);
836
+ setWizCursor(0);
837
+ setMode("move-folder");
838
+ }
715
839
  if (input === "q" || key.escape) exit();
716
840
  },
717
841
  { isActive: mode === "list" && !!sessions }
@@ -799,6 +923,97 @@ var App = () => {
799
923
  },
800
924
  { isActive: mode === "preview-claude-md" }
801
925
  );
926
+ const goToMoveConfirm = (toDir) => {
927
+ if (!moveSessionSel?.sessionId) return;
928
+ setMoveToDir(toDir);
929
+ setMoveAnalysis(
930
+ analyzeSessionMove(moveSessionSel.sessionId, moveSessionSel.dir)
931
+ );
932
+ setMode("move-confirm");
933
+ };
934
+ useInput(
935
+ (_, key) => {
936
+ const length = mode === "move-folder" ? moveFolders.length : mode === "move-session" ? moveSessionsList.length : moveDestFolders.length + 2;
937
+ if (key.upArrow) setWizCursor((c) => Math.max(0, c - 1));
938
+ if (key.downArrow) setWizCursor((c) => Math.min(length - 1, c + 1));
939
+ if (key.escape) {
940
+ if (mode === "move-folder") setMode("list");
941
+ else if (mode === "move-session") {
942
+ setMode("move-folder");
943
+ setWizCursor(0);
944
+ } else {
945
+ setMode("move-session");
946
+ setWizCursor(0);
947
+ }
948
+ }
949
+ if (key.return) {
950
+ if (mode === "move-folder") {
951
+ const dir = moveFolders[wizCursor];
952
+ if (!dir) return;
953
+ setMoveFromDir(dir);
954
+ setWizCursor(0);
955
+ setMode("move-session");
956
+ } else if (mode === "move-session") {
957
+ const session = moveSessionsList[wizCursor];
958
+ if (!session) return;
959
+ setMoveSessionSel(session);
960
+ setWizCursor(0);
961
+ setMode("move-dest");
962
+ } else if (mode === "move-dest") {
963
+ if (wizCursor < moveDestFolders.length) {
964
+ const dir = moveDestFolders[wizCursor];
965
+ if (dir) goToMoveConfirm(dir);
966
+ } else {
967
+ setMoveDestKind(
968
+ wizCursor === moveDestFolders.length ? "new-subfolder" : "other"
969
+ );
970
+ setMoveDestInput("");
971
+ setMode("move-dest-input");
972
+ }
973
+ }
974
+ }
975
+ },
976
+ {
977
+ isActive: mode === "move-folder" || mode === "move-session" || mode === "move-dest"
978
+ }
979
+ );
980
+ useInput(
981
+ (_, key) => {
982
+ if (key.escape) setMode("move-dest");
983
+ },
984
+ { isActive: mode === "move-dest-input" }
985
+ );
986
+ useInput(
987
+ (input, key) => {
988
+ if (input === "y" && moveSessionSel?.sessionId && moveToDir) {
989
+ const result = moveSession(
990
+ moveSessionSel.sessionId,
991
+ moveSessionSel.dir,
992
+ moveToDir
993
+ );
994
+ setMoveResult(result);
995
+ if (result.ok) loadSessions().then(setSessions);
996
+ setMode("move-done");
997
+ }
998
+ if (input === "n" || key.escape) setMode("move-dest");
999
+ },
1000
+ { isActive: mode === "move-confirm" }
1001
+ );
1002
+ useInput(
1003
+ (_, key) => {
1004
+ if (key.return || key.escape) {
1005
+ setMoveResult(null);
1006
+ setMoveFromDir(null);
1007
+ setMoveSessionSel(null);
1008
+ setMoveToDir(null);
1009
+ setMoveAnalysis(null);
1010
+ setWizCursor(0);
1011
+ setCursor(0);
1012
+ setMode("list");
1013
+ }
1014
+ },
1015
+ { isActive: mode === "move-done" }
1016
+ );
802
1017
  const isSearching = mode === "search";
803
1018
  const visibleItems = mode === "list" || mode === "search" ? displayItems.slice(scrollOffset, scrollOffset + listHeight) : [];
804
1019
  const renderContent = () => {
@@ -971,6 +1186,209 @@ var App = () => {
971
1186
  ) })
972
1187
  ] });
973
1188
  }
1189
+ const moveHeader = (step, subtitle) => /* @__PURE__ */ jsxs(Fragment, { children: [
1190
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
1191
+ "Move session ",
1192
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1193
+ "\xB7 step ",
1194
+ step,
1195
+ "/3"
1196
+ ] })
1197
+ ] }),
1198
+ subtitle && /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: subtitle })
1199
+ ] });
1200
+ if (mode === "move-folder") {
1201
+ const { start, items } = windowed(moveFolders, wizCursor, listHeight);
1202
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
1203
+ moveHeader(1, "Pick the folder to move a session from"),
1204
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1205
+ moveFolders.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "no code sessions found" }),
1206
+ items.map((dir, vi) => {
1207
+ const i = start + vi;
1208
+ const sel = i === wizCursor;
1209
+ return /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
1210
+ /* @__PURE__ */ jsx(Text, { color: sel ? "green" : "gray", children: sel ? "\u203A" : " " }),
1211
+ /* @__PURE__ */ jsx(Text, { color: sel ? SEL_COLOR : "green", children: ICON_CODE }),
1212
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx(Text, { color: sel ? SEL_COLOR : "white", wrap: "truncate-end", children: dir.replace(HOME, "~") }) })
1213
+ ] }, dir);
1214
+ })
1215
+ ] }),
1216
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
1217
+ Hint,
1218
+ {
1219
+ pairs: [
1220
+ ["\u2191\u2193", "nav"],
1221
+ ["enter", "select"],
1222
+ ["esc", "cancel"]
1223
+ ]
1224
+ }
1225
+ ) })
1226
+ ] });
1227
+ }
1228
+ if (mode === "move-session") {
1229
+ const { start, items } = windowed(moveSessionsList, wizCursor, listHeight);
1230
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
1231
+ moveHeader(2, moveFromDir?.replace(HOME, "~")),
1232
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: items.map((s, vi) => {
1233
+ const i = start + vi;
1234
+ const sel = i === wizCursor;
1235
+ return /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
1236
+ /* @__PURE__ */ jsx(Text, { color: sel ? "green" : "gray", children: sel ? "\u203A" : "\xB7" }),
1237
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx(Text, { color: sel ? SEL_COLOR : "white", wrap: "truncate-end", children: s.label }) }),
1238
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, minWidth: 9, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: s.ago }) })
1239
+ ] }, s.sessionId);
1240
+ }) }),
1241
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
1242
+ Hint,
1243
+ {
1244
+ pairs: [
1245
+ ["\u2191\u2193", "nav"],
1246
+ ["enter", "select"],
1247
+ ["esc", "back"]
1248
+ ]
1249
+ }
1250
+ ) })
1251
+ ] });
1252
+ }
1253
+ if (mode === "move-dest") {
1254
+ const specials = [
1255
+ `+ New subfolder of ${moveFromDir?.replace(HOME, "~")}`,
1256
+ "+ Other path\u2026"
1257
+ ];
1258
+ const all = [
1259
+ ...moveDestFolders.map((d) => d.replace(HOME, "~")),
1260
+ ...specials
1261
+ ];
1262
+ const { start, items } = windowed(all, wizCursor, listHeight);
1263
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
1264
+ moveHeader(3, moveSessionSel?.label),
1265
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: items.map((labelText, vi) => {
1266
+ const i = start + vi;
1267
+ const sel = i === wizCursor;
1268
+ const isSpecial = i >= moveDestFolders.length;
1269
+ return /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
1270
+ /* @__PURE__ */ jsx(Text, { color: sel ? "green" : "gray", children: sel ? "\u203A" : " " }),
1271
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx(
1272
+ Text,
1273
+ {
1274
+ color: sel ? SEL_COLOR : isSpecial ? "cyan" : "white",
1275
+ wrap: "truncate-end",
1276
+ children: labelText
1277
+ }
1278
+ ) })
1279
+ ] }, labelText);
1280
+ }) }),
1281
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
1282
+ Hint,
1283
+ {
1284
+ pairs: [
1285
+ ["\u2191\u2193", "nav"],
1286
+ ["enter", "select"],
1287
+ ["esc", "back"]
1288
+ ]
1289
+ }
1290
+ ) })
1291
+ ] });
1292
+ }
1293
+ if (mode === "move-dest-input") {
1294
+ const isSub = moveDestKind === "new-subfolder";
1295
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
1296
+ moveHeader(3),
1297
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: isSub ? `New subfolder of ${moveFromDir?.replace(HOME, "~")} (kebab-case)` : "Destination path (absolute, ~ allowed)" }),
1298
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, gap: 1, children: [
1299
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u203A" }),
1300
+ /* @__PURE__ */ jsx(
1301
+ TextInput,
1302
+ {
1303
+ value: moveDestInput,
1304
+ onChange: setMoveDestInput,
1305
+ onSubmit: (val) => {
1306
+ const trimmed = val.trim();
1307
+ if (!trimmed || !moveFromDir) {
1308
+ setMode("move-dest");
1309
+ return;
1310
+ }
1311
+ if (isSub) {
1312
+ const slug = slugify(trimmed);
1313
+ if (!slug) {
1314
+ setMode("move-dest");
1315
+ return;
1316
+ }
1317
+ goToMoveConfirm(join(moveFromDir, slug));
1318
+ } else {
1319
+ const toDir = trimmed.replace(/^~(?=$|\/)/, HOME);
1320
+ if (!toDir.startsWith("/")) {
1321
+ setMode("move-dest");
1322
+ return;
1323
+ }
1324
+ goToMoveConfirm(toDir);
1325
+ }
1326
+ }
1327
+ }
1328
+ )
1329
+ ] }),
1330
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "enter to continue \xB7 esc to go back" }) })
1331
+ ] });
1332
+ }
1333
+ if (mode === "move-confirm" && moveSessionSel && moveToDir) {
1334
+ const refs = moveAnalysis?.embeddedRefs ?? 0;
1335
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
1336
+ /* @__PURE__ */ jsxs(Text, { children: [
1337
+ "Move",
1338
+ " ",
1339
+ /* @__PURE__ */ jsx(Text, { color: SEL_COLOR, bold: true, children: moveSessionSel.label }),
1340
+ "?"
1341
+ ] }),
1342
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1343
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1344
+ "from ",
1345
+ moveSessionSel.dir.replace(HOME, "~")
1346
+ ] }),
1347
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1348
+ "to ",
1349
+ moveToDir.replace(HOME, "~")
1350
+ ] })
1351
+ ] }),
1352
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1353
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1354
+ "rewrites cwd on ",
1355
+ moveAnalysis?.lineCount ?? 0,
1356
+ " lines \xB7 updates ~/.claude.json"
1357
+ ] }),
1358
+ refs > 0 && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1359
+ "\u26A0 ",
1360
+ refs,
1361
+ " embedded path reference",
1362
+ refs === 1 ? "" : "s",
1363
+ " to the old folder won't be rewritten"
1364
+ ] })
1365
+ ] }),
1366
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
1367
+ Hint,
1368
+ {
1369
+ pairs: [
1370
+ ["y", "move"],
1371
+ ["n / esc", "back"]
1372
+ ]
1373
+ }
1374
+ ) })
1375
+ ] });
1376
+ }
1377
+ if (mode === "move-done")
1378
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, children: [
1379
+ moveResult?.ok ? /* @__PURE__ */ jsxs(Fragment, { children: [
1380
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713 moved" }),
1381
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1382
+ "now under ",
1383
+ moveToDir?.replace(HOME, "~")
1384
+ ] }),
1385
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "resume it from the Code list" })
1386
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1387
+ /* @__PURE__ */ jsx(Text, { color: "red", children: "\u2717 move failed" }),
1388
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: moveResult?.error ?? "unknown error" })
1389
+ ] }),
1390
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Hint, { pairs: [["enter / esc", "back"]] }) })
1391
+ ] });
974
1392
  if (mode === "clean-confirm")
975
1393
  return /* @__PURE__ */ jsx(
976
1394
  CleanConfirm,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/claude-sessions-cli",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "TUI session manager for Claude Code",
5
5
  "homepage": "https://kud.github.io/claude-sessions-cli",
6
6
  "repository": {