tpc-explorer 1.1.1 → 2.0.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 +33 -11
- package/index.js +477 -230
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TPC Explorer Pro
|
|
2
2
|
|
|
3
|
-
A terminal file explorer with embedded AI assistants
|
|
3
|
+
A terminal file explorer with embedded AI assistants, command palette, git integration, and multi-session support.
|
|
4
4
|
|
|
5
5
|
 
|
|
6
6
|
|
|
@@ -19,17 +19,24 @@ npx tpc-explorer
|
|
|
19
19
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
22
|
-
- File tree with syntax-highlighted preview and
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
22
|
+
- **File tree** with syntax-highlighted preview, line numbers, and breadcrumb navigation
|
|
23
|
+
- **Git integration** — branch name in status bar, M/A/U/D indicators on changed files
|
|
24
|
+
- **Embedded AI terminal** — launch Claude Code or OpenAI Codex inside the explorer
|
|
25
|
+
- **Multi-session** — run multiple AI sessions simultaneously, switch with `Ctrl+A`
|
|
26
|
+
- **Session history** — browse and resume past Claude/Codex conversations
|
|
27
|
+
- **Command palette** (`Ctrl+B`) — git status, diff, log, branches, disk usage, package info, and more
|
|
28
|
+
- **File search** (`Ctrl+F`) — fuzzy find files across the project
|
|
29
|
+
- **Welcome screen** — ASCII logo with shortcuts and project info on startup
|
|
30
|
+
- **Loading spinner** when AI sessions start
|
|
31
|
+
- **Copy path** and **Open in $EDITOR** from the tree
|
|
26
32
|
- Tokyo Night Storm theme with emoji file-type icons (no Nerd Font needed)
|
|
27
|
-
- File management: create files/folders, delete
|
|
28
33
|
|
|
29
34
|
## Keyboard Shortcuts
|
|
30
35
|
|
|
31
36
|
| Key | Action |
|
|
32
37
|
|-----|--------|
|
|
38
|
+
| `Ctrl+B` | Command palette (git, views, actions) |
|
|
39
|
+
| `Ctrl+F` | Search files |
|
|
33
40
|
| `Ctrl+T` | Switch panel (Tree / Sessions / Viewer) |
|
|
34
41
|
| `Ctrl+A` | Switch between active AI sessions |
|
|
35
42
|
| `Ctrl+D` | Kill all sessions and quit |
|
|
@@ -37,7 +44,9 @@ npx tpc-explorer
|
|
|
37
44
|
| `n` | New file |
|
|
38
45
|
| `f` | New folder |
|
|
39
46
|
| `d` | Delete file/folder |
|
|
40
|
-
| `
|
|
47
|
+
| `c` | Copy file path to clipboard |
|
|
48
|
+
| `e` | Open in $EDITOR |
|
|
49
|
+
| `r` | Refresh tree, git status, and sessions |
|
|
41
50
|
| `q` / `Esc` | Quit |
|
|
42
51
|
|
|
43
52
|
### Inside AI session
|
|
@@ -47,21 +56,34 @@ npx tpc-explorer
|
|
|
47
56
|
| `Ctrl+C` | Send interrupt to AI |
|
|
48
57
|
| `Ctrl+C` x2 | Force kill active session |
|
|
49
58
|
| `Ctrl+T` | Switch to another panel (AI keeps running) |
|
|
59
|
+
| `Ctrl+B` | Open command palette |
|
|
50
60
|
|
|
51
61
|
## Layout
|
|
52
62
|
|
|
53
63
|
```
|
|
54
64
|
┌─ EXPLORER ──┬─────────── PREVIEW ───────────┐
|
|
55
|
-
│ file tree │
|
|
65
|
+
│ file tree │ breadcrumb path │
|
|
66
|
+
│ with git │ ───────────────────── │
|
|
67
|
+
│ indicators │ syntax-highlighted content │
|
|
56
68
|
│ │ or embedded AI terminal │
|
|
57
69
|
├─ SESSIONS ──┤ │
|
|
58
|
-
│ Claude
|
|
59
|
-
│ Codex
|
|
70
|
+
│ ◆ Claude │ │
|
|
71
|
+
│ ⊞ Codex │ │
|
|
60
72
|
├─────────────┴────────────────────────────────┤
|
|
61
|
-
│
|
|
73
|
+
│ Ctrl-T Switch │ Ctrl-B Cmds │ ⊙ main Ctrl-D│
|
|
62
74
|
└──────────────────────────────────────────────┘
|
|
63
75
|
```
|
|
64
76
|
|
|
77
|
+
## Command Palette
|
|
78
|
+
|
|
79
|
+
Press `Ctrl+B` to open. Available actions:
|
|
80
|
+
|
|
81
|
+
**Actions** — Launch AI, Switch Sessions, Search Files, New File/Folder, Delete, Copy Path, Open in Editor
|
|
82
|
+
|
|
83
|
+
**Git** — Status, Diff, Log (last 20), Branches, Stash List
|
|
84
|
+
|
|
85
|
+
**View** — Disk Usage, Package Info (parsed package.json with scripts, deps)
|
|
86
|
+
|
|
65
87
|
## Requirements
|
|
66
88
|
|
|
67
89
|
- Node.js >= 18
|
package/index.js
CHANGED
|
@@ -68,6 +68,13 @@ function hexToAnsi(hex) {
|
|
|
68
68
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
69
69
|
return `\x1b[38;2;${r};${g};${b}m`;
|
|
70
70
|
}
|
|
71
|
+
function hexToBgAnsi(hex) {
|
|
72
|
+
if (!hex || hex[0] !== '#') return '';
|
|
73
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
74
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
75
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
76
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
77
|
+
}
|
|
71
78
|
|
|
72
79
|
function getFileIcon(filename, isDir) {
|
|
73
80
|
if (isDir) return `${hexToAnsi(theme.accent)}▸ \x1b[0m`;
|
|
@@ -82,12 +89,53 @@ function getFileIcon(filename, isDir) {
|
|
|
82
89
|
return `${hexToAnsi(color)}${icon}\x1b[0m`;
|
|
83
90
|
}
|
|
84
91
|
|
|
92
|
+
// ── Git Helpers ─────────────────────────────────────────────────────────
|
|
93
|
+
let isGitRepo = false;
|
|
94
|
+
let gitStatusMap = {}; // relativePath → status char (M, A, ?, D, etc.)
|
|
95
|
+
let gitBranch = '';
|
|
96
|
+
|
|
97
|
+
function refreshGitStatus() {
|
|
98
|
+
try {
|
|
99
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd: process.cwd(), stdio: 'ignore' });
|
|
100
|
+
isGitRepo = true;
|
|
101
|
+
} catch (e) { isGitRepo = false; gitStatusMap = {}; gitBranch = ''; return; }
|
|
102
|
+
try {
|
|
103
|
+
gitBranch = execSync('git branch --show-current', { cwd: process.cwd(), encoding: 'utf-8' }).trim();
|
|
104
|
+
} catch (e) { gitBranch = ''; }
|
|
105
|
+
gitStatusMap = {};
|
|
106
|
+
try {
|
|
107
|
+
const out = execSync('git status --porcelain', { cwd: process.cwd(), encoding: 'utf-8' });
|
|
108
|
+
for (const line of out.split('\n')) {
|
|
109
|
+
if (line.length < 4) continue;
|
|
110
|
+
const xy = line.slice(0, 2);
|
|
111
|
+
const file = line.slice(3);
|
|
112
|
+
const first = file.split('/')[0];
|
|
113
|
+
// Simplified: M=modified, A=added, ?=untracked, D=deleted
|
|
114
|
+
if (xy.includes('?')) { gitStatusMap[file] = '?'; gitStatusMap[first] = gitStatusMap[first] || '?'; }
|
|
115
|
+
else if (xy.includes('A')) { gitStatusMap[file] = 'A'; gitStatusMap[first] = gitStatusMap[first] || 'A'; }
|
|
116
|
+
else if (xy.includes('D')) { gitStatusMap[file] = 'D'; gitStatusMap[first] = gitStatusMap[first] || 'D'; }
|
|
117
|
+
else if (xy.includes('M') || xy.includes('U')) { gitStatusMap[file] = 'M'; gitStatusMap[first] = 'M'; }
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getGitIndicator(filePath) {
|
|
123
|
+
if (!isGitRepo) return '';
|
|
124
|
+
const rel = path.relative(process.cwd(), filePath);
|
|
125
|
+
const status = gitStatusMap[rel];
|
|
126
|
+
if (!status) return '';
|
|
127
|
+
if (status === 'M') return ` ${hexToAnsi(theme.yellow)}M\x1b[0m`;
|
|
128
|
+
if (status === 'A') return ` ${hexToAnsi(theme.green)}A\x1b[0m`;
|
|
129
|
+
if (status === '?') return ` ${hexToAnsi(theme.textDim)}U\x1b[0m`;
|
|
130
|
+
if (status === 'D') return ` ${hexToAnsi(theme.red)}D\x1b[0m`;
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
|
|
85
134
|
// ── Session History Loading ─────────────────────────────────────────────
|
|
86
135
|
function loadSessions() {
|
|
87
136
|
const cwd = process.cwd();
|
|
88
137
|
const sessions = [];
|
|
89
138
|
|
|
90
|
-
// Claude sessions
|
|
91
139
|
const claudeProjectKey = cwd.replace(/\//g, '-');
|
|
92
140
|
const claudeDir = path.join(os.homedir(), '.claude', 'projects', claudeProjectKey);
|
|
93
141
|
if (fs.existsSync(claudeDir)) {
|
|
@@ -107,18 +155,11 @@ function loadSessions() {
|
|
|
107
155
|
}
|
|
108
156
|
} catch (e) {}
|
|
109
157
|
}
|
|
110
|
-
sessions.push({
|
|
111
|
-
type: 'claude',
|
|
112
|
-
id: f.replace('.jsonl', ''),
|
|
113
|
-
modified: stat.mtime,
|
|
114
|
-
summary: firstMsg || '(empty)',
|
|
115
|
-
messages: lines.length,
|
|
116
|
-
});
|
|
158
|
+
sessions.push({ type: 'claude', id: f.replace('.jsonl', ''), modified: stat.mtime, summary: firstMsg || '(empty)', messages: lines.length });
|
|
117
159
|
} catch (e) {}
|
|
118
160
|
}
|
|
119
161
|
}
|
|
120
162
|
|
|
121
|
-
// Codex sessions — scan for sessions whose cwd matches
|
|
122
163
|
const codexBaseDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
123
164
|
if (fs.existsSync(codexBaseDir)) {
|
|
124
165
|
try {
|
|
@@ -133,14 +174,7 @@ function loadSessions() {
|
|
|
133
174
|
const firstLine = fs.readFileSync(full, 'utf-8').split('\n')[0];
|
|
134
175
|
const meta = JSON.parse(firstLine);
|
|
135
176
|
if (meta.type === 'session_meta' && meta.payload?.cwd === cwd) {
|
|
136
|
-
sessions.push({
|
|
137
|
-
type: 'codex',
|
|
138
|
-
id: meta.payload.id,
|
|
139
|
-
modified: new Date(meta.payload.timestamp || st.mtime),
|
|
140
|
-
summary: `Codex ${meta.payload.cli_version || ''}`.trim(),
|
|
141
|
-
messages: 0,
|
|
142
|
-
filePath: full,
|
|
143
|
-
});
|
|
177
|
+
sessions.push({ type: 'codex', id: meta.payload.id, modified: new Date(meta.payload.timestamp || st.mtime), summary: `Codex ${meta.payload.cli_version || ''}`.trim(), messages: 0, filePath: full });
|
|
144
178
|
}
|
|
145
179
|
} catch (e) {}
|
|
146
180
|
}
|
|
@@ -149,7 +183,6 @@ function loadSessions() {
|
|
|
149
183
|
} catch (e) {}
|
|
150
184
|
}
|
|
151
185
|
|
|
152
|
-
// Sort newest first
|
|
153
186
|
sessions.sort((a, b) => b.modified - a.modified);
|
|
154
187
|
return sessions;
|
|
155
188
|
}
|
|
@@ -173,7 +206,7 @@ function formatAge(date) {
|
|
|
173
206
|
return `${days}d`;
|
|
174
207
|
}
|
|
175
208
|
|
|
176
|
-
// ── AI Session Pool State
|
|
209
|
+
// ── AI Session Pool State ───────────────────────────────────────────────
|
|
177
210
|
let liveSessions = [];
|
|
178
211
|
let activeSessionIdx = -1;
|
|
179
212
|
let viewMode = 'file';
|
|
@@ -194,8 +227,7 @@ const tree = grid.set(0, 0, 7, 3, contrib.tree, {
|
|
|
194
227
|
label: ' ⊟ EXPLORER ',
|
|
195
228
|
mouse: true,
|
|
196
229
|
style: {
|
|
197
|
-
fg: theme.text,
|
|
198
|
-
bg: theme.sidebar,
|
|
230
|
+
fg: theme.text, bg: theme.sidebar,
|
|
199
231
|
selected: { bg: theme.accentDim, fg: theme.textBright },
|
|
200
232
|
label: { fg: theme.accent }
|
|
201
233
|
},
|
|
@@ -206,18 +238,13 @@ const tree = grid.set(0, 0, 7, 3, contrib.tree, {
|
|
|
206
238
|
|
|
207
239
|
const historyList = grid.set(7, 0, 4, 3, blessed.list, {
|
|
208
240
|
label: ' ◆ SESSIONS ',
|
|
209
|
-
mouse: true,
|
|
210
|
-
|
|
211
|
-
tags: false,
|
|
212
|
-
scrollable: true,
|
|
213
|
-
alwaysScroll: true,
|
|
241
|
+
mouse: true, keys: true, tags: false,
|
|
242
|
+
scrollable: true, alwaysScroll: true,
|
|
214
243
|
scrollbar: { ch: '▐', style: { fg: theme.accentDim, bg: theme.sidebar } },
|
|
215
244
|
style: {
|
|
216
|
-
fg: theme.text,
|
|
217
|
-
bg: theme.sidebar,
|
|
245
|
+
fg: theme.text, bg: theme.sidebar,
|
|
218
246
|
selected: { bg: theme.accentDim, fg: theme.textBright },
|
|
219
|
-
label: { fg: theme.purple },
|
|
220
|
-
border: { fg: theme.border }
|
|
247
|
+
label: { fg: theme.purple }, border: { fg: theme.border }
|
|
221
248
|
},
|
|
222
249
|
border: { type: 'line', fg: theme.border },
|
|
223
250
|
padding: { left: 1 }
|
|
@@ -225,17 +252,10 @@ const historyList = grid.set(7, 0, 4, 3, blessed.list, {
|
|
|
225
252
|
|
|
226
253
|
const viewer = grid.set(0, 3, 11, 9, blessed.box, {
|
|
227
254
|
label: ' ◈ PREVIEW ',
|
|
228
|
-
mouse: true,
|
|
229
|
-
|
|
230
|
-
tags: false,
|
|
231
|
-
alwaysScroll: true,
|
|
232
|
-
scrollable: true,
|
|
255
|
+
mouse: true, keys: true, tags: false,
|
|
256
|
+
alwaysScroll: true, scrollable: true,
|
|
233
257
|
scrollbar: { ch: '▐', style: { fg: theme.accentDim, bg: theme.bg } },
|
|
234
|
-
style: {
|
|
235
|
-
fg: theme.text,
|
|
236
|
-
bg: theme.bg,
|
|
237
|
-
label: { fg: theme.accent }
|
|
238
|
-
},
|
|
258
|
+
style: { fg: theme.text, bg: theme.bg, label: { fg: theme.accent } },
|
|
239
259
|
border: { type: 'line', fg: theme.border },
|
|
240
260
|
padding: { left: 1 }
|
|
241
261
|
});
|
|
@@ -262,35 +282,271 @@ const question = blessed.question({
|
|
|
262
282
|
|
|
263
283
|
// ── AI Picker ───────────────────────────────────────────────────────────
|
|
264
284
|
const aiPicker = blessed.list({
|
|
265
|
-
parent: screen,
|
|
266
|
-
top: 'center',
|
|
267
|
-
left: 'center',
|
|
268
|
-
width: 42,
|
|
269
|
-
height: 8,
|
|
285
|
+
parent: screen, top: 'center', left: 'center', width: 42, height: 8,
|
|
270
286
|
label: ` ◆ Launch AI Assistant `,
|
|
271
|
-
mouse: true,
|
|
272
|
-
keys: true,
|
|
273
|
-
tags: false,
|
|
287
|
+
mouse: true, keys: true, tags: false,
|
|
274
288
|
border: { type: 'line', fg: theme.purple },
|
|
275
289
|
style: {
|
|
276
|
-
fg: theme.text,
|
|
277
|
-
bg: theme.sidebar,
|
|
290
|
+
fg: theme.text, bg: theme.sidebar,
|
|
278
291
|
selected: { bg: theme.accentDim, fg: theme.textBright },
|
|
279
|
-
label: { fg: theme.purple },
|
|
280
|
-
border: { fg: theme.purple }
|
|
292
|
+
label: { fg: theme.purple }, border: { fg: theme.purple }
|
|
281
293
|
},
|
|
282
294
|
padding: { left: 2, right: 2 },
|
|
283
|
-
items: [
|
|
284
|
-
`⬡ Claude Code`,
|
|
285
|
-
`⊞ OpenAI Codex`,
|
|
286
|
-
],
|
|
295
|
+
items: [`⬡ Claude Code`, `⊞ OpenAI Codex`],
|
|
287
296
|
hidden: true
|
|
288
297
|
});
|
|
289
298
|
|
|
299
|
+
// ── Command Palette (Ctrl+B) ────────────────────────────────────────────
|
|
300
|
+
const cmdPalette = blessed.list({
|
|
301
|
+
parent: screen, top: 'center', left: 'center', width: 56, height: 20,
|
|
302
|
+
label: ` ◆ Command Palette `,
|
|
303
|
+
mouse: true, keys: true, tags: false,
|
|
304
|
+
border: { type: 'line', fg: theme.accent },
|
|
305
|
+
style: {
|
|
306
|
+
fg: theme.text, bg: theme.sidebar,
|
|
307
|
+
selected: { bg: theme.accentDim, fg: theme.textBright },
|
|
308
|
+
label: { fg: theme.accent }, border: { fg: theme.accent }
|
|
309
|
+
},
|
|
310
|
+
padding: { left: 2, right: 2 },
|
|
311
|
+
hidden: true
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const cmdActions = [
|
|
315
|
+
{ label: `${hexToAnsi(theme.purple)}◆\x1b[0m Launch AI Assistant`, key: 'a', action: () => showAIPicker() },
|
|
316
|
+
{ label: `${hexToAnsi(theme.accent)}◆\x1b[0m Switch Active Session`, key: 'Ctrl+A', action: () => { if (liveSessions.length > 0) showSessionSwitcher(); } },
|
|
317
|
+
{ label: `${hexToAnsi(theme.cyan)}⊹\x1b[0m Search Files`, key: 'Ctrl+F', action: () => showSearch() },
|
|
318
|
+
{ label: `${hexToAnsi(theme.green)}⊡\x1b[0m New File`, key: 'n', action: () => doNewFile() },
|
|
319
|
+
{ label: `${hexToAnsi(theme.green)}▸\x1b[0m New Folder`, key: 'f', action: () => doNewFolder() },
|
|
320
|
+
{ label: `${hexToAnsi(theme.red)}⊘\x1b[0m Delete Selected`, key: 'd', action: () => doDelete() },
|
|
321
|
+
{ label: `${hexToAnsi(theme.yellow)}↻\x1b[0m Refresh All`, key: 'r', action: () => doRefresh() },
|
|
322
|
+
{ label: `${hexToAnsi(theme.cyan)}⎘\x1b[0m Copy File Path`, key: 'c', action: () => doCopyPath() },
|
|
323
|
+
{ label: `${hexToAnsi(theme.orange)}⊡\x1b[0m Open in $EDITOR`, key: 'e', action: () => doOpenEditor() },
|
|
324
|
+
{ label: `${hexToAnsi('#292e42')}─────────── Git ───────────\x1b[0m`, action: null },
|
|
325
|
+
{ label: `${hexToAnsi(theme.green)}⊙\x1b[0m Git Status`, action: () => runGitCommand('git status') },
|
|
326
|
+
{ label: `${hexToAnsi(theme.yellow)}⊙\x1b[0m Git Diff`, action: () => runGitCommand('git diff --stat') },
|
|
327
|
+
{ label: `${hexToAnsi(theme.cyan)}⊙\x1b[0m Git Log (last 20)`, action: () => runGitCommand('git log --oneline -20') },
|
|
328
|
+
{ label: `${hexToAnsi(theme.purple)}⊙\x1b[0m Git Branches`, action: () => runGitCommand('git branch -a') },
|
|
329
|
+
{ label: `${hexToAnsi(theme.orange)}⊙\x1b[0m Git Stash List`, action: () => runGitCommand('git stash list') },
|
|
330
|
+
{ label: `${hexToAnsi('#292e42')}─────────── View ──────────\x1b[0m`, action: null },
|
|
331
|
+
{ label: `${hexToAnsi(theme.cyan)}⊞\x1b[0m Disk Usage (du)`, action: () => runGitCommand('du -sh * | sort -rh | head -20') },
|
|
332
|
+
{ label: `${hexToAnsi(theme.green)}⊞\x1b[0m Package Info`, action: () => showPackageInfo() },
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
function showCmdPalette() {
|
|
336
|
+
cmdPalette.setItems(cmdActions.map(a => ` ${a.label}`));
|
|
337
|
+
cmdPalette.select(0);
|
|
338
|
+
cmdPalette.show();
|
|
339
|
+
cmdPalette.focus();
|
|
340
|
+
screen.render();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
cmdPalette.on('select', (item, index) => {
|
|
344
|
+
cmdPalette.hide();
|
|
345
|
+
screen.render();
|
|
346
|
+
const action = cmdActions[index];
|
|
347
|
+
if (action && action.action) action.action();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
cmdPalette.key(['escape', 'q', 'C-b'], () => {
|
|
351
|
+
cmdPalette.hide();
|
|
352
|
+
screen.render();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ── Command Palette Actions ─────────────────────────────────────────────
|
|
356
|
+
function runGitCommand(cmd) {
|
|
357
|
+
viewMode = 'file';
|
|
358
|
+
lastPath = null;
|
|
359
|
+
viewer.scrollTo(0);
|
|
360
|
+
screen.realloc();
|
|
361
|
+
try {
|
|
362
|
+
const out = execSync(cmd, { cwd: process.cwd(), encoding: 'utf-8', timeout: 5000 });
|
|
363
|
+
const highlighted = cmd.startsWith('git diff') ? out : out;
|
|
364
|
+
viewer.setContent(addLineNumbers(highlighted));
|
|
365
|
+
viewer.setLabel(` ${hexToAnsi(theme.green)}⊙\x1b[0m ${cmd} `);
|
|
366
|
+
} catch (e) {
|
|
367
|
+
viewer.setContent(`${hexToAnsi(theme.red)}⊘ Error:\x1b[0m ${e.message}`);
|
|
368
|
+
viewer.setLabel(` ${hexToAnsi(theme.red)}⊘\x1b[0m ${cmd} `);
|
|
369
|
+
}
|
|
370
|
+
screen.render();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function showPackageInfo() {
|
|
374
|
+
viewMode = 'file';
|
|
375
|
+
lastPath = null;
|
|
376
|
+
viewer.scrollTo(0);
|
|
377
|
+
screen.realloc();
|
|
378
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
379
|
+
if (!fs.existsSync(pkgPath)) {
|
|
380
|
+
viewer.setContent(`\n ${hexToAnsi(theme.textDim)}No package.json found\x1b[0m`);
|
|
381
|
+
viewer.setLabel(` ${hexToAnsi(theme.textDim)}⊡\x1b[0m No package.json `);
|
|
382
|
+
screen.render();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
387
|
+
const dim = hexToAnsi(theme.textDim);
|
|
388
|
+
const accent = hexToAnsi(theme.accent);
|
|
389
|
+
const cyan = hexToAnsi(theme.cyan);
|
|
390
|
+
const green = hexToAnsi(theme.green);
|
|
391
|
+
const yellow = hexToAnsi(theme.yellow);
|
|
392
|
+
let content = `\n ${accent}\x1b[1m${pkg.name || '(unnamed)'}\x1b[0m ${dim}v${pkg.version || '0.0.0'}\x1b[0m\n`;
|
|
393
|
+
if (pkg.description) content += ` ${dim}${pkg.description}\x1b[0m\n`;
|
|
394
|
+
content += `\n`;
|
|
395
|
+
if (pkg.scripts) {
|
|
396
|
+
content += ` ${cyan}\x1b[1mScripts:\x1b[0m\n`;
|
|
397
|
+
for (const [k, v] of Object.entries(pkg.scripts)) {
|
|
398
|
+
content += ` ${green}${k}\x1b[0m ${dim}→ ${v}\x1b[0m\n`;
|
|
399
|
+
}
|
|
400
|
+
content += '\n';
|
|
401
|
+
}
|
|
402
|
+
if (pkg.dependencies) {
|
|
403
|
+
content += ` ${yellow}\x1b[1mDependencies (${Object.keys(pkg.dependencies).length}):\x1b[0m\n`;
|
|
404
|
+
for (const [k, v] of Object.entries(pkg.dependencies)) {
|
|
405
|
+
content += ` ${k} ${dim}${v}\x1b[0m\n`;
|
|
406
|
+
}
|
|
407
|
+
content += '\n';
|
|
408
|
+
}
|
|
409
|
+
if (pkg.devDependencies) {
|
|
410
|
+
content += ` ${dim}\x1b[1mDev Dependencies (${Object.keys(pkg.devDependencies).length}):\x1b[0m\n`;
|
|
411
|
+
for (const [k, v] of Object.entries(pkg.devDependencies)) {
|
|
412
|
+
content += ` ${dim}${k} ${v}\x1b[0m\n`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
viewer.setContent(content);
|
|
416
|
+
viewer.setLabel(` ${hexToAnsi(theme.green)}⬢\x1b[0m ${pkg.name} `);
|
|
417
|
+
} catch (e) {
|
|
418
|
+
viewer.setContent(`${hexToAnsi(theme.red)}⊘ Error:\x1b[0m ${e.message}`);
|
|
419
|
+
}
|
|
420
|
+
screen.render();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function doCopyPath() {
|
|
424
|
+
const node = tree.nodeLines && tree.nodeLines[tree.rows.selected];
|
|
425
|
+
if (!node || !node.path) return;
|
|
426
|
+
try {
|
|
427
|
+
const platform = os.platform();
|
|
428
|
+
if (platform === 'darwin') execSync(`echo -n "${node.path}" | pbcopy`);
|
|
429
|
+
else if (platform === 'linux') execSync(`echo -n "${node.path}" | xclip -selection clipboard 2>/dev/null || echo -n "${node.path}" | xsel --clipboard 2>/dev/null`);
|
|
430
|
+
viewer.setLabel(` ${hexToAnsi(theme.green)}⎘ Copied!\x1b[0m `);
|
|
431
|
+
setTimeout(() => { if (lastPath) updateViewer(lastPath); screen.render(); }, 1500);
|
|
432
|
+
} catch (e) {}
|
|
433
|
+
screen.render();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function doOpenEditor() {
|
|
437
|
+
const node = tree.nodeLines && tree.nodeLines[tree.rows.selected];
|
|
438
|
+
if (!node || !node.path) return;
|
|
439
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
440
|
+
try {
|
|
441
|
+
screen.destroy();
|
|
442
|
+
execSync(`${editor} "${node.path}"`, { stdio: 'inherit' });
|
|
443
|
+
} catch (e) {}
|
|
444
|
+
// Restart screen after editor closes
|
|
445
|
+
process.exit(0);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function doNewFile() {
|
|
449
|
+
prompt.input('New file name:', '', (err, value) => {
|
|
450
|
+
if (value) { try { fs.ensureFileSync(path.join(process.cwd(), value)); refreshTree(); } catch (e) {} }
|
|
451
|
+
tree.rows.focus();
|
|
452
|
+
screen.render();
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function doNewFolder() {
|
|
457
|
+
prompt.input('New folder name:', '', (err, value) => {
|
|
458
|
+
if (value) { try { fs.ensureDirSync(path.join(process.cwd(), value)); refreshTree(); } catch (e) {} }
|
|
459
|
+
tree.rows.focus();
|
|
460
|
+
screen.render();
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function doDelete() {
|
|
465
|
+
const node = tree.nodeLines && tree.nodeLines[tree.rows.selected];
|
|
466
|
+
if (node && node.path) {
|
|
467
|
+
question.ask(`Delete ${path.basename(node.path)}? (y/n)`, (err, data) => {
|
|
468
|
+
if (data) { try { fs.removeSync(node.path); lastPath = null; viewer.setContent(''); refreshTree(); } catch (e) {} }
|
|
469
|
+
tree.rows.focus();
|
|
470
|
+
screen.render();
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function doRefresh() {
|
|
476
|
+
lastPath = null;
|
|
477
|
+
screen.realloc();
|
|
478
|
+
refreshGitStatus();
|
|
479
|
+
refreshTree();
|
|
480
|
+
refreshHistory(true);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ── Search (Ctrl+F) ────────────────────────────────────────────────────
|
|
484
|
+
function showSearch() {
|
|
485
|
+
prompt.input('Search files:', '', (err, query) => {
|
|
486
|
+
if (!query) { tree.rows.focus(); screen.render(); return; }
|
|
487
|
+
const results = [];
|
|
488
|
+
const q = query.toLowerCase();
|
|
489
|
+
function walk(dir, depth) {
|
|
490
|
+
if (depth > 5) return;
|
|
491
|
+
try {
|
|
492
|
+
const items = fs.readdirSync(dir);
|
|
493
|
+
for (const item of items) {
|
|
494
|
+
if (item.startsWith('.') || item === 'node_modules' || item === 'dist') continue;
|
|
495
|
+
const full = path.join(dir, item);
|
|
496
|
+
if (item.toLowerCase().includes(q)) results.push(full);
|
|
497
|
+
try { if (fs.statSync(full).isDirectory()) walk(full, depth + 1); } catch (e) {}
|
|
498
|
+
}
|
|
499
|
+
} catch (e) {}
|
|
500
|
+
}
|
|
501
|
+
walk(process.cwd(), 0);
|
|
502
|
+
|
|
503
|
+
if (results.length === 0) {
|
|
504
|
+
viewer.setContent(`\n ${hexToAnsi(theme.textDim)}No results for "${query}"\x1b[0m`);
|
|
505
|
+
viewer.setLabel(` ${hexToAnsi(theme.cyan)}⊹\x1b[0m Search: ${query} `);
|
|
506
|
+
screen.render();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Show results in a picker
|
|
511
|
+
const searchPicker = blessed.list({
|
|
512
|
+
parent: screen, top: 'center', left: 'center',
|
|
513
|
+
width: '60%', height: Math.min(results.length + 4, 20),
|
|
514
|
+
label: ` ⊹ ${results.length} results for "${query}" `,
|
|
515
|
+
mouse: true, keys: true, tags: false,
|
|
516
|
+
border: { type: 'line', fg: theme.cyan },
|
|
517
|
+
style: {
|
|
518
|
+
fg: theme.text, bg: theme.sidebar,
|
|
519
|
+
selected: { bg: theme.accentDim, fg: theme.textBright },
|
|
520
|
+
label: { fg: theme.cyan }, border: { fg: theme.cyan }
|
|
521
|
+
},
|
|
522
|
+
padding: { left: 1, right: 1 },
|
|
523
|
+
items: results.map(r => {
|
|
524
|
+
const rel = path.relative(process.cwd(), r);
|
|
525
|
+
const isDir = fs.statSync(r).isDirectory();
|
|
526
|
+
return `${getFileIcon(path.basename(r), isDir)} ${rel}`;
|
|
527
|
+
})
|
|
528
|
+
});
|
|
529
|
+
searchPicker.focus();
|
|
530
|
+
screen.render();
|
|
531
|
+
|
|
532
|
+
searchPicker.on('select', (item, idx) => {
|
|
533
|
+
searchPicker.destroy();
|
|
534
|
+
viewMode = 'file';
|
|
535
|
+
updateViewer(results[idx]);
|
|
536
|
+
screen.render();
|
|
537
|
+
});
|
|
538
|
+
searchPicker.key(['escape', 'q'], () => {
|
|
539
|
+
searchPicker.destroy();
|
|
540
|
+
tree.rows.focus();
|
|
541
|
+
screen.render();
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
290
546
|
// ── Session History ─────────────────────────────────────────────────────
|
|
291
|
-
let sessionData = [];
|
|
292
|
-
let historyLineMap = [];
|
|
293
|
-
let sessionsLoaded = false;
|
|
547
|
+
let sessionData = [];
|
|
548
|
+
let historyLineMap = [];
|
|
549
|
+
let sessionsLoaded = false;
|
|
294
550
|
|
|
295
551
|
function refreshHistory(forceReload) {
|
|
296
552
|
const prevSelected = historyList.selected || 0;
|
|
@@ -304,30 +560,20 @@ function refreshHistory(forceReload) {
|
|
|
304
560
|
const items = [];
|
|
305
561
|
historyLineMap = [];
|
|
306
562
|
|
|
307
|
-
// Claude section
|
|
308
563
|
items.push(`${hexToAnsi(theme.orange)}\x1b[1m◆ Claude\x1b[0m`);
|
|
309
564
|
historyLineMap.push(null);
|
|
310
565
|
if (claudeSessions.length > 0) {
|
|
311
|
-
for (const s of claudeSessions) {
|
|
312
|
-
items.push(` ${formatSessionItem(s)}`);
|
|
313
|
-
historyLineMap.push(s);
|
|
314
|
-
}
|
|
566
|
+
for (const s of claudeSessions) { items.push(` ${formatSessionItem(s)}`); historyLineMap.push(s); }
|
|
315
567
|
} else {
|
|
316
|
-
items.push(` ${hexToAnsi(theme.textDim)}(none)\x1b[0m`);
|
|
317
|
-
historyLineMap.push(null);
|
|
568
|
+
items.push(` ${hexToAnsi(theme.textDim)}(none)\x1b[0m`); historyLineMap.push(null);
|
|
318
569
|
}
|
|
319
570
|
|
|
320
|
-
// Codex section
|
|
321
571
|
items.push(`${hexToAnsi(theme.green)}\x1b[1m⊞ Codex\x1b[0m`);
|
|
322
572
|
historyLineMap.push(null);
|
|
323
573
|
if (codexSessions.length > 0) {
|
|
324
|
-
for (const s of codexSessions) {
|
|
325
|
-
items.push(` ${formatSessionItem(s)}`);
|
|
326
|
-
historyLineMap.push(s);
|
|
327
|
-
}
|
|
574
|
+
for (const s of codexSessions) { items.push(` ${formatSessionItem(s)}`); historyLineMap.push(s); }
|
|
328
575
|
} else {
|
|
329
|
-
items.push(` ${hexToAnsi(theme.textDim)}(none)\x1b[0m`);
|
|
330
|
-
historyLineMap.push(null);
|
|
576
|
+
items.push(` ${hexToAnsi(theme.textDim)}(none)\x1b[0m`); historyLineMap.push(null);
|
|
331
577
|
}
|
|
332
578
|
|
|
333
579
|
historyList.setItems(items);
|
|
@@ -335,15 +581,11 @@ function refreshHistory(forceReload) {
|
|
|
335
581
|
screen.render();
|
|
336
582
|
}
|
|
337
583
|
|
|
338
|
-
// Resume a session from the history list
|
|
339
584
|
historyList.on('select', (item, index) => {
|
|
340
585
|
const session = historyLineMap[index];
|
|
341
|
-
if (!session) return;
|
|
342
|
-
if (session.type === 'claude')
|
|
343
|
-
|
|
344
|
-
} else {
|
|
345
|
-
launchAIWithArgs('codex', ['resume', session.id], 'OpenAI Codex');
|
|
346
|
-
}
|
|
586
|
+
if (!session) return;
|
|
587
|
+
if (session.type === 'claude') launchAIWithArgs('claude', ['--resume', session.id], 'Claude Code');
|
|
588
|
+
else launchAIWithArgs('codex', ['resume', session.id], 'OpenAI Codex');
|
|
347
589
|
});
|
|
348
590
|
|
|
349
591
|
// ── File Tree Logic ─────────────────────────────────────────────────────
|
|
@@ -364,7 +606,8 @@ function getFiles(dir) {
|
|
|
364
606
|
const fullPath = path.join(dir, item);
|
|
365
607
|
let isDir = false;
|
|
366
608
|
try { isDir = fs.statSync(fullPath).isDirectory(); } catch (e) { return; }
|
|
367
|
-
const
|
|
609
|
+
const gitInd = getGitIndicator(fullPath);
|
|
610
|
+
const displayName = `${getFileIcon(item, isDir)} ${item}${gitInd}`;
|
|
368
611
|
|
|
369
612
|
if (isDir) {
|
|
370
613
|
result.children[displayName] = { name: displayName, path: fullPath, children: {} };
|
|
@@ -374,7 +617,8 @@ function getFiles(dir) {
|
|
|
374
617
|
if (!sub.startsWith('.')) {
|
|
375
618
|
try {
|
|
376
619
|
const subStat = fs.statSync(path.join(fullPath, sub));
|
|
377
|
-
const
|
|
620
|
+
const subGit = getGitIndicator(path.join(fullPath, sub));
|
|
621
|
+
const subDisplayName = `${getFileIcon(sub, subStat.isDirectory())} ${sub}${subGit}`;
|
|
378
622
|
result.children[displayName].children[subDisplayName] = { name: subDisplayName, path: path.join(fullPath, sub) };
|
|
379
623
|
} catch (e) {}
|
|
380
624
|
}
|
|
@@ -409,6 +653,19 @@ function addLineNumbers(text) {
|
|
|
409
653
|
}).join('\n');
|
|
410
654
|
}
|
|
411
655
|
|
|
656
|
+
// ── Breadcrumb ──────────────────────────────────────────────────────────
|
|
657
|
+
function getBreadcrumb(filePath) {
|
|
658
|
+
const rel = path.relative(process.cwd(), filePath);
|
|
659
|
+
const parts = rel.split(path.sep);
|
|
660
|
+
const dim = hexToAnsi(theme.textDim);
|
|
661
|
+
const accent = hexToAnsi(theme.accent);
|
|
662
|
+
const sep = `${dim} > \x1b[0m`;
|
|
663
|
+
return parts.map((p, i) => {
|
|
664
|
+
if (i === parts.length - 1) return `${accent}\x1b[1m${p}\x1b[0m`;
|
|
665
|
+
return `${dim}${p}\x1b[0m`;
|
|
666
|
+
}).join(sep);
|
|
667
|
+
}
|
|
668
|
+
|
|
412
669
|
// ── Viewer Update ───────────────────────────────────────────────────────
|
|
413
670
|
let lastPath = null;
|
|
414
671
|
function updateViewer(nodePath) {
|
|
@@ -426,16 +683,18 @@ function updateViewer(nodePath) {
|
|
|
426
683
|
const content = fs.readFileSync(nodePath, 'utf-8');
|
|
427
684
|
const ext = path.extname(nodePath).slice(1);
|
|
428
685
|
const lineCount = content.split('\n').length;
|
|
686
|
+
const breadcrumb = getBreadcrumb(nodePath);
|
|
429
687
|
const highlighted = highlight(content, { language: ext, ignoreIllegals: true });
|
|
430
|
-
viewer.setContent(addLineNumbers(highlighted));
|
|
688
|
+
viewer.setContent(`${breadcrumb}\n${hexToAnsi('#292e42')}${'─'.repeat(60)}\x1b[0m\n${addLineNumbers(highlighted)}`);
|
|
431
689
|
const icon = getFileIcon(path.basename(nodePath), false);
|
|
432
690
|
const dim = hexToAnsi('#565f89');
|
|
433
|
-
|
|
691
|
+
const gitInd = getGitIndicator(nodePath);
|
|
692
|
+
viewer.setLabel(` ${icon} ${path.basename(nodePath)}${gitInd} ${dim}${lineCount} lines │ ${sizeKB} KB\x1b[0m `);
|
|
434
693
|
} else if (stat.isDirectory()) {
|
|
435
694
|
const items = fs.readdirSync(nodePath);
|
|
436
695
|
const dirIcon = `${hexToAnsi(theme.accent)}▾ \x1b[0m`;
|
|
437
|
-
|
|
438
|
-
content
|
|
696
|
+
const breadcrumb = getBreadcrumb(nodePath);
|
|
697
|
+
let content = `${breadcrumb}\n${hexToAnsi('#292e42')}${'─'.repeat(60)}\x1b[0m\n\n`;
|
|
439
698
|
const dirItems = items.filter(i => !i.startsWith('.')).sort((a, b) => {
|
|
440
699
|
try {
|
|
441
700
|
const aIsDir = fs.statSync(path.join(nodePath, a)).isDirectory();
|
|
@@ -448,7 +707,8 @@ function updateViewer(nodePath) {
|
|
|
448
707
|
dirItems.forEach(item => {
|
|
449
708
|
let subIsDir = false;
|
|
450
709
|
try { subIsDir = fs.statSync(path.join(nodePath, item)).isDirectory(); } catch (e) {}
|
|
451
|
-
|
|
710
|
+
const gitInd = getGitIndicator(path.join(nodePath, item));
|
|
711
|
+
content += ` ${getFileIcon(item, subIsDir)} ${item}${gitInd}\n`;
|
|
452
712
|
});
|
|
453
713
|
if (dirItems.length === 0) content += ` ${hexToAnsi('#565f89')}(Empty directory)\x1b[0m`;
|
|
454
714
|
viewer.setContent(content);
|
|
@@ -462,20 +722,72 @@ function updateViewer(nodePath) {
|
|
|
462
722
|
screen.render();
|
|
463
723
|
}
|
|
464
724
|
|
|
725
|
+
// ── Welcome Screen ──────────────────────────────────────────────────────
|
|
726
|
+
function showWelcome() {
|
|
727
|
+
const dim = hexToAnsi(theme.textDim);
|
|
728
|
+
const accent = hexToAnsi(theme.accent);
|
|
729
|
+
const cyan = hexToAnsi(theme.cyan);
|
|
730
|
+
const purple = hexToAnsi(theme.purple);
|
|
731
|
+
const green = hexToAnsi(theme.green);
|
|
732
|
+
const orange = hexToAnsi(theme.orange);
|
|
733
|
+
const yellow = hexToAnsi(theme.yellow);
|
|
734
|
+
const bright = hexToAnsi(theme.textBright);
|
|
735
|
+
const border = hexToAnsi('#292e42');
|
|
736
|
+
|
|
737
|
+
const logo = `
|
|
738
|
+
${accent}████████╗${cyan}██████╗ ${purple}██████╗\x1b[0m
|
|
739
|
+
${accent}╚══██╔══╝${cyan}██╔══██╗${purple}██╔════╝\x1b[0m
|
|
740
|
+
${accent} ██║ ${cyan}██████╔╝${purple}██║ \x1b[0m
|
|
741
|
+
${accent} ██║ ${cyan}██╔═══╝ ${purple}██║ \x1b[0m
|
|
742
|
+
${accent} ██║ ${cyan}██║ ${purple}╚██████╗\x1b[0m
|
|
743
|
+
${accent} ╚═╝ ${cyan}╚═╝ ${purple} ╚═════╝\x1b[0m
|
|
744
|
+
|
|
745
|
+
${bright}\x1b[1mExplorer Pro\x1b[0m ${dim}v${require('./package.json').version}\x1b[0m
|
|
746
|
+
`;
|
|
747
|
+
|
|
748
|
+
const gitLine = isGitRepo ? ` ${green}⊙\x1b[0m Branch: ${accent}${gitBranch}\x1b[0m ${dim}│\x1b[0m ${Object.keys(gitStatusMap).length} changes` : ` ${dim}Not a git repository\x1b[0m`;
|
|
749
|
+
|
|
750
|
+
const shortcuts = `
|
|
751
|
+
${border}${'─'.repeat(44)}\x1b[0m
|
|
752
|
+
|
|
753
|
+
${bright}\x1b[1mQuick Start\x1b[0m
|
|
754
|
+
|
|
755
|
+
${accent}Ctrl+T\x1b[0m Switch panels ${accent}a\x1b[0m Launch AI
|
|
756
|
+
${accent}Ctrl+A\x1b[0m Switch AI sessions ${accent}Ctrl+F\x1b[0m Search files
|
|
757
|
+
${accent}Ctrl+B\x1b[0m Command palette ${accent}Ctrl+D\x1b[0m Quit all
|
|
758
|
+
|
|
759
|
+
${bright}\x1b[1mFiles\x1b[0m
|
|
760
|
+
|
|
761
|
+
${green}n\x1b[0m New file ${green}f\x1b[0m New folder ${orange}d\x1b[0m Delete
|
|
762
|
+
${cyan}e\x1b[0m Open in $EDITOR ${cyan}c\x1b[0m Copy path
|
|
763
|
+
${yellow}r\x1b[0m Refresh
|
|
764
|
+
|
|
765
|
+
${border}${'─'.repeat(44)}\x1b[0m
|
|
766
|
+
|
|
767
|
+
${gitLine}
|
|
768
|
+
${dim}${process.cwd()}\x1b[0m
|
|
769
|
+
`;
|
|
770
|
+
|
|
771
|
+
viewer.setContent(logo + shortcuts);
|
|
772
|
+
viewer.setLabel(` ${accent}◈ Welcome\x1b[0m `);
|
|
773
|
+
screen.render();
|
|
774
|
+
}
|
|
775
|
+
|
|
465
776
|
// ── Status Bar ──────────────────────────────────────────────────────────
|
|
466
777
|
function updateStatusBar(mode) {
|
|
467
778
|
const sep = `${hexToAnsi('#292e42')}│\x1b[0m`;
|
|
468
779
|
const key = (k) => `\x1b[1m${hexToAnsi(theme.accent)}${k}\x1b[0m`;
|
|
469
780
|
const sessionCount = liveSessions.length;
|
|
470
781
|
const sessionHint = sessionCount > 0 ? ` ${sep} ${key('Ctrl-A')} Sessions (${sessionCount})` : '';
|
|
471
|
-
const
|
|
782
|
+
const gitHint = isGitRepo ? ` ${sep} ${hexToAnsi(theme.green)}⊙ ${gitBranch}\x1b[0m` : '';
|
|
783
|
+
const quit = `${key('Ctrl-D')} ${hexToAnsi(theme.red)}Quit\x1b[0m`;
|
|
472
784
|
if (mode === 'ai') {
|
|
473
785
|
statusBar.setContent(
|
|
474
|
-
` ${key('Ctrl-C')} Stop ${sep} ${key('Ctrl-T')} Switch ${sep} ${key('Ctrl-A')} Sessions (${sessionCount}) ${sep} ${quit}`
|
|
786
|
+
` ${key('Ctrl-C')} Stop ${sep} ${key('Ctrl-T')} Switch ${sep} ${key('Ctrl-A')} Sessions (${sessionCount}) ${sep} ${key('Ctrl-B')} Cmds ${sep} ${quit}`
|
|
475
787
|
);
|
|
476
788
|
} else {
|
|
477
789
|
statusBar.setContent(
|
|
478
|
-
` ${key('
|
|
790
|
+
` ${key('Ctrl-T')} Switch ${sep} ${key('Ctrl-B')} Cmds ${sep} ${key('Ctrl-F')} Find ${sep} ${key('a')} AI${sessionHint}${gitHint} ${sep} ${quit}`
|
|
479
791
|
);
|
|
480
792
|
}
|
|
481
793
|
}
|
|
@@ -510,26 +822,15 @@ function getXterm() {
|
|
|
510
822
|
getXterm();
|
|
511
823
|
|
|
512
824
|
// ── AI Session Pool ─────────────────────────────────────────────────────
|
|
513
|
-
// Each live session: { id, cmd, label, color, pty, xterm, serialize, ready, renderTimer }
|
|
514
825
|
|
|
515
826
|
function getActiveSession() {
|
|
516
827
|
if (activeSessionIdx >= 0 && activeSessionIdx < liveSessions.length) return liveSessions[activeSessionIdx];
|
|
517
828
|
return null;
|
|
518
829
|
}
|
|
519
830
|
|
|
520
|
-
function showAIPicker() {
|
|
521
|
-
|
|
522
|
-
aiPicker.focus();
|
|
523
|
-
screen.render();
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function hideAIPicker() {
|
|
527
|
-
aiPicker.hide();
|
|
528
|
-
tree.rows.focus();
|
|
529
|
-
screen.render();
|
|
530
|
-
}
|
|
831
|
+
function showAIPicker() { aiPicker.show(); aiPicker.focus(); screen.render(); }
|
|
832
|
+
function hideAIPicker() { aiPicker.hide(); tree.rows.focus(); screen.render(); }
|
|
531
833
|
|
|
532
|
-
// Switch viewer to show a live session by index
|
|
533
834
|
function switchToSession(idx) {
|
|
534
835
|
if (idx < 0 || idx >= liveSessions.length) return;
|
|
535
836
|
activeSessionIdx = idx;
|
|
@@ -538,9 +839,7 @@ function switchToSession(idx) {
|
|
|
538
839
|
const s = liveSessions[idx];
|
|
539
840
|
viewer.setLabel(` ${hexToAnsi(s.color)}◆ ${s.label}\x1b[0m `);
|
|
540
841
|
viewer.style.border.fg = s.color;
|
|
541
|
-
if (s.xterm && s.serialize)
|
|
542
|
-
viewer.setContent(cleanForBlessed(s.serialize.serialize()));
|
|
543
|
-
}
|
|
842
|
+
if (s.xterm && s.serialize) viewer.setContent(cleanForBlessed(s.serialize.serialize()));
|
|
544
843
|
updateStatusBar('ai');
|
|
545
844
|
focusIndex = panels.indexOf('viewer');
|
|
546
845
|
tree.style.border.fg = theme.border;
|
|
@@ -551,22 +850,14 @@ function switchToSession(idx) {
|
|
|
551
850
|
|
|
552
851
|
// ── Session Switcher Popup (Ctrl+A) ────────────────────────────────────
|
|
553
852
|
const sessionSwitcher = blessed.list({
|
|
554
|
-
parent: screen,
|
|
555
|
-
top: 'center',
|
|
556
|
-
left: 'center',
|
|
557
|
-
width: 50,
|
|
558
|
-
height: 10,
|
|
853
|
+
parent: screen, top: 'center', left: 'center', width: 50, height: 10,
|
|
559
854
|
label: ` ◆ Active Sessions `,
|
|
560
|
-
mouse: true,
|
|
561
|
-
keys: true,
|
|
562
|
-
tags: false,
|
|
855
|
+
mouse: true, keys: true, tags: false,
|
|
563
856
|
border: { type: 'line', fg: theme.accent },
|
|
564
857
|
style: {
|
|
565
|
-
fg: theme.text,
|
|
566
|
-
bg: theme.sidebar,
|
|
858
|
+
fg: theme.text, bg: theme.sidebar,
|
|
567
859
|
selected: { bg: theme.accentDim, fg: theme.textBright },
|
|
568
|
-
label: { fg: theme.accent },
|
|
569
|
-
border: { fg: theme.accent }
|
|
860
|
+
label: { fg: theme.accent }, border: { fg: theme.accent }
|
|
570
861
|
},
|
|
571
862
|
padding: { left: 2, right: 2 },
|
|
572
863
|
hidden: true
|
|
@@ -585,17 +876,9 @@ function showSessionSwitcher() {
|
|
|
585
876
|
screen.render();
|
|
586
877
|
}
|
|
587
878
|
|
|
588
|
-
sessionSwitcher.on('select', (item, index) => {
|
|
589
|
-
|
|
590
|
-
switchToSession(index);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
sessionSwitcher.key(['escape', 'q', 'C-a'], () => {
|
|
594
|
-
sessionSwitcher.hide();
|
|
595
|
-
screen.render();
|
|
596
|
-
});
|
|
879
|
+
sessionSwitcher.on('select', (item, index) => { sessionSwitcher.hide(); switchToSession(index); });
|
|
880
|
+
sessionSwitcher.key(['escape', 'q', 'C-a'], () => { sessionSwitcher.hide(); screen.render(); });
|
|
597
881
|
|
|
598
|
-
// Launch AI: new session
|
|
599
882
|
async function launchAI(choice) {
|
|
600
883
|
hideAIPicker();
|
|
601
884
|
const cmd = choice === 0 ? 'claude' : 'codex';
|
|
@@ -603,10 +886,8 @@ async function launchAI(choice) {
|
|
|
603
886
|
await launchAIWithArgs(cmd, [], label);
|
|
604
887
|
}
|
|
605
888
|
|
|
606
|
-
// Launch AI: with arbitrary args (used for both new and resume)
|
|
607
889
|
async function launchAIWithArgs(cmd, args, label) {
|
|
608
890
|
const cwd = process.cwd();
|
|
609
|
-
|
|
610
891
|
viewMode = 'ai';
|
|
611
892
|
lastPath = null;
|
|
612
893
|
|
|
@@ -615,8 +896,17 @@ async function launchAIWithArgs(cmd, args, label) {
|
|
|
615
896
|
screen.realloc();
|
|
616
897
|
|
|
617
898
|
const headerColor = cmd === 'claude' ? theme.orange : theme.green;
|
|
899
|
+
|
|
900
|
+
// Show spinner while loading
|
|
618
901
|
viewer.setLabel(` ${hexToAnsi(headerColor)}◆ ${label}\x1b[0m `);
|
|
619
902
|
viewer.style.border.fg = headerColor;
|
|
903
|
+
const spinChars = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
904
|
+
let spinIdx = 0;
|
|
905
|
+
const spinner = setInterval(() => {
|
|
906
|
+
viewer.setContent(`\n\n ${hexToAnsi(headerColor)}${spinChars[spinIdx++ % spinChars.length]}\x1b[0m Starting ${label}...`);
|
|
907
|
+
screen.render();
|
|
908
|
+
}, 80);
|
|
909
|
+
|
|
620
910
|
updateStatusBar('ai');
|
|
621
911
|
screen.render();
|
|
622
912
|
|
|
@@ -632,6 +922,7 @@ async function launchAIWithArgs(cmd, args, label) {
|
|
|
632
922
|
try {
|
|
633
923
|
fullCmd = execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
|
|
634
924
|
} catch (e) {
|
|
925
|
+
clearInterval(spinner);
|
|
635
926
|
viewer.setContent(`\n ${hexToAnsi(theme.red)}⊘ Error: "${cmd}" not found in PATH.\x1b[0m\n\n Install it first, then try again.\n`);
|
|
636
927
|
viewMode = 'file';
|
|
637
928
|
xterm.dispose();
|
|
@@ -643,12 +934,11 @@ async function launchAIWithArgs(cmd, args, label) {
|
|
|
643
934
|
let ptyProc;
|
|
644
935
|
try {
|
|
645
936
|
ptyProc = pty.spawn(fullCmd, args, {
|
|
646
|
-
name: 'xterm-256color',
|
|
647
|
-
cols, rows,
|
|
648
|
-
cwd: cwd,
|
|
937
|
+
name: 'xterm-256color', cols, rows, cwd: cwd,
|
|
649
938
|
env: { ...process.env, TERM: 'xterm-256color' }
|
|
650
939
|
});
|
|
651
940
|
} catch (e) {
|
|
941
|
+
clearInterval(spinner);
|
|
652
942
|
viewer.setContent(`\n ${hexToAnsi(theme.red)}⊘ Failed to launch "${cmd}":\x1b[0m\n\n ${e.message}\n`);
|
|
653
943
|
viewMode = 'file';
|
|
654
944
|
xterm.dispose();
|
|
@@ -657,19 +947,15 @@ async function launchAIWithArgs(cmd, args, label) {
|
|
|
657
947
|
return;
|
|
658
948
|
}
|
|
659
949
|
|
|
660
|
-
// Create session object
|
|
661
950
|
const session = {
|
|
662
|
-
id: `${cmd}-${Date.now()}`,
|
|
663
|
-
|
|
664
|
-
pty: ptyProc, xterm, serialize,
|
|
665
|
-
ready: false, renderTimer: null
|
|
951
|
+
id: `${cmd}-${Date.now()}`, cmd, label, color: headerColor,
|
|
952
|
+
pty: ptyProc, xterm, serialize, ready: false, renderTimer: null
|
|
666
953
|
};
|
|
667
954
|
liveSessions.push(session);
|
|
668
955
|
activeSessionIdx = liveSessions.length - 1;
|
|
669
956
|
|
|
670
957
|
function renderXterm() {
|
|
671
958
|
if (!session.xterm || !session.serialize) return;
|
|
672
|
-
// Only update viewer if this session is the active one being shown
|
|
673
959
|
if (viewMode !== 'ai' || liveSessions[activeSessionIdx] !== session) return;
|
|
674
960
|
const raw = session.serialize.serialize();
|
|
675
961
|
viewer.setContent(cleanForBlessed(raw));
|
|
@@ -677,25 +963,21 @@ async function launchAIWithArgs(cmd, args, label) {
|
|
|
677
963
|
}
|
|
678
964
|
|
|
679
965
|
ptyProc.onData((data) => {
|
|
680
|
-
if (!session.ready) session.ready = true;
|
|
966
|
+
if (!session.ready) { session.ready = true; clearInterval(spinner); }
|
|
681
967
|
session.xterm.write(data, () => {
|
|
682
968
|
if (!session.renderTimer) {
|
|
683
|
-
session.renderTimer = setTimeout(() => {
|
|
684
|
-
session.renderTimer = null;
|
|
685
|
-
renderXterm();
|
|
686
|
-
}, 16);
|
|
969
|
+
session.renderTimer = setTimeout(() => { session.renderTimer = null; renderXterm(); }, 16);
|
|
687
970
|
}
|
|
688
971
|
});
|
|
689
972
|
});
|
|
690
973
|
|
|
691
974
|
ptyProc.onExit(({ exitCode }) => {
|
|
975
|
+
clearInterval(spinner);
|
|
692
976
|
if (session.renderTimer) { clearTimeout(session.renderTimer); session.renderTimer = null; }
|
|
693
977
|
|
|
694
|
-
// Remove from live sessions
|
|
695
978
|
const idx = liveSessions.indexOf(session);
|
|
696
979
|
if (idx !== -1) {
|
|
697
980
|
liveSessions.splice(idx, 1);
|
|
698
|
-
// Adjust activeSessionIdx
|
|
699
981
|
if (liveSessions.length === 0) {
|
|
700
982
|
activeSessionIdx = -1;
|
|
701
983
|
viewMode = 'file';
|
|
@@ -707,10 +989,7 @@ async function launchAIWithArgs(cmd, args, label) {
|
|
|
707
989
|
}
|
|
708
990
|
}
|
|
709
991
|
|
|
710
|
-
|
|
711
|
-
if (viewMode === 'ai' && activeSessionIdx >= 0) {
|
|
712
|
-
switchToSession(activeSessionIdx);
|
|
713
|
-
}
|
|
992
|
+
if (viewMode === 'ai' && activeSessionIdx >= 0) switchToSession(activeSessionIdx);
|
|
714
993
|
|
|
715
994
|
session.xterm.dispose();
|
|
716
995
|
session.xterm = null;
|
|
@@ -731,9 +1010,7 @@ async function launchAIWithArgs(cmd, args, label) {
|
|
|
731
1010
|
|
|
732
1011
|
function stopActiveSession() {
|
|
733
1012
|
const s = getActiveSession();
|
|
734
|
-
if (s && s.pty)
|
|
735
|
-
s.pty.kill();
|
|
736
|
-
}
|
|
1013
|
+
if (s && s.pty) s.pty.kill();
|
|
737
1014
|
}
|
|
738
1015
|
|
|
739
1016
|
// Forward keyboard input to active AI session
|
|
@@ -741,96 +1018,71 @@ viewer.on('keypress', (ch, key) => {
|
|
|
741
1018
|
const s = getActiveSession();
|
|
742
1019
|
if (!s || !s.pty || !s.ready || viewMode !== 'ai') return;
|
|
743
1020
|
if (key) {
|
|
744
|
-
if (key.name === 'return')
|
|
745
|
-
else if (key.name === 'backspace')
|
|
746
|
-
else if (key.name === 'escape')
|
|
747
|
-
else if (key.name === 'up')
|
|
748
|
-
else if (key.name === 'down')
|
|
749
|
-
else if (key.name === 'right')
|
|
750
|
-
else if (key.name === 'left')
|
|
751
|
-
else if (key.ctrl && key.name === 'c')
|
|
752
|
-
else if (key.ctrl && key.name === 'd')
|
|
753
|
-
else if (key.ctrl && key.name === 'l')
|
|
754
|
-
else if (key.ctrl && key.name === 'z')
|
|
755
|
-
else if (ch)
|
|
1021
|
+
if (key.name === 'return') s.pty.write('\r');
|
|
1022
|
+
else if (key.name === 'backspace') s.pty.write('\x7f');
|
|
1023
|
+
else if (key.name === 'escape') s.pty.write('\x1b');
|
|
1024
|
+
else if (key.name === 'up') s.pty.write('\x1b[A');
|
|
1025
|
+
else if (key.name === 'down') s.pty.write('\x1b[B');
|
|
1026
|
+
else if (key.name === 'right') s.pty.write('\x1b[C');
|
|
1027
|
+
else if (key.name === 'left') s.pty.write('\x1b[D');
|
|
1028
|
+
else if (key.ctrl && key.name === 'c') s.pty.write('\x03');
|
|
1029
|
+
else if (key.ctrl && key.name === 'd') s.pty.write('\x04');
|
|
1030
|
+
else if (key.ctrl && key.name === 'l') s.pty.write('\x0c');
|
|
1031
|
+
else if (key.ctrl && key.name === 'z') s.pty.write('\x1a');
|
|
1032
|
+
else if (ch) s.pty.write(ch);
|
|
756
1033
|
} else if (ch) {
|
|
757
1034
|
s.pty.write(ch);
|
|
758
1035
|
}
|
|
759
1036
|
});
|
|
760
1037
|
|
|
761
|
-
aiPicker.on('select', (item, index) => {
|
|
762
|
-
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
aiPicker.key(['escape', 'q'], () => {
|
|
766
|
-
hideAIPicker();
|
|
767
|
-
});
|
|
1038
|
+
aiPicker.on('select', (item, index) => { launchAI(index); });
|
|
1039
|
+
aiPicker.key(['escape', 'q'], () => { hideAIPicker(); });
|
|
768
1040
|
|
|
769
1041
|
// ── Event Handlers ──────────────────────────────────────────────────────
|
|
770
1042
|
tree.on('select', (node) => {
|
|
771
|
-
if (node.path) {
|
|
772
|
-
viewMode = 'file';
|
|
773
|
-
updateViewer(node.path);
|
|
774
|
-
}
|
|
1043
|
+
if (node.path) { viewMode = 'file'; updateViewer(node.path); }
|
|
775
1044
|
});
|
|
776
1045
|
|
|
777
1046
|
tree.rows.on('scroll', () => {
|
|
778
1047
|
if (isRefreshing) return;
|
|
779
1048
|
const node = tree.nodeLines[tree.rows.selected];
|
|
780
|
-
if (node && node.path) {
|
|
781
|
-
viewMode = 'file';
|
|
782
|
-
updateViewer(node.path);
|
|
783
|
-
}
|
|
1049
|
+
if (node && node.path) { viewMode = 'file'; updateViewer(node.path); }
|
|
784
1050
|
});
|
|
785
1051
|
|
|
786
|
-
tree.rows.key(['n'], () =>
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
});
|
|
792
|
-
});
|
|
1052
|
+
tree.rows.key(['n'], () => doNewFile());
|
|
1053
|
+
tree.rows.key(['f'], () => doNewFolder());
|
|
1054
|
+
tree.rows.key(['d', 'delete'], () => doDelete());
|
|
1055
|
+
tree.rows.key(['c'], () => doCopyPath());
|
|
1056
|
+
tree.rows.key(['e'], () => doOpenEditor());
|
|
793
1057
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
screen.render();
|
|
799
|
-
});
|
|
1058
|
+
screen.key(['a'], () => {
|
|
1059
|
+
if (!aiPicker.hidden || !sessionSwitcher.hidden || !cmdPalette.hidden) return;
|
|
1060
|
+
if (panels[focusIndex] === 'viewer' && viewMode === 'ai') return;
|
|
1061
|
+
showAIPicker();
|
|
800
1062
|
});
|
|
801
1063
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if (
|
|
805
|
-
|
|
806
|
-
if (data) { try { fs.removeSync(node.path); lastPath = null; viewer.setContent(''); refreshTree(); } catch (e) {} }
|
|
807
|
-
tree.rows.focus();
|
|
808
|
-
screen.render();
|
|
809
|
-
});
|
|
810
|
-
}
|
|
1064
|
+
screen.key(['C-a'], () => {
|
|
1065
|
+
if (!aiPicker.hidden || !cmdPalette.hidden) return;
|
|
1066
|
+
if (!sessionSwitcher.hidden) { sessionSwitcher.hide(); screen.render(); return; }
|
|
1067
|
+
if (liveSessions.length > 0) showSessionSwitcher();
|
|
811
1068
|
});
|
|
812
1069
|
|
|
813
|
-
screen.key(['
|
|
1070
|
+
screen.key(['C-b'], () => {
|
|
814
1071
|
if (!aiPicker.hidden || !sessionSwitcher.hidden) return;
|
|
815
|
-
if (
|
|
816
|
-
|
|
1072
|
+
if (!cmdPalette.hidden) { cmdPalette.hide(); screen.render(); return; }
|
|
1073
|
+
showCmdPalette();
|
|
817
1074
|
});
|
|
818
1075
|
|
|
819
|
-
screen.key(['C-
|
|
820
|
-
if (!aiPicker.hidden) return;
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
showSessionSwitcher();
|
|
824
|
-
}
|
|
1076
|
+
screen.key(['C-f'], () => {
|
|
1077
|
+
if (!aiPicker.hidden || !sessionSwitcher.hidden || !cmdPalette.hidden) return;
|
|
1078
|
+
if (panels[focusIndex] === 'viewer' && viewMode === 'ai') return;
|
|
1079
|
+
showSearch();
|
|
825
1080
|
});
|
|
826
1081
|
|
|
827
1082
|
screen.key(['r'], () => {
|
|
828
|
-
if (!aiPicker.hidden || !sessionSwitcher.hidden) return;
|
|
1083
|
+
if (!aiPicker.hidden || !sessionSwitcher.hidden || !cmdPalette.hidden) return;
|
|
829
1084
|
if (panels[focusIndex] === 'viewer' && viewMode === 'ai') return;
|
|
830
|
-
|
|
831
|
-
screen.realloc();
|
|
832
|
-
refreshTree();
|
|
833
|
-
refreshHistory(true);
|
|
1085
|
+
doRefresh();
|
|
834
1086
|
});
|
|
835
1087
|
|
|
836
1088
|
// Tab cycles: tree → history → viewer → tree
|
|
@@ -839,7 +1091,6 @@ let focusIndex = 0;
|
|
|
839
1091
|
|
|
840
1092
|
function setFocusPanel(idx) {
|
|
841
1093
|
focusIndex = idx;
|
|
842
|
-
// Reset all borders
|
|
843
1094
|
tree.style.border.fg = theme.border;
|
|
844
1095
|
historyList.style.border.fg = theme.border;
|
|
845
1096
|
viewer.style.border.fg = theme.border;
|
|
@@ -849,7 +1100,7 @@ function setFocusPanel(idx) {
|
|
|
849
1100
|
tree.style.border.fg = theme.borderFocus;
|
|
850
1101
|
updateStatusBar('normal');
|
|
851
1102
|
} else if (panels[idx] === 'history') {
|
|
852
|
-
refreshHistory(true);
|
|
1103
|
+
refreshHistory(true);
|
|
853
1104
|
historyList.focus();
|
|
854
1105
|
historyList.style.border.fg = theme.purple;
|
|
855
1106
|
updateStatusBar('normal');
|
|
@@ -857,14 +1108,11 @@ function setFocusPanel(idx) {
|
|
|
857
1108
|
viewer.focus();
|
|
858
1109
|
const s = getActiveSession();
|
|
859
1110
|
if (s && s.pty) {
|
|
860
|
-
// Restore AI terminal view
|
|
861
1111
|
viewMode = 'ai';
|
|
862
1112
|
lastPath = null;
|
|
863
1113
|
viewer.style.border.fg = s.color;
|
|
864
1114
|
viewer.setLabel(` ${hexToAnsi(s.color)}◆ ${s.label}\x1b[0m `);
|
|
865
|
-
if (s.xterm && s.serialize)
|
|
866
|
-
viewer.setContent(cleanForBlessed(s.serialize.serialize()));
|
|
867
|
-
}
|
|
1115
|
+
if (s.xterm && s.serialize) viewer.setContent(cleanForBlessed(s.serialize.serialize()));
|
|
868
1116
|
updateStatusBar('ai');
|
|
869
1117
|
} else {
|
|
870
1118
|
viewer.style.border.fg = theme.borderFocus;
|
|
@@ -875,14 +1123,13 @@ function setFocusPanel(idx) {
|
|
|
875
1123
|
}
|
|
876
1124
|
|
|
877
1125
|
screen.key(['C-t'], () => {
|
|
878
|
-
if (!aiPicker.hidden || !sessionSwitcher.hidden) return;
|
|
1126
|
+
if (!aiPicker.hidden || !sessionSwitcher.hidden || !cmdPalette.hidden) return;
|
|
879
1127
|
focusIndex = (focusIndex + 1) % panels.length;
|
|
880
1128
|
setFocusPanel(focusIndex);
|
|
881
1129
|
});
|
|
882
1130
|
|
|
883
1131
|
screen.key(['tab'], () => {
|
|
884
|
-
if (!aiPicker.hidden || !sessionSwitcher.hidden) return;
|
|
885
|
-
// When AI is active and viewer is focused, forward tab to pty
|
|
1132
|
+
if (!aiPicker.hidden || !sessionSwitcher.hidden || !cmdPalette.hidden) return;
|
|
886
1133
|
const s = getActiveSession();
|
|
887
1134
|
if (s && s.pty && s.ready && viewMode === 'ai' && panels[focusIndex] === 'viewer') {
|
|
888
1135
|
s.pty.write('\t');
|
|
@@ -893,6 +1140,7 @@ screen.key(['tab'], () => {
|
|
|
893
1140
|
});
|
|
894
1141
|
|
|
895
1142
|
screen.key(['escape'], () => {
|
|
1143
|
+
if (!cmdPalette.hidden) { cmdPalette.hide(); screen.render(); return; }
|
|
896
1144
|
if (!sessionSwitcher.hidden) { sessionSwitcher.hide(); screen.render(); return; }
|
|
897
1145
|
if (!aiPicker.hidden) { hideAIPicker(); return; }
|
|
898
1146
|
const s = getActiveSession();
|
|
@@ -901,7 +1149,7 @@ screen.key(['escape'], () => {
|
|
|
901
1149
|
});
|
|
902
1150
|
|
|
903
1151
|
screen.key(['q'], () => {
|
|
904
|
-
if (!aiPicker.hidden || !sessionSwitcher.hidden) return;
|
|
1152
|
+
if (!aiPicker.hidden || !sessionSwitcher.hidden || !cmdPalette.hidden) return;
|
|
905
1153
|
if (viewMode === 'ai' && panels[focusIndex] === 'viewer') return;
|
|
906
1154
|
process.exit(0);
|
|
907
1155
|
});
|
|
@@ -928,9 +1176,7 @@ screen.on('keypress', (ch, key) => {
|
|
|
928
1176
|
const s = getActiveSession();
|
|
929
1177
|
if (key && key.ctrl && key.name === 'c' && s && s.pty && viewMode === 'ai' && panels[focusIndex] === 'viewer') {
|
|
930
1178
|
const now = Date.now();
|
|
931
|
-
if (now - lastCtrlC < 500)
|
|
932
|
-
stopActiveSession();
|
|
933
|
-
}
|
|
1179
|
+
if (now - lastCtrlC < 500) stopActiveSession();
|
|
934
1180
|
lastCtrlC = now;
|
|
935
1181
|
}
|
|
936
1182
|
});
|
|
@@ -938,7 +1184,6 @@ screen.on('keypress', (ch, key) => {
|
|
|
938
1184
|
screen.on('resize', () => {
|
|
939
1185
|
const cols = Math.max(viewer.width - viewer.iwidth - 2, 40);
|
|
940
1186
|
const rows = Math.max(viewer.height - viewer.iheight, 10);
|
|
941
|
-
// Resize ALL live sessions
|
|
942
1187
|
for (const s of liveSessions) {
|
|
943
1188
|
if (s.pty) s.pty.resize(cols, rows);
|
|
944
1189
|
if (s.xterm) s.xterm.resize(cols, rows);
|
|
@@ -946,6 +1191,8 @@ screen.on('resize', () => {
|
|
|
946
1191
|
});
|
|
947
1192
|
|
|
948
1193
|
// ── Init ────────────────────────────────────────────────────────────────
|
|
1194
|
+
refreshGitStatus();
|
|
949
1195
|
refreshTree();
|
|
950
1196
|
refreshHistory();
|
|
1197
|
+
showWelcome();
|
|
951
1198
|
setFocusPanel(0);
|