@bramblex/codex-workbench 0.1.19 → 0.1.21
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 +11 -1
- package/package.json +1 -1
- package/src/cli-output.js +3 -0
- package/src/providers/claude.js +283 -0
- package/src/providers/index.js +3 -1
- package/src/providers/opencode.js +31 -0
- package/src/ui/directory-picker.js +25 -7
- package/src/ui/workbench.js +182 -64
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ codex-workbench is an **interactive terminal UI** for coding-agent sessions. Ins
|
|
|
18
18
|
|
|
19
19
|
It also connects to **remote machines over SSH**, so you can manage sessions across all your servers from a single pane of glass.
|
|
20
20
|
|
|
21
|
-
Built-in backends currently include [Codex](https://github.com/openai/codex), pi, and opencode. The backend layer is intentionally provider-based so additional agents can be added without changing the TUI workflow.
|
|
21
|
+
Built-in backends currently include [Codex](https://github.com/openai/codex), Claude Code, pi, and opencode. The backend layer is intentionally provider-based so additional agents can be added without changing the TUI workflow.
|
|
22
22
|
|
|
23
23
|
A handful of CLI subcommands are available for scripting, but the TUI is the product.
|
|
24
24
|
|
|
@@ -136,6 +136,7 @@ codex-workbench auto-detects installed backends by checking each backend's sessi
|
|
|
136
136
|
| Backend | Sessions | Binary override | Notes |
|
|
137
137
|
|---------|----------|-----------------|-------|
|
|
138
138
|
| `codex` | `$CODEX_SESSIONS_DIR` or `~/.codex/sessions` | `CODEX_BIN` | Uses the Codex CLI for new, resume, fork, archive, unarchive, and delete. |
|
|
139
|
+
| `claude` | `$CLAUDE_PROJECTS_DIR` or `~/.claude/projects` | `CLAUDE_BIN` | Uses the Claude Code CLI for new, resume, and fork. Archive/unarchive use workbench metadata; delete removes the session file. |
|
|
139
140
|
| `pi` | `$PI_CODING_AGENT_SESSION_DIR` or `$PI_CODING_AGENT_DIR/sessions` | `PI_BIN` | Uses the pi CLI for new, resume, and fork. Archive/unarchive use workbench metadata; delete removes the session file. |
|
|
140
141
|
| `opencode` | `$OPENCODE_DB`, `$OPENCODE_DATA_DIR/opencode.db`, or `~/.local/share/opencode/opencode.db` | `OPENCODE_BIN` | Uses the opencode CLI and database for list, new, resume, fork, archive, unarchive, and delete. |
|
|
141
142
|
|
|
@@ -165,6 +166,7 @@ cwb fork <session>
|
|
|
165
166
|
cwb delete <session> --force
|
|
166
167
|
|
|
167
168
|
cwb new --cwd ~/projects/foo --backend codex "Summarize this repo"
|
|
169
|
+
cwb new --cwd ~/projects/foo --backend claude "Summarize this repo"
|
|
168
170
|
cwb new --cwd ~/projects/foo --backend pi "Summarize this repo"
|
|
169
171
|
cwb new --cwd ~/projects/foo --backend opencode "Summarize this repo"
|
|
170
172
|
cwb resume <session> "what was the conclusion about the rate limiter?"
|
|
@@ -191,6 +193,8 @@ When you run `new` or `resume`, the selected backend takes over the terminal. Wh
|
|
|
191
193
|
| `CWB_CONFIG` | `$CWB_HOME/config.json` | SSH remote sources config |
|
|
192
194
|
| `CODEX_HOME` | `~/.codex` | Codex data directory |
|
|
193
195
|
| `CODEX_SESSIONS_DIR` | `$CODEX_HOME/sessions` | Session JSONL files |
|
|
196
|
+
| `CLAUDE_HOME` | `~/.claude` | Claude Code data directory |
|
|
197
|
+
| `CLAUDE_PROJECTS_DIR` | `$CLAUDE_HOME/projects` | Claude Code project session JSONL files |
|
|
194
198
|
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | pi coding agent data directory |
|
|
195
199
|
| `PI_CODING_AGENT_SESSION_DIR` | `$PI_CODING_AGENT_DIR/sessions` | pi session JSONL files |
|
|
196
200
|
| `OPENCODE_DATA_DIR` | `~/.local/share/opencode` | opencode data directory |
|
|
@@ -198,6 +202,7 @@ When you run `new` or `resume`, the selected backend takes over the terminal. Wh
|
|
|
198
202
|
| `CODEX_WORKBENCH_META` | unset | Legacy override for `CWB_META` |
|
|
199
203
|
| `CODEX_WORKBENCH_CONFIG` | unset | Legacy override for `CWB_CONFIG` |
|
|
200
204
|
| `CODEX_BIN` | auto-detected | Force a specific Codex executable |
|
|
205
|
+
| `CLAUDE_BIN` | auto-detected | Force a specific Claude Code executable |
|
|
201
206
|
| `PI_BIN` | auto-detected | Force a specific pi executable |
|
|
202
207
|
| `OPENCODE_BIN` | auto-detected | Force a specific opencode executable |
|
|
203
208
|
|
|
@@ -223,6 +228,10 @@ Run `codex-workbench doctor` to see where codex-workbench is looking. Common fix
|
|
|
223
228
|
|
|
224
229
|
Make sure you've run Codex at least once. Sessions are stored as `.jsonl` files under `$CODEX_SESSIONS_DIR`. Run `ls ~/.codex/sessions/` to verify.
|
|
225
230
|
|
|
231
|
+
### No Claude Code sessions appear
|
|
232
|
+
|
|
233
|
+
Make sure you've run Claude Code at least once. Sessions are stored as `.jsonl` files under `$CLAUDE_PROJECTS_DIR`. Run `find ~/.claude/projects -name '*.jsonl'` to verify.
|
|
234
|
+
|
|
226
235
|
### No pi sessions appear
|
|
227
236
|
|
|
228
237
|
Make sure you've run the pi coding agent at least once. Sessions are stored as `.jsonl` files under `$PI_CODING_AGENT_SESSION_DIR` or `$PI_CODING_AGENT_DIR/sessions`. Run `ls ~/.pi/agent/sessions/` to verify.
|
|
@@ -293,6 +302,7 @@ src/
|
|
|
293
302
|
workbench-config.js # SSH remote source config loader
|
|
294
303
|
providers/
|
|
295
304
|
codex.js # Codex provider
|
|
305
|
+
claude.js # Claude Code provider
|
|
296
306
|
pi.js # pi provider
|
|
297
307
|
opencode.js # opencode provider
|
|
298
308
|
index.js # provider registry
|
package/package.json
CHANGED
package/src/cli-output.js
CHANGED
|
@@ -31,6 +31,8 @@ Environment:
|
|
|
31
31
|
CWB_CONFIG default: $CWB_HOME/config.json
|
|
32
32
|
CODEX_HOME default: ~/.codex
|
|
33
33
|
CODEX_SESSIONS_DIR default: $CODEX_HOME/sessions
|
|
34
|
+
CLAUDE_HOME default: ~/.claude
|
|
35
|
+
CLAUDE_PROJECTS_DIR default: $CLAUDE_HOME/projects
|
|
34
36
|
PI_CODING_AGENT_DIR default: ~/.pi/agent
|
|
35
37
|
PI_CODING_AGENT_SESSION_DIR default: $PI_CODING_AGENT_DIR/sessions
|
|
36
38
|
OPENCODE_DATA_DIR default: ~/.local/share/opencode
|
|
@@ -38,6 +40,7 @@ Environment:
|
|
|
38
40
|
CODEX_WORKBENCH_META legacy override for CWB_META
|
|
39
41
|
CODEX_WORKBENCH_CONFIG legacy override for CWB_CONFIG
|
|
40
42
|
CODEX_BIN default: codex from shell PATH
|
|
43
|
+
CLAUDE_BIN default: claude from shell PATH
|
|
41
44
|
PI_BIN default: pi from shell PATH
|
|
42
45
|
OPENCODE_BIN default: opencode from shell PATH
|
|
43
46
|
`);
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Claude Code provider – session parsing, binary discovery, and CLI operations
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawn, spawnSync } = require('child_process');
|
|
11
|
+
const { removeMetadata, updateMetadata } = require('../model/metadata');
|
|
12
|
+
|
|
13
|
+
const HOME = os.homedir();
|
|
14
|
+
const CLAUDE_HOME = process.env.CLAUDE_HOME || path.join(HOME, '.claude');
|
|
15
|
+
const CLAUDE_PROJECTS_DIR = process.env.CLAUDE_PROJECTS_DIR || path.join(CLAUDE_HOME, 'projects');
|
|
16
|
+
|
|
17
|
+
function shellQuote(value) {
|
|
18
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isExecutable(file) {
|
|
22
|
+
try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findOnPath(command, pathValue) {
|
|
26
|
+
for (const dir of (pathValue || process.env.PATH || '').split(path.delimiter)) {
|
|
27
|
+
if (!dir) continue;
|
|
28
|
+
const candidate = path.join(dir, command);
|
|
29
|
+
if (isExecutable(candidate)) return candidate;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function executableFromOutput(output) {
|
|
35
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
36
|
+
const candidate = line.trim();
|
|
37
|
+
if (candidate && path.isAbsolute(candidate) && isExecutable(candidate)) return candidate;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runShellLookup(shell, shellArgs, command, env) {
|
|
43
|
+
const result = spawnSync(shell, [...shellArgs, `command -v ${shellQuote(command)}`], {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
env,
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
});
|
|
48
|
+
if (result.error || result.status !== 0) return null;
|
|
49
|
+
return executableFromOutput(result.stdout);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findWithShell(command, env) {
|
|
53
|
+
const shell = (env || process.env).SHELL || '/bin/sh';
|
|
54
|
+
if (!isExecutable(shell)) return null;
|
|
55
|
+
return runShellLookup(shell, ['-lc'], command, env) ||
|
|
56
|
+
runShellLookup(shell, ['-ic'], command, env);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveClaudeBin() {
|
|
60
|
+
const env = process.env;
|
|
61
|
+
if (env.CLAUDE_BIN) {
|
|
62
|
+
if (isExecutable(env.CLAUDE_BIN)) return env.CLAUDE_BIN;
|
|
63
|
+
throw new Error(`CLAUDE_BIN is not executable: ${env.CLAUDE_BIN}`);
|
|
64
|
+
}
|
|
65
|
+
const fromShell = findWithShell('claude', env);
|
|
66
|
+
if (fromShell) return fromShell;
|
|
67
|
+
const fromPath = findOnPath('claude', env.PATH);
|
|
68
|
+
if (fromPath) return fromPath;
|
|
69
|
+
throw new Error('Could not find the claude executable. Set CLAUDE_BIN or add claude to PATH.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function commandShell() {
|
|
73
|
+
const shell = process.env.SHELL || '/bin/sh';
|
|
74
|
+
try { fs.accessSync(shell, fs.constants.X_OK); return shell; } catch { return '/bin/sh'; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function usableCwd(dir) {
|
|
78
|
+
for (const candidate of [dir, process.cwd(), HOME]) {
|
|
79
|
+
if (!candidate || candidate === '(unknown)') continue;
|
|
80
|
+
try { if (fs.statSync(candidate).isDirectory()) return candidate; } catch { /* skip */ }
|
|
81
|
+
}
|
|
82
|
+
return HOME;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function walk(dir, out) {
|
|
86
|
+
out = out || [];
|
|
87
|
+
let entries;
|
|
88
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
const full = path.join(dir, entry.name);
|
|
91
|
+
if (entry.isDirectory()) walk(full, out);
|
|
92
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function textFromContent(content) {
|
|
98
|
+
if (typeof content === 'string') return content.trim();
|
|
99
|
+
if (!Array.isArray(content)) return '';
|
|
100
|
+
return content
|
|
101
|
+
.map((item) => {
|
|
102
|
+
if (!item) return '';
|
|
103
|
+
if (typeof item === 'string') return item;
|
|
104
|
+
if (item.type === 'text') return item.text || '';
|
|
105
|
+
if (item.type === 'tool_result') return '';
|
|
106
|
+
return '';
|
|
107
|
+
})
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(' ')
|
|
110
|
+
.replace(/\s+/g, ' ')
|
|
111
|
+
.trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function firstUserText(messages) {
|
|
115
|
+
const message = messages.find((msg) => msg.role === 'user');
|
|
116
|
+
return message ? message.text : '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function lastText(messages, role) {
|
|
120
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
121
|
+
if (!role || messages[i].role === role) return messages[i].text;
|
|
122
|
+
}
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isNoiseUserText(text) {
|
|
127
|
+
return /^<local-command-/.test(text) ||
|
|
128
|
+
/^<command-name>/.test(text) ||
|
|
129
|
+
text.includes('<environment_context>') ||
|
|
130
|
+
text.includes('<permissions instructions>');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function emptySession(file, stat) {
|
|
134
|
+
return {
|
|
135
|
+
id: path.basename(file, '.jsonl'),
|
|
136
|
+
file,
|
|
137
|
+
cwd: '(unknown)',
|
|
138
|
+
startedAt: stat.birthtime?.toISOString() || null,
|
|
139
|
+
updatedAt: stat.mtime.toISOString(),
|
|
140
|
+
cliVersion: '',
|
|
141
|
+
provider: '',
|
|
142
|
+
turns: 0,
|
|
143
|
+
first: '',
|
|
144
|
+
last: '',
|
|
145
|
+
lastAssistant: '',
|
|
146
|
+
messages: [],
|
|
147
|
+
backend: 'claude',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseSession(file) {
|
|
152
|
+
const stat = fs.statSync(file);
|
|
153
|
+
const raw = fs.readFileSync(file, 'utf8').trim();
|
|
154
|
+
if (!raw) return emptySession(file, stat);
|
|
155
|
+
|
|
156
|
+
const messages = [];
|
|
157
|
+
let id = path.basename(file, '.jsonl');
|
|
158
|
+
let cwd = '';
|
|
159
|
+
let startedAt = '';
|
|
160
|
+
let updatedAt = stat.mtime.toISOString();
|
|
161
|
+
let cliVersion = '';
|
|
162
|
+
let provider = '';
|
|
163
|
+
let turns = 0;
|
|
164
|
+
|
|
165
|
+
for (const line of raw.split(/\n/)) {
|
|
166
|
+
let row;
|
|
167
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
168
|
+
|
|
169
|
+
if (row.sessionId) id = row.sessionId;
|
|
170
|
+
if (row.cwd) cwd = row.cwd;
|
|
171
|
+
if (row.version) cliVersion = row.version;
|
|
172
|
+
if (row.timestamp) {
|
|
173
|
+
if (!startedAt) startedAt = row.timestamp;
|
|
174
|
+
updatedAt = row.timestamp;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if ((row.type === 'user' || row.type === 'assistant') && row.message) {
|
|
178
|
+
const role = row.message.role || row.type;
|
|
179
|
+
const text = textFromContent(row.message.content);
|
|
180
|
+
if (!text) continue;
|
|
181
|
+
if (role === 'user' && isNoiseUserText(text)) continue;
|
|
182
|
+
messages.push({ role, text });
|
|
183
|
+
if (role === 'user') turns += 1;
|
|
184
|
+
if (!provider && role === 'assistant' && row.message.model) provider = row.message.model;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
id,
|
|
190
|
+
file,
|
|
191
|
+
cwd: cwd || '(unknown)',
|
|
192
|
+
startedAt: startedAt || stat.birthtime?.toISOString() || null,
|
|
193
|
+
updatedAt,
|
|
194
|
+
cliVersion,
|
|
195
|
+
provider,
|
|
196
|
+
turns,
|
|
197
|
+
first: firstUserText(messages),
|
|
198
|
+
last: lastText(messages, 'user'),
|
|
199
|
+
lastAssistant: lastText(messages, 'assistant'),
|
|
200
|
+
messages,
|
|
201
|
+
backend: 'claude',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function runArgv(argv, cwd, inherit) {
|
|
206
|
+
const shellCommand = `exec ${argv.map(shellQuote).join(' ')}`;
|
|
207
|
+
const shell = commandShell();
|
|
208
|
+
if (inherit) {
|
|
209
|
+
const child = spawn(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
210
|
+
child.on('error', (err) => { console.error(`error: failed to start claude: ${err.message}`); process.exit(1); });
|
|
211
|
+
child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); process.exit(code || 0); });
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
const result = spawnSync(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
215
|
+
if (result.error) throw new Error(`failed to start claude: ${result.error.message}`);
|
|
216
|
+
const status = typeof result.status === 'number' ? result.status : 1;
|
|
217
|
+
process.exitCode = status;
|
|
218
|
+
return status;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function runSessionCommand(command, session, args, inherit) {
|
|
222
|
+
const executable = resolveClaudeBin();
|
|
223
|
+
const cwd = usableCwd(session.cwd);
|
|
224
|
+
switch (command) {
|
|
225
|
+
case 'resume': {
|
|
226
|
+
const argv = [executable, '--resume', session.id];
|
|
227
|
+
if (args && args.length) argv.push(args.join(' '));
|
|
228
|
+
return runArgv(argv, cwd, inherit);
|
|
229
|
+
}
|
|
230
|
+
case 'fork': {
|
|
231
|
+
return runArgv([executable, '--resume', session.id, '--fork-session'], cwd, inherit);
|
|
232
|
+
}
|
|
233
|
+
case 'delete': {
|
|
234
|
+
fs.unlinkSync(session.file);
|
|
235
|
+
removeMetadata(session);
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
case 'archive':
|
|
239
|
+
case 'unarchive': {
|
|
240
|
+
updateMetadata(session, { archived: command === 'archive' });
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
throw new Error(`Unknown command for claude backend: ${command}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runNew(cwd, args, inherit) {
|
|
249
|
+
const argv = [resolveClaudeBin(), ...(args || [])];
|
|
250
|
+
return runArgv(argv, usableCwd(cwd), inherit);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isAvailable() {
|
|
254
|
+
try { return fs.statSync(CLAUDE_PROJECTS_DIR).isDirectory(); } catch { return false; }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getSessionFiles() {
|
|
258
|
+
return walk(CLAUDE_PROJECTS_DIR);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function resolveBin() {
|
|
262
|
+
try { return resolveClaudeBin(); } catch { return null; }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
id: 'claude',
|
|
267
|
+
label: 'Claude Code',
|
|
268
|
+
capabilities: {
|
|
269
|
+
new: true,
|
|
270
|
+
resume: true,
|
|
271
|
+
fork: true,
|
|
272
|
+
archive: true,
|
|
273
|
+
unarchive: true,
|
|
274
|
+
delete: true,
|
|
275
|
+
},
|
|
276
|
+
isAvailable,
|
|
277
|
+
getSessionFiles,
|
|
278
|
+
parseSession,
|
|
279
|
+
resolveBin,
|
|
280
|
+
runCommand: runSessionCommand,
|
|
281
|
+
runNew,
|
|
282
|
+
resolveClaudeBin,
|
|
283
|
+
};
|
package/src/providers/index.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
7
|
const codex = require('./codex');
|
|
8
|
+
const claude = require('./claude');
|
|
8
9
|
const opencode = require('./opencode');
|
|
9
10
|
const pi = require('./pi');
|
|
10
11
|
|
|
11
|
-
const ALL_PROVIDERS = [codex, pi, opencode];
|
|
12
|
+
const ALL_PROVIDERS = [codex, pi, opencode, claude];
|
|
12
13
|
const providerMap = new Map(ALL_PROVIDERS.map((p) => [p.id, p]));
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -59,6 +60,7 @@ module.exports = {
|
|
|
59
60
|
getProvider,
|
|
60
61
|
providerForSession,
|
|
61
62
|
getAllSessionFiles,
|
|
63
|
+
claude,
|
|
62
64
|
codex,
|
|
63
65
|
opencode,
|
|
64
66
|
pi,
|
|
@@ -24,6 +24,35 @@ function findOnPath(command, pathValue) {
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function shellQuote(value) {
|
|
28
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function executableFromOutput(output) {
|
|
32
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
33
|
+
const candidate = line.trim();
|
|
34
|
+
if (candidate && path.isAbsolute(candidate) && isExecutable(candidate)) return candidate;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runShellLookup(shell, shellArgs, command, env) {
|
|
40
|
+
const result = spawnSync(shell, [...shellArgs, `command -v ${shellQuote(command)}`], {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
env,
|
|
43
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
});
|
|
45
|
+
if (result.error || result.status !== 0) return null;
|
|
46
|
+
return executableFromOutput(result.stdout);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findWithShell(command, env) {
|
|
50
|
+
const shell = (env || process.env).SHELL || '/bin/sh';
|
|
51
|
+
if (!isExecutable(shell)) return null;
|
|
52
|
+
return runShellLookup(shell, ['-lc'], command, env) ||
|
|
53
|
+
runShellLookup(shell, ['-ic'], command, env);
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
function resolveOpenCodeBin() {
|
|
28
57
|
if (process.env.OPENCODE_BIN) {
|
|
29
58
|
if (isExecutable(process.env.OPENCODE_BIN)) return process.env.OPENCODE_BIN;
|
|
@@ -31,6 +60,8 @@ function resolveOpenCodeBin() {
|
|
|
31
60
|
}
|
|
32
61
|
const fromPath = findOnPath('opencode', process.env.PATH);
|
|
33
62
|
if (fromPath) return fromPath;
|
|
63
|
+
const fromShell = findWithShell('opencode', process.env);
|
|
64
|
+
if (fromShell) return fromShell;
|
|
34
65
|
throw new Error('Could not find the opencode executable. Set OPENCODE_BIN or add opencode to PATH.');
|
|
35
66
|
}
|
|
36
67
|
|
|
@@ -6,13 +6,23 @@ const blessed = require('blessed');
|
|
|
6
6
|
const { createChildDirectory, directoryNameError, listDirectories } = require('../model/directories');
|
|
7
7
|
|
|
8
8
|
const DEFAULT_HELP = '↑/↓ move ←/h parent →/l child n new directory Enter choose selected Esc/q cancel';
|
|
9
|
+
const color = (hex) => blessed.colors.match(hex);
|
|
10
|
+
|
|
11
|
+
const FALLBACK_THEME = {
|
|
12
|
+
bg: color('#101216'),
|
|
13
|
+
surface: color('#151a21'),
|
|
14
|
+
text: color('#e5e7eb'),
|
|
15
|
+
textInverse: color('#0b0f14'),
|
|
16
|
+
project: color('#22c55e'),
|
|
17
|
+
danger: color('#f87171'),
|
|
18
|
+
};
|
|
9
19
|
|
|
10
20
|
const DEFAULT_OPS = {
|
|
11
21
|
listDirectories: (dir) => listDirectories(dir),
|
|
12
22
|
createDirectory: (parent, name) => createChildDirectory(parent, name),
|
|
13
23
|
};
|
|
14
24
|
|
|
15
|
-
function createDirectoryPicker({ screen, askInput, focusOnClose, truncate }) {
|
|
25
|
+
function createDirectoryPicker({ screen, askInput, focusOnClose, theme = FALLBACK_THEME, truncate }) {
|
|
16
26
|
const list = blessed.list({
|
|
17
27
|
parent: screen,
|
|
18
28
|
label: ' Choose directory ',
|
|
@@ -25,11 +35,13 @@ function createDirectoryPicker({ screen, askInput, focusOnClose, truncate }) {
|
|
|
25
35
|
mouse: true,
|
|
26
36
|
keys: true,
|
|
27
37
|
vi: false,
|
|
28
|
-
scrollbar: { ch: ' ', track: { bg:
|
|
38
|
+
scrollbar: { ch: ' ', track: { bg: theme.surface }, style: { bg: theme.project } },
|
|
29
39
|
style: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
bg: theme.bg,
|
|
41
|
+
border: { fg: theme.project, bg: theme.bg },
|
|
42
|
+
label: { fg: theme.project, bg: theme.bg },
|
|
43
|
+
selected: { fg: theme.textInverse, bg: theme.project, bold: true },
|
|
44
|
+
item: { fg: theme.text, bg: theme.bg },
|
|
33
45
|
},
|
|
34
46
|
});
|
|
35
47
|
|
|
@@ -42,7 +54,12 @@ function createDirectoryPicker({ screen, askInput, focusOnClose, truncate }) {
|
|
|
42
54
|
border: 'line',
|
|
43
55
|
padding: { left: 1, right: 1 },
|
|
44
56
|
content: DEFAULT_HELP,
|
|
45
|
-
style: {
|
|
57
|
+
style: {
|
|
58
|
+
bg: theme.surface,
|
|
59
|
+
border: { fg: theme.project, bg: theme.surface },
|
|
60
|
+
fg: theme.text,
|
|
61
|
+
label: { fg: theme.project, bg: theme.surface },
|
|
62
|
+
},
|
|
46
63
|
});
|
|
47
64
|
|
|
48
65
|
let state = null;
|
|
@@ -51,7 +68,8 @@ function createDirectoryPicker({ screen, askInput, focusOnClose, truncate }) {
|
|
|
51
68
|
|
|
52
69
|
const setHelp = (text = DEFAULT_HELP, isError = false) => {
|
|
53
70
|
help.setContent(text);
|
|
54
|
-
help.style.fg = isError ?
|
|
71
|
+
help.style.fg = isError ? theme.danger : theme.text;
|
|
72
|
+
help.style.bg = theme.surface;
|
|
55
73
|
};
|
|
56
74
|
|
|
57
75
|
const entriesFor = (dir) => {
|
package/src/ui/workbench.js
CHANGED
|
@@ -24,6 +24,40 @@ const { usableCwd } = require('../services/codex-runner');
|
|
|
24
24
|
const { checkForUpdate } = require('../services/update-checker');
|
|
25
25
|
const { createDirectoryPicker } = require('./directory-picker');
|
|
26
26
|
|
|
27
|
+
const color = (hex) => blessed.colors.match(hex);
|
|
28
|
+
|
|
29
|
+
const THEME = {
|
|
30
|
+
bg: color('#101216'),
|
|
31
|
+
surface: color('#151a21'),
|
|
32
|
+
surfaceRaised: color('#1f2630'),
|
|
33
|
+
surfaceMuted: color('#343b46'),
|
|
34
|
+
text: color('#e5e7eb'),
|
|
35
|
+
textMuted: color('#8b95a1'),
|
|
36
|
+
textInverse: color('#0b0f14'),
|
|
37
|
+
borderIdle: color('#3f4652'),
|
|
38
|
+
accent: color('#38bdf8'),
|
|
39
|
+
accentAlt: color('#2563eb'),
|
|
40
|
+
project: color('#22c55e'),
|
|
41
|
+
detail: color('#f59e0b'),
|
|
42
|
+
warning: color('#facc15'),
|
|
43
|
+
danger: color('#f87171'),
|
|
44
|
+
success: color('#34d399'),
|
|
45
|
+
tag: {
|
|
46
|
+
accent: 'cyan',
|
|
47
|
+
detail: 'yellow',
|
|
48
|
+
muted: 'gray',
|
|
49
|
+
text: 'white',
|
|
50
|
+
warning: 'yellow',
|
|
51
|
+
},
|
|
52
|
+
backend: {
|
|
53
|
+
claude: 'yellow',
|
|
54
|
+
codex: 'cyan',
|
|
55
|
+
opencode: 'green',
|
|
56
|
+
pi: 'magenta',
|
|
57
|
+
unknown: 'blue',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
27
61
|
async function runWorkbench() {
|
|
28
62
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
29
63
|
return printList(loadWorkbenchSessions().sessions);
|
|
@@ -45,6 +79,7 @@ async function runWorkbench() {
|
|
|
45
79
|
let remoteLoading = false;
|
|
46
80
|
let updateInfo = null;
|
|
47
81
|
let closed = false;
|
|
82
|
+
let searchQuery = '';
|
|
48
83
|
|
|
49
84
|
const screen = blessed.screen({
|
|
50
85
|
smartCSR: true,
|
|
@@ -52,6 +87,15 @@ async function runWorkbench() {
|
|
|
52
87
|
title: appTitle,
|
|
53
88
|
});
|
|
54
89
|
|
|
90
|
+
blessed.box({
|
|
91
|
+
parent: screen,
|
|
92
|
+
top: 0,
|
|
93
|
+
left: 0,
|
|
94
|
+
right: 0,
|
|
95
|
+
bottom: 0,
|
|
96
|
+
style: { bg: THEME.bg },
|
|
97
|
+
});
|
|
98
|
+
|
|
55
99
|
const header = blessed.box({
|
|
56
100
|
parent: screen,
|
|
57
101
|
top: 0,
|
|
@@ -59,7 +103,7 @@ async function runWorkbench() {
|
|
|
59
103
|
right: 0,
|
|
60
104
|
height: 3,
|
|
61
105
|
padding: { left: 1, right: 1 },
|
|
62
|
-
style: { fg:
|
|
106
|
+
style: { fg: THEME.text, bg: THEME.accentAlt },
|
|
63
107
|
content: appTitle,
|
|
64
108
|
});
|
|
65
109
|
|
|
@@ -75,11 +119,13 @@ async function runWorkbench() {
|
|
|
75
119
|
keys: true,
|
|
76
120
|
vi: false,
|
|
77
121
|
tags: true,
|
|
78
|
-
scrollbar: { ch: ' ', track: { bg:
|
|
122
|
+
scrollbar: { ch: ' ', track: { bg: THEME.surface }, style: { bg: THEME.project } },
|
|
79
123
|
style: {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
124
|
+
bg: THEME.bg,
|
|
125
|
+
border: { fg: THEME.project, bg: THEME.bg },
|
|
126
|
+
label: { fg: THEME.project, bg: THEME.bg },
|
|
127
|
+
selected: { fg: THEME.textInverse, bg: THEME.project, bold: true },
|
|
128
|
+
item: { fg: THEME.text, bg: THEME.bg },
|
|
83
129
|
},
|
|
84
130
|
});
|
|
85
131
|
|
|
@@ -95,11 +141,13 @@ async function runWorkbench() {
|
|
|
95
141
|
keys: true,
|
|
96
142
|
vi: false,
|
|
97
143
|
tags: true,
|
|
98
|
-
scrollbar: { ch: ' ', track: { bg:
|
|
144
|
+
scrollbar: { ch: ' ', track: { bg: THEME.surface }, style: { bg: THEME.accent } },
|
|
99
145
|
style: {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
bg: THEME.bg,
|
|
147
|
+
border: { fg: THEME.accent, bg: THEME.bg },
|
|
148
|
+
label: { fg: THEME.accent, bg: THEME.bg },
|
|
149
|
+
selected: { fg: THEME.textInverse, bg: THEME.accent, bold: true },
|
|
150
|
+
item: { fg: THEME.text, bg: THEME.bg },
|
|
103
151
|
},
|
|
104
152
|
});
|
|
105
153
|
|
|
@@ -117,10 +165,15 @@ async function runWorkbench() {
|
|
|
117
165
|
keys: true,
|
|
118
166
|
vi: true,
|
|
119
167
|
alwaysScroll: true,
|
|
120
|
-
tags:
|
|
121
|
-
parseTags:
|
|
122
|
-
scrollbar: { ch: ' ', track: { bg:
|
|
123
|
-
style: {
|
|
168
|
+
tags: true,
|
|
169
|
+
parseTags: true,
|
|
170
|
+
scrollbar: { ch: ' ', track: { bg: THEME.surface }, style: { bg: THEME.accent } },
|
|
171
|
+
style: {
|
|
172
|
+
bg: THEME.bg,
|
|
173
|
+
border: { fg: THEME.accent, bg: THEME.bg },
|
|
174
|
+
fg: THEME.text,
|
|
175
|
+
label: { fg: THEME.accent, bg: THEME.bg },
|
|
176
|
+
},
|
|
124
177
|
});
|
|
125
178
|
|
|
126
179
|
const status = blessed.box({
|
|
@@ -130,7 +183,7 @@ async function runWorkbench() {
|
|
|
130
183
|
bottom: 0,
|
|
131
184
|
height: 3,
|
|
132
185
|
padding: { left: 1, right: 1 },
|
|
133
|
-
style: { fg:
|
|
186
|
+
style: { fg: THEME.text, bg: THEME.surface },
|
|
134
187
|
});
|
|
135
188
|
|
|
136
189
|
const prompt = blessed.prompt({
|
|
@@ -141,7 +194,12 @@ async function runWorkbench() {
|
|
|
141
194
|
top: 'center',
|
|
142
195
|
left: 'center',
|
|
143
196
|
padding: { left: 1, right: 1 },
|
|
144
|
-
style: {
|
|
197
|
+
style: {
|
|
198
|
+
border: { fg: THEME.warning, bg: THEME.surface },
|
|
199
|
+
fg: THEME.text,
|
|
200
|
+
bg: THEME.surface,
|
|
201
|
+
label: { fg: THEME.warning, bg: THEME.surface },
|
|
202
|
+
},
|
|
145
203
|
});
|
|
146
204
|
|
|
147
205
|
const question = blessed.question({
|
|
@@ -152,7 +210,12 @@ async function runWorkbench() {
|
|
|
152
210
|
top: 'center',
|
|
153
211
|
left: 'center',
|
|
154
212
|
padding: { left: 1, right: 1 },
|
|
155
|
-
style: {
|
|
213
|
+
style: {
|
|
214
|
+
border: { fg: THEME.danger, bg: THEME.surface },
|
|
215
|
+
fg: THEME.text,
|
|
216
|
+
bg: THEME.surface,
|
|
217
|
+
label: { fg: THEME.danger, bg: THEME.surface },
|
|
218
|
+
},
|
|
156
219
|
});
|
|
157
220
|
|
|
158
221
|
const backendPicker = blessed.list({
|
|
@@ -167,10 +230,13 @@ async function runWorkbench() {
|
|
|
167
230
|
mouse: true,
|
|
168
231
|
keys: true,
|
|
169
232
|
vi: false,
|
|
233
|
+
tags: true,
|
|
170
234
|
style: {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
235
|
+
bg: THEME.bg,
|
|
236
|
+
border: { fg: THEME.warning, bg: THEME.bg },
|
|
237
|
+
label: { fg: THEME.warning, bg: THEME.bg },
|
|
238
|
+
selected: { fg: THEME.textInverse, bg: THEME.warning, bold: true },
|
|
239
|
+
item: { fg: THEME.text, bg: THEME.bg },
|
|
174
240
|
},
|
|
175
241
|
});
|
|
176
242
|
|
|
@@ -204,13 +270,35 @@ async function runWorkbench() {
|
|
|
204
270
|
if (index !== -1) groupIndex = index;
|
|
205
271
|
};
|
|
206
272
|
|
|
207
|
-
const
|
|
273
|
+
const currentGroupSessions = () => {
|
|
208
274
|
const group = currentGroup();
|
|
209
275
|
if (group.kind === 'all') return sessions;
|
|
210
276
|
if (group.kind === 'source') return sessionsForSource(group.source.id);
|
|
211
277
|
return sessions.filter((session) => session.sourceId === group.source.id && session.cwd === group.cwd);
|
|
212
278
|
};
|
|
213
279
|
|
|
280
|
+
const normalizedSearch = () => searchQuery.trim().toLowerCase();
|
|
281
|
+
|
|
282
|
+
const sessionMatchesSearch = (session) => {
|
|
283
|
+
const query = normalizedSearch();
|
|
284
|
+
if (!query) return true;
|
|
285
|
+
const haystack = [
|
|
286
|
+
session.backend,
|
|
287
|
+
session.cwd,
|
|
288
|
+
session.first,
|
|
289
|
+
session.id,
|
|
290
|
+
session.last,
|
|
291
|
+
session.lastAssistant,
|
|
292
|
+
session.name,
|
|
293
|
+
session.note,
|
|
294
|
+
session.provider,
|
|
295
|
+
session.sourceLabel,
|
|
296
|
+
].filter(Boolean).join('\n').toLowerCase();
|
|
297
|
+
return haystack.includes(query);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const currentSessions = () => currentGroupSessions().filter(sessionMatchesSearch);
|
|
301
|
+
|
|
214
302
|
const selectedSession = () => currentSessions()[selected] || null;
|
|
215
303
|
|
|
216
304
|
const groupDisplayName = (group) => {
|
|
@@ -226,15 +314,9 @@ async function runWorkbench() {
|
|
|
226
314
|
|
|
227
315
|
const styledListLabel = (color, text) => `{${color}-fg}{bold}${blessed.escape(text)}{/}`;
|
|
228
316
|
|
|
229
|
-
const backendThemes = {
|
|
230
|
-
codex: 'cyan',
|
|
231
|
-
pi: 'magenta',
|
|
232
|
-
opencode: 'green',
|
|
233
|
-
};
|
|
234
|
-
|
|
235
317
|
const backendLabel = (backend, width = 0) => {
|
|
236
318
|
const text = backend || 'unknown';
|
|
237
|
-
const color =
|
|
319
|
+
const color = THEME.backend[text] || THEME.backend.unknown;
|
|
238
320
|
return `{${color}-fg}{bold}${blessed.escape(text.padEnd(width))}{/}`;
|
|
239
321
|
};
|
|
240
322
|
|
|
@@ -246,53 +328,58 @@ async function runWorkbench() {
|
|
|
246
328
|
const width = Math.max(12, projectWidth - 4);
|
|
247
329
|
const head = `= ${text} `;
|
|
248
330
|
const line = `${head}${'='.repeat(width)}`.slice(0, width);
|
|
249
|
-
return styledListLabel(
|
|
331
|
+
return styledListLabel(THEME.tag.warning, line);
|
|
250
332
|
};
|
|
251
333
|
|
|
252
334
|
const projectLabel = (group) => {
|
|
253
|
-
if (group.kind === 'all') return styledListLabel(
|
|
335
|
+
if (group.kind === 'all') return styledListLabel(THEME.tag.text, `0 All (${sessions.length})`);
|
|
254
336
|
if (group.kind === 'source') {
|
|
255
337
|
const count = sessionsForSource(group.source.id).length;
|
|
256
338
|
return machineLabel(group.source, count);
|
|
257
339
|
}
|
|
258
340
|
const count = sessions.filter((session) => session.sourceId === group.source.id && session.cwd === group.cwd).length;
|
|
259
341
|
const base = path.basename(group.cwd) || group.cwd;
|
|
260
|
-
return ` ${blessed.escape(`${truncate(base, Math.max(10, projectWidth - 12))} (${count})`)}`;
|
|
342
|
+
return `{${THEME.tag.muted}-fg} ${blessed.escape(`${truncate(base, Math.max(10, projectWidth - 12))} (${count})`)}{/}`;
|
|
261
343
|
};
|
|
262
344
|
|
|
263
345
|
const sessionLabel = (session) => {
|
|
264
346
|
const title = session.name || session.first || session.last || '(no prompt)';
|
|
265
347
|
const width = Math.max(24, (screen.width || 80) - projectWidth - 8);
|
|
266
|
-
const backendWidth =
|
|
348
|
+
const backendWidth = 11;
|
|
267
349
|
const time = truncate(localTime(session.updatedAt), 18).padEnd(18);
|
|
268
350
|
const detailWidth = Math.max(12, width - backendWidth - 22);
|
|
269
|
-
return `${backendLabel(session.backend, backendWidth)} ${time} ${blessed.escape(truncate(title, detailWidth))}`;
|
|
351
|
+
return `${backendLabel(session.backend, backendWidth)} {${THEME.tag.muted}-fg}${blessed.escape(time)}{/} ${blessed.escape(truncate(title, detailWidth))}`;
|
|
270
352
|
};
|
|
271
353
|
|
|
272
354
|
const detailContent = (session) => {
|
|
273
|
-
if (!session) return
|
|
355
|
+
if (!session) return `{${THEME.tag.muted}-fg}No sessions match this view.{/}`;
|
|
274
356
|
const title = session.name || session.first || session.last || '(no prompt)';
|
|
275
357
|
return [
|
|
276
|
-
title
|
|
358
|
+
`{${THEME.tag.accent}-fg}{bold}${blessed.escape(title)}{/}`,
|
|
359
|
+
'',
|
|
360
|
+
`{${THEME.tag.muted}-fg}Session{/}`,
|
|
361
|
+
` backend ${session.backend || 'unknown'}`,
|
|
362
|
+
` id ${session.id}`,
|
|
363
|
+
` source ${session.sourceLabel || 'Local'}`,
|
|
364
|
+
` cwd ${session.cwd}`,
|
|
277
365
|
'',
|
|
278
|
-
`
|
|
279
|
-
`
|
|
280
|
-
`
|
|
281
|
-
`
|
|
282
|
-
|
|
283
|
-
`updated: ${localTime(session.updatedAt)}`,
|
|
284
|
-
`turns: ${session.turns}`,
|
|
285
|
-
session.note ? `note: ${session.note}` : '',
|
|
366
|
+
`{${THEME.tag.muted}-fg}Timeline{/}`,
|
|
367
|
+
` started ${localTime(session.startedAt)}`,
|
|
368
|
+
` updated ${localTime(session.updatedAt)}`,
|
|
369
|
+
` turns ${session.turns}`,
|
|
370
|
+
session.note ? `\n{${THEME.tag.muted}-fg}Note{/}\n ${session.note}` : '',
|
|
286
371
|
'',
|
|
287
|
-
`
|
|
372
|
+
`{${THEME.tag.muted}-fg}Last user{/}`,
|
|
373
|
+
`${session.last || session.first || ''}`,
|
|
288
374
|
'',
|
|
289
|
-
`
|
|
375
|
+
`{${THEME.tag.muted}-fg}Last assistant{/}`,
|
|
376
|
+
`${session.lastAssistant || ''}`,
|
|
290
377
|
].filter((line) => line !== '').join('\n');
|
|
291
378
|
};
|
|
292
379
|
|
|
293
380
|
const setMessage = (text, isError = false) => {
|
|
294
381
|
message = text || 'Ready';
|
|
295
|
-
status.style.fg = isError ?
|
|
382
|
+
status.style.fg = isError ? THEME.danger : THEME.text;
|
|
296
383
|
};
|
|
297
384
|
|
|
298
385
|
const visibleSession = (session) => !session.archived;
|
|
@@ -428,7 +515,9 @@ async function runWorkbench() {
|
|
|
428
515
|
const syncList = () => {
|
|
429
516
|
const visible = currentSessions();
|
|
430
517
|
const listRows = Math.max(1, (sessionsList.height || Math.floor((screen.height || 24) * 0.4)) - 2);
|
|
431
|
-
const items = visible.length
|
|
518
|
+
const items = visible.length
|
|
519
|
+
? visible.map(sessionLabel)
|
|
520
|
+
: [`{${THEME.tag.muted}-fg}${searchQuery ? 'No sessions match this search.' : 'No sessions in this project.'}{/}`];
|
|
432
521
|
while (items.length < listRows) items.push('');
|
|
433
522
|
syncingList = true;
|
|
434
523
|
sessionsList.clearItems();
|
|
@@ -444,8 +533,8 @@ async function runWorkbench() {
|
|
|
444
533
|
const setPanelLabel = (panel, title, focused, fg) => {
|
|
445
534
|
panel.setLabel(focused ? ` > ${title} ` : ` ${title} `);
|
|
446
535
|
if (!panel._label) return;
|
|
447
|
-
panel._label.style.fg = focused ? fg :
|
|
448
|
-
panel._label.style.bg =
|
|
536
|
+
panel._label.style.fg = focused ? fg : THEME.textMuted;
|
|
537
|
+
panel._label.style.bg = THEME.bg;
|
|
449
538
|
panel._label.style.bold = focused;
|
|
450
539
|
};
|
|
451
540
|
|
|
@@ -454,33 +543,39 @@ async function runWorkbench() {
|
|
|
454
543
|
const sessionsFocused = activePanel === 'sessions';
|
|
455
544
|
const detailFocused = activePanel === 'details';
|
|
456
545
|
|
|
457
|
-
projectsList.style.border.fg = projectFocused ?
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
546
|
+
projectsList.style.border.fg = projectFocused ? THEME.project : THEME.borderIdle;
|
|
547
|
+
projectsList.style.border.bg = THEME.bg;
|
|
548
|
+
sessionsList.style.border.fg = sessionsFocused ? THEME.accent : THEME.borderIdle;
|
|
549
|
+
sessionsList.style.border.bg = THEME.bg;
|
|
550
|
+
detailBox.style.border.fg = detailFocused ? THEME.detail : THEME.borderIdle;
|
|
551
|
+
detailBox.style.border.bg = THEME.bg;
|
|
552
|
+
projectsList.style.selected.bg = projectFocused ? THEME.project : THEME.surfaceMuted;
|
|
553
|
+
projectsList.style.selected.fg = THEME.textInverse;
|
|
554
|
+
sessionsList.style.selected.bg = sessionsFocused ? THEME.accent : THEME.surfaceMuted;
|
|
555
|
+
sessionsList.style.selected.fg = THEME.textInverse;
|
|
556
|
+
|
|
557
|
+
setPanelLabel(projectsList, `Sources (${sources.length})`, projectFocused, THEME.project);
|
|
558
|
+
setPanelLabel(sessionsList, searchQuery ? `Sessions / ${searchQuery}` : 'Sessions', sessionsFocused, THEME.accent);
|
|
559
|
+
setPanelLabel(detailBox, 'Details', detailFocused, THEME.detail);
|
|
468
560
|
|
|
469
561
|
const firstLine = message || 'Ready';
|
|
562
|
+
const searchHelp = searchQuery ? ` search "${searchQuery}" x clear` : ' / search';
|
|
470
563
|
if (projectFocused) {
|
|
471
|
-
status.setContent(`${firstLine}\nSources: ↑/↓ select 0 all 1-9 machine [/] prev/next n new → sessions q quit`);
|
|
564
|
+
status.setContent(`${firstLine}\nSources: ↑/↓ select 0 all 1-9 machine [/] prev/next n new${searchHelp} → sessions q quit`);
|
|
472
565
|
} else if (detailFocused) {
|
|
473
|
-
status.setContent(`${firstLine}\nDetails: ↑/↓ scroll 1-9 machine [/] prev/next n new ← sessions q quit`);
|
|
566
|
+
status.setContent(`${firstLine}\nDetails: ↑/↓ scroll 1-9 machine [/] prev/next n new${searchHelp} ← sessions q quit`);
|
|
474
567
|
} else {
|
|
475
|
-
status.setContent(`${firstLine}\nSessions: ↑/↓ select Enter resume
|
|
568
|
+
status.setContent(`${firstLine}\nSessions: ↑/↓ select Enter resume r rename n new d delete${searchHelp} q quit`);
|
|
476
569
|
}
|
|
477
570
|
};
|
|
478
571
|
|
|
479
572
|
const render = () => {
|
|
480
573
|
applyLayout();
|
|
481
574
|
const visible = currentSessions();
|
|
575
|
+
const groupTotal = currentGroupSessions().length;
|
|
482
576
|
const updateText = updateInfo ? ` Update available: v${updateInfo.latestVersion}` : '';
|
|
483
|
-
|
|
577
|
+
const searchText = searchQuery ? ` search: ${searchQuery}` : '';
|
|
578
|
+
header.setContent(` ${appTitle}${updateText}\n ${visible.length}/${groupTotal} shown ${groupDisplayName(currentGroup())}${searchText}`);
|
|
484
579
|
detailBox.setContent(detailContent(selectedSession()));
|
|
485
580
|
updateFocusStyles();
|
|
486
581
|
screen.render();
|
|
@@ -520,7 +615,10 @@ async function runWorkbench() {
|
|
|
520
615
|
|
|
521
616
|
backendPickerState = { backends, resolve };
|
|
522
617
|
backendPicker.clearItems();
|
|
523
|
-
backendPicker.setItems(backends.map((backend) =>
|
|
618
|
+
backendPicker.setItems(backends.map((backend) => {
|
|
619
|
+
const id = String(backend.id);
|
|
620
|
+
return `${backendLabel(id, 11)} ${blessed.escape(backend.label || id)}`;
|
|
621
|
+
}));
|
|
524
622
|
backendPicker.select(0);
|
|
525
623
|
backendPicker.show();
|
|
526
624
|
backendPicker.setFront();
|
|
@@ -532,6 +630,7 @@ async function runWorkbench() {
|
|
|
532
630
|
askInput,
|
|
533
631
|
focusOnClose: () => focusPanel(projectsList, 'projects'),
|
|
534
632
|
screen,
|
|
633
|
+
theme: THEME,
|
|
535
634
|
truncate,
|
|
536
635
|
});
|
|
537
636
|
|
|
@@ -831,6 +930,25 @@ async function runWorkbench() {
|
|
|
831
930
|
switchSource(-1);
|
|
832
931
|
});
|
|
833
932
|
|
|
933
|
+
screen.key(['/'], async () => {
|
|
934
|
+
if (promptOpen()) return;
|
|
935
|
+
const value = await askInput('Search', searchQuery);
|
|
936
|
+
if (value === null) return render();
|
|
937
|
+
searchQuery = value.trim();
|
|
938
|
+
selected = 0;
|
|
939
|
+
syncList();
|
|
940
|
+
render();
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
screen.key(['x'], () => {
|
|
944
|
+
if (promptOpen() || !searchQuery) return;
|
|
945
|
+
searchQuery = '';
|
|
946
|
+
selected = 0;
|
|
947
|
+
setMessage('Search cleared.');
|
|
948
|
+
syncList();
|
|
949
|
+
render();
|
|
950
|
+
});
|
|
951
|
+
|
|
834
952
|
screen.key(['q', 'escape', 'C-c'], () => {
|
|
835
953
|
if (promptOpen()) return;
|
|
836
954
|
leaveScreen();
|