@kud/claude-sessions-cli 2.0.0-next.3 → 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.
- package/LICENSE +21 -0
- package/README.md +20 -139
- package/dist/index.js +418 -0
- package/package.json +7 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erwann Mest
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,165 +1,46 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
3
|
[](https://www.typescriptlang.org/)
|
|
6
4
|
[](https://nodejs.org/)
|
|
7
|
-
[](https://www.npmjs.com/package/@kud/claude-sessions-cli)
|
|
8
6
|
[](LICENSE)
|
|
9
7
|
|
|
10
|
-
**TUI session manager for Claude Code
|
|
8
|
+
**TUI session manager for Claude Code**
|
|
11
9
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
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,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kud/claude-sessions-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "TUI session manager for Claude Code",
|
|
5
|
+
"homepage": "https://kud.github.io/claude-sessions-cli",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/kud/claude-sessions-cli.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
5
11
|
"type": "module",
|
|
6
12
|
"bin": {
|
|
7
13
|
"claude-sessions": "dist/index.js"
|