@vinh0809it/claude-sessions 0.3.2 → 0.4.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/README.md +24 -9
- package/bin/claude-sessions.js +4 -0
- package/lib/ansi.js +66 -0
- package/lib/cli.js +63 -0
- package/lib/config.js +93 -0
- package/lib/format.js +45 -0
- package/lib/launch.js +164 -0
- package/lib/operations.js +137 -0
- package/lib/paths.js +86 -0
- package/lib/sessions.js +236 -0
- package/lib/tui/app.js +525 -0
- package/lib/tui/keys.js +45 -0
- package/lib/tui/line-input.js +63 -0
- package/lib/tui/picker.js +166 -0
- package/package.json +5 -4
- package/index.js +0 -1415
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { screen, bold, dim, fg, pad, trunc, selectedLine, COLOR } = require('../ansi');
|
|
5
|
+
const { expandHome, tabComplete } = require('../paths');
|
|
6
|
+
const { is } = require('./keys');
|
|
7
|
+
const { LineInput } = require('./line-input');
|
|
8
|
+
|
|
9
|
+
const { moveTo, clearLine } = screen;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The profile → folder → typed path flow shared by "move" and "new session".
|
|
13
|
+
*
|
|
14
|
+
* Steps: 'profile' (skipped when there is only one), 'folder', 'input'.
|
|
15
|
+
* Items are { type: 'profile' | 'folder' | 'same' | 'new', label, ... }.
|
|
16
|
+
*/
|
|
17
|
+
class Picker {
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} o
|
|
20
|
+
* @param {string|(() => string)} o.headerLabel
|
|
21
|
+
* @param {string} [o.profileLabel]
|
|
22
|
+
* @param {string} o.inputPrompt
|
|
23
|
+
* @param {() => object[]} o.buildProfileItems
|
|
24
|
+
* @param {(dir: string) => object[]} o.buildFolderItems
|
|
25
|
+
* @param {(item: object) => void} o.onConfirmItem A folder or 'same' item was chosen
|
|
26
|
+
* @param {(absPath: string) => void} o.onConfirmCustom A path was typed
|
|
27
|
+
*/
|
|
28
|
+
constructor({ headerLabel, profileLabel = 'Select profile', inputPrompt,
|
|
29
|
+
buildProfileItems, buildFolderItems, onConfirmItem, onConfirmCustom }) {
|
|
30
|
+
this.headerLabel = headerLabel;
|
|
31
|
+
this.profileLabel = profileLabel;
|
|
32
|
+
this.inputPrompt = inputPrompt;
|
|
33
|
+
this.buildProfileItems = buildProfileItems;
|
|
34
|
+
this.buildFolderItems = buildFolderItems;
|
|
35
|
+
this.onConfirmItem = onConfirmItem;
|
|
36
|
+
this.onConfirmCustom = onConfirmCustom;
|
|
37
|
+
|
|
38
|
+
this.field = new LineInput();
|
|
39
|
+
this.completions = [];
|
|
40
|
+
this._showItems('profile', []);
|
|
41
|
+
this.config = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
enter() {
|
|
45
|
+
this.config = null;
|
|
46
|
+
this.field.set('');
|
|
47
|
+
this.completions = [];
|
|
48
|
+
this._showItems('profile', this.buildProfileItems());
|
|
49
|
+
if (this.items.length === 1) this._enterFolderStep(this.items[0].configDirPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_showItems(step, items) {
|
|
53
|
+
this.step = step;
|
|
54
|
+
this.items = items;
|
|
55
|
+
this.selIdx = 0;
|
|
56
|
+
this.scroll = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_enterFolderStep(configDirPath) {
|
|
60
|
+
this.config = configDirPath;
|
|
61
|
+
this._showItems('folder', this.buildFolderItems(configDirPath));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Returns false when the picker should close. */
|
|
65
|
+
handleKey(key) {
|
|
66
|
+
if (this.step === 'input') return this._handleInputKey(key);
|
|
67
|
+
|
|
68
|
+
if (is(key, 'ESCAPE')) {
|
|
69
|
+
const profiles = this.buildProfileItems();
|
|
70
|
+
if (this.step === 'folder' && profiles.length > 1) { this._showItems('profile', profiles); return true; }
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (is(key, 'UP') || key === 'k') this.selIdx = Math.max(0, this.selIdx - 1);
|
|
74
|
+
if (is(key, 'DOWN') || key === 'j') this.selIdx = Math.min(this.items.length - 1, this.selIdx + 1);
|
|
75
|
+
|
|
76
|
+
if (is(key, 'ENTER')) {
|
|
77
|
+
const item = this.items[this.selIdx];
|
|
78
|
+
if (!item) return true;
|
|
79
|
+
if (item.type === 'profile') this._enterFolderStep(item.configDirPath);
|
|
80
|
+
else if (item.type === 'new') { this.step = 'input'; this.field.set('~/'); this.completions = []; }
|
|
81
|
+
else this.onConfirmItem(item);
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_handleInputKey(key) {
|
|
87
|
+
if (is(key, 'ESCAPE')) {
|
|
88
|
+
this.completions = [];
|
|
89
|
+
this._showItems('folder', this.buildFolderItems(this.config));
|
|
90
|
+
} else if (is(key, 'TAB')) {
|
|
91
|
+
const { completed, matches } = tabComplete(this.field.text || '~/');
|
|
92
|
+
this.field.set(completed);
|
|
93
|
+
this.completions = matches.length > 1 ? matches : [];
|
|
94
|
+
} else if (is(key, 'ENTER')) {
|
|
95
|
+
const raw = this.field.text.trim();
|
|
96
|
+
this.completions = [];
|
|
97
|
+
if (raw) this.onConfirmCustom(path.resolve(expandHome(raw).replace(/[\\/]+$/, '') || path.sep));
|
|
98
|
+
else this.step = 'folder';
|
|
99
|
+
} else if (this.field.handle(key)) {
|
|
100
|
+
this.completions = [];
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Screen position [row, col] of the text cursor, or null outside the input step. */
|
|
106
|
+
cursorPosition() {
|
|
107
|
+
if (this.step !== 'input') return null;
|
|
108
|
+
return [4, ` ${this.inputPrompt} `.length + [...this.field.before].length + 1];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Footer key hints for the current step. */
|
|
112
|
+
hints() {
|
|
113
|
+
if (this.step === 'profile') return ' ↑↓/jk=navigate Enter=select profile Esc=cancel';
|
|
114
|
+
if (this.step === 'input') return ' Tab=complete path Enter=confirm Esc=back';
|
|
115
|
+
return ' ↑↓/jk=navigate Enter=select Esc=back';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
render(out, W, H) {
|
|
119
|
+
const header = typeof this.headerLabel === 'function' ? this.headerLabel() : this.headerLabel;
|
|
120
|
+
out.push(moveTo(2, 1), clearLine, ` ${header}`);
|
|
121
|
+
|
|
122
|
+
const divider = dim('─'.repeat(Math.max(0, W - 30)));
|
|
123
|
+
const title = this.step === 'profile'
|
|
124
|
+
? bold(this.profileLabel)
|
|
125
|
+
: bold('Select folder in ') + bold(fg('96', `[${path.basename(this.config)}]`));
|
|
126
|
+
out.push(moveTo(3, 1), clearLine, ` ${title} ${divider}`);
|
|
127
|
+
|
|
128
|
+
let listStart = 4;
|
|
129
|
+
if (this.step === 'input') {
|
|
130
|
+
out.push(moveTo(4, 1), clearLine, ` ${fg('93', this.inputPrompt)} ${this.field.text}`);
|
|
131
|
+
out.push(moveTo(5, 1), clearLine);
|
|
132
|
+
if (this.completions.length > 1) {
|
|
133
|
+
const cols = Math.max(1, Math.floor((W - 2) / 22));
|
|
134
|
+
const rows = Math.ceil(this.completions.length / cols);
|
|
135
|
+
for (let r = 0; r < Math.min(rows, H - 8); r++) {
|
|
136
|
+
const names = this.completions.slice(r * cols, r * cols + cols).map(n => pad(n, 22)).join(' ');
|
|
137
|
+
out.push(moveTo(r + 5, 1), clearLine, ` ${dim(names)}`);
|
|
138
|
+
}
|
|
139
|
+
listStart = H;
|
|
140
|
+
} else {
|
|
141
|
+
out.push(dim(' Tab to complete path'));
|
|
142
|
+
listStart = 6;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const items = this.step === 'input' ? [] : this.items;
|
|
147
|
+
const visible = Math.max(0, H - listStart - 1);
|
|
148
|
+
if (this.selIdx < this.scroll) this.scroll = this.selIdx;
|
|
149
|
+
if (this.selIdx >= this.scroll + visible) this.scroll = this.selIdx - visible + 1;
|
|
150
|
+
if (this.scroll < 0) this.scroll = 0;
|
|
151
|
+
|
|
152
|
+
for (let r = 0; r < visible; r++) {
|
|
153
|
+
out.push(moveTo(listStart + r, 1), clearLine);
|
|
154
|
+
const item = items[this.scroll + r];
|
|
155
|
+
if (!item) continue;
|
|
156
|
+
const isSelected = this.scroll + r === this.selIdx;
|
|
157
|
+
const label = item.type === 'same' ? (isSelected && COLOR ? fg('37', item.label) : dim(item.label))
|
|
158
|
+
: item.type === 'new' ? fg('93', item.label)
|
|
159
|
+
: item.type === 'profile' ? bold(fg('96', item.label))
|
|
160
|
+
: fg('33', item.label);
|
|
161
|
+
out.push(isSelected ? selectedLine(` ${label}`, ` ${trunc(item.label, W - 5)}`, W) : ` ${label}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { Picker };
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vinh0809it/claude-sessions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Fast terminal UI for browsing, previewing, resuming, renaming, moving, and deleting Claude Code sessions",
|
|
5
5
|
"bin": {
|
|
6
|
-
"claude-sessions": "
|
|
6
|
+
"claude-sessions": "bin/claude-sessions.js"
|
|
7
7
|
},
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=18"
|
|
@@ -27,9 +27,10 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/vinh0809it/claude-sessions#readme",
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node --test test/*.js"
|
|
30
|
+
"test": "node --test test/*.test.js"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
|
-
"
|
|
33
|
+
"bin",
|
|
34
|
+
"lib"
|
|
34
35
|
]
|
|
35
36
|
}
|