ccompactor 0.1.14 → 0.1.16
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 +23 -20
- package/dist/install.js +19 -1
- package/dist/install.js.map +1 -1
- package/dist/tui/App.js +548 -385
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/frame.d.ts +109 -0
- package/dist/tui/frame.js +155 -0
- package/dist/tui/frame.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/tui/App.js
CHANGED
|
@@ -1,44 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* `ccompactor --tui`
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* and pressing `h` did nothing at all unless you had already extracted, which
|
|
9
|
-
* is the wrong order for anyone who does not yet know what they want.
|
|
5
|
+
* A frame with a top bar, a sidebar, a content pane and a bottom bar, and
|
|
6
|
+
* modals over the top of it. The screens that need a decision are modals rather
|
|
7
|
+
* than pages, so the thing you were looking at stays where it was.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* need to copy, and a truncated id is a useless one.
|
|
15
|
-
* - **Nothing is silent.** Every operation that can take longer than a blink
|
|
16
|
-
* shows the stage it is in, because on a 260 MB transcript a frozen screen and
|
|
17
|
-
* a working one look identical.
|
|
18
|
-
* - **The actions are named, not remembered.** Choosing a session shows what can
|
|
19
|
-
* be done to it, with the destination written out.
|
|
9
|
+
* Nothing here is reachable only by a key. Every cell of the top bar, the
|
|
10
|
+
* sidebar and the bottom bar is clickable, and the bottom bar prints the key on
|
|
11
|
+
* the button so the shortcut is learned by using it.
|
|
20
12
|
*/
|
|
21
13
|
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
22
14
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
23
15
|
import { listSessions, fuzzyScore } from '../discover/index.js';
|
|
24
16
|
import { extractSession } from '../extract.js';
|
|
25
17
|
import { plan, installed } from '../handoff/index.js';
|
|
26
|
-
import { THEME, hazard, sizeLabel } from './theme.js';
|
|
27
|
-
import { enableMouse, readMouse } from './mouse.js';
|
|
28
|
-
import { barCells, inSpan, layoutCells } from './bar.js';
|
|
29
18
|
import { loadDetails } from './details.js';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* Every screen puts the stripe, the title chips and the action bar on rows 1-3,
|
|
34
|
-
* so a click means the same thing on all of them and the arithmetic stays in one
|
|
35
|
-
* place. The row below the bar is a column header on the browse screen and a
|
|
36
|
-
* question on the menu screens; either way the first thing you can click is 5.
|
|
37
|
-
*/
|
|
38
|
-
const ROW_CHIPS = 2;
|
|
39
|
-
const ROW_BAR = 3;
|
|
40
|
-
const LIST_TOP = 5;
|
|
41
|
-
const MENU_TOP = 5;
|
|
19
|
+
import { THEME, sizeLabel } from './theme.js';
|
|
20
|
+
import { enableMouse, readMouse } from './mouse.js';
|
|
21
|
+
import { MENU, ROW_TOP, SIDEBAR_TOP, SIDEBAR_WIDTH, bottomBar, inCell, layoutCells, menuIndex, tabCells, } from './frame.js';
|
|
42
22
|
/**
|
|
43
23
|
* How large a transcript may be before the table stops reading it in the
|
|
44
24
|
* background. Above this the row shows a size and a date, and the rest arrives
|
|
@@ -49,12 +29,11 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
49
29
|
const { exit } = useApp();
|
|
50
30
|
const { stdout } = useStdout();
|
|
51
31
|
// Not `?? 100`: a pty with no window size reports 0, and 0 is not nullish, so
|
|
52
|
-
// every layout decision saw a zero-width terminal and collapsed to
|
|
53
|
-
// button. A width too small to draw anything is a width we do not know.
|
|
32
|
+
// every layout decision saw a zero-width terminal and collapsed to nothing.
|
|
54
33
|
const rawColumns = stdout?.columns;
|
|
55
|
-
const columns = rawColumns !== undefined && rawColumns >=
|
|
34
|
+
const columns = rawColumns !== undefined && rawColumns >= 40 ? rawColumns : 100;
|
|
56
35
|
const rawRows = stdout?.rows;
|
|
57
|
-
const rows = rawRows !== undefined && rawRows >=
|
|
36
|
+
const rows = rawRows !== undefined && rawRows >= 12 ? rawRows : 30;
|
|
58
37
|
const [sessions, setSessions] = useState([]);
|
|
59
38
|
const [loading, setLoading] = useState('scanning agent stores …');
|
|
60
39
|
const [query, setQuery] = useState('');
|
|
@@ -62,11 +41,10 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
62
41
|
const [agentsOff, setAgentsOff] = useState(new Set());
|
|
63
42
|
const [cursor, setCursor] = useState(0);
|
|
64
43
|
const [offset, setOffset] = useState(0);
|
|
65
|
-
const [
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// empty until they land on a row.
|
|
44
|
+
const [page, setPage] = useState('sessions');
|
|
45
|
+
const [menuCursor, setMenuCursor] = useState(0);
|
|
46
|
+
const [focus, setFocus] = useState('content');
|
|
47
|
+
const [modal, setModal] = useState();
|
|
70
48
|
const [cache, setCache] = useState(new Map());
|
|
71
49
|
const [previewScroll, setPreviewScroll] = useState(0);
|
|
72
50
|
const [actionIndex, setActionIndex] = useState(0);
|
|
@@ -77,6 +55,8 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
77
55
|
const [results, setResults] = useState([]);
|
|
78
56
|
const [lastArtifact, setLastArtifact] = useState();
|
|
79
57
|
const [error, setError] = useState();
|
|
58
|
+
const [report, setReport] = useState();
|
|
59
|
+
const [artifacts, setArtifacts] = useState([]);
|
|
80
60
|
const busy = useRef(false);
|
|
81
61
|
// ---- loading ------------------------------------------------------------
|
|
82
62
|
useEffect(() => {
|
|
@@ -104,9 +84,11 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
104
84
|
}
|
|
105
85
|
return out;
|
|
106
86
|
}, [sessions, query, agentsOff]);
|
|
107
|
-
|
|
87
|
+
// Rows the content pane may draw on, minus what the pane itself spends.
|
|
88
|
+
const bodyHeight = Math.max(4, rows - 2);
|
|
89
|
+
const contentHeight = Math.max(2, bodyHeight - 3);
|
|
90
|
+
const visibleRows = Math.max(2, contentHeight - 2);
|
|
108
91
|
const selected = filtered[cursor];
|
|
109
|
-
// Keep the cursor on screen without a scroll library.
|
|
110
92
|
useEffect(() => {
|
|
111
93
|
setOffset((current) => {
|
|
112
94
|
if (cursor < current)
|
|
@@ -116,21 +98,9 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
116
98
|
return current;
|
|
117
99
|
});
|
|
118
100
|
}, [cursor, visibleRows]);
|
|
119
|
-
// ---- details, loaded for whatever is under the cursor
|
|
120
|
-
//
|
|
121
|
-
// Debounced, because holding an arrow key should not queue twenty parses of a
|
|
122
|
-
// 260 MB file.
|
|
123
|
-
// ---- prefill the window -------------------------------------------------
|
|
124
|
-
//
|
|
125
|
-
// Bounded on purpose: only what is on screen, three at a time, and never a
|
|
126
|
-
// file already in the cache. Filling all 720 would mean parsing hundreds of
|
|
127
|
-
// megabytes for rows nobody has looked at.
|
|
101
|
+
// ---- details, loaded for whatever is under the cursor --------------------
|
|
128
102
|
useEffect(() => {
|
|
129
103
|
const window = filtered.slice(offset, offset + visibleRows);
|
|
130
|
-
// Big transcripts are left alone here and read only when the reader lands on
|
|
131
|
-
// one. Parsing a 268 MB session to fill in a column of file sizes is not a
|
|
132
|
-
// trade worth making, and doing thirty of them at once is what killed the
|
|
133
|
-
// process.
|
|
134
104
|
const wanted = window.filter((ref) => !cache.has(`${ref.agent}:${ref.id}`) && (ref.bytes ?? 0) < PREFILL_MAX_BYTES);
|
|
135
105
|
if (wanted.length === 0)
|
|
136
106
|
return;
|
|
@@ -150,8 +120,7 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
150
120
|
});
|
|
151
121
|
}
|
|
152
122
|
catch {
|
|
153
|
-
// A session that cannot be read is a row with no metadata
|
|
154
|
-
// failed run. The real error is still available through `extract`.
|
|
123
|
+
// A session that cannot be read is a row with no metadata.
|
|
155
124
|
}
|
|
156
125
|
}
|
|
157
126
|
})();
|
|
@@ -159,7 +128,6 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
159
128
|
cancelled = true;
|
|
160
129
|
};
|
|
161
130
|
}, [filtered, offset, visibleRows, cache]);
|
|
162
|
-
// Whatever is under the cursor is always read, however large, one at a time.
|
|
163
131
|
useEffect(() => {
|
|
164
132
|
if (!selected)
|
|
165
133
|
return;
|
|
@@ -188,20 +156,10 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
188
156
|
}, [selected, cache]);
|
|
189
157
|
// ---- actions ------------------------------------------------------------
|
|
190
158
|
const actions = useMemo(() => {
|
|
191
|
-
const hasLlm = Boolean(process.env['
|
|
159
|
+
const hasLlm = Boolean(process.env['CCOMPACTOR_API_KEY'] ??
|
|
160
|
+
process.env['ANTHROPIC_API_KEY'] ??
|
|
161
|
+
process.env['OPENAI_API_KEY']);
|
|
192
162
|
return [
|
|
193
|
-
// These two read as synonyms — "hand off" and "launch" both suggest the
|
|
194
|
-
// agent starts. They differ in who starts it, so the labels say that.
|
|
195
|
-
{
|
|
196
|
-
id: 'handoff',
|
|
197
|
-
label: 'Hand off — print the command for the next agent',
|
|
198
|
-
detail: 'fork this session into a new one and show the command; you run it yourself',
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
id: 'handoff-run',
|
|
202
|
-
label: 'Hand off — and start the next agent here',
|
|
203
|
-
detail: 'the same command, run for you in this terminal',
|
|
204
|
-
},
|
|
205
163
|
{
|
|
206
164
|
id: 'extract',
|
|
207
165
|
label: 'Extract handoff — deterministic, no model',
|
|
@@ -210,7 +168,16 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
210
168
|
{
|
|
211
169
|
id: 'extract-llm',
|
|
212
170
|
label: 'Extract handoff — with a model-written summary',
|
|
213
|
-
detail: hasLlm
|
|
171
|
+
detail: hasLlm
|
|
172
|
+
? `adds L1 via the configured backend → ${outDir}/handoff.md`
|
|
173
|
+
: 'no API key found; set CCOMPACTOR_API_KEY and CCOMPACTOR_BASE_URL',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'narrate',
|
|
177
|
+
label: 'Narrate — write L1 from the artifact',
|
|
178
|
+
detail: hasLlm
|
|
179
|
+
? `reads ${outDir}/handoff.md, not the transcript; ~5k tokens`
|
|
180
|
+
: 'needs a model; and an artifact to narrate',
|
|
214
181
|
},
|
|
215
182
|
{
|
|
216
183
|
id: 'transcript',
|
|
@@ -222,6 +189,16 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
222
189
|
label: 'Readable transcript — including tool calls',
|
|
223
190
|
detail: `adds every tool call and its output → ${outDir}/transcript.md`,
|
|
224
191
|
},
|
|
192
|
+
{
|
|
193
|
+
id: 'handoff',
|
|
194
|
+
label: 'Hand off — print the command for the next agent',
|
|
195
|
+
detail: 'fork this session into a new one and show the command; you run it yourself',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'handoff-run',
|
|
199
|
+
label: 'Hand off — and start the next agent here',
|
|
200
|
+
detail: 'the same command, run for you in this terminal',
|
|
201
|
+
},
|
|
225
202
|
{
|
|
226
203
|
id: 'verify',
|
|
227
204
|
label: 'Verify the artifact already in this directory',
|
|
@@ -236,19 +213,37 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
236
213
|
setError(undefined);
|
|
237
214
|
setResults([]);
|
|
238
215
|
setProgress([]);
|
|
239
|
-
|
|
216
|
+
setModal({ kind: 'running' });
|
|
240
217
|
const onProgress = (stage, message) => {
|
|
241
218
|
setProgress((current) => [...current.slice(-8), `${stage}: ${message}`]);
|
|
242
219
|
};
|
|
243
220
|
try {
|
|
244
221
|
if (actionId === 'verify') {
|
|
245
222
|
const { verify } = await import('../artifact/verify.js');
|
|
246
|
-
const
|
|
223
|
+
const r = await verify(outDir);
|
|
224
|
+
setResults([
|
|
225
|
+
`${r.constraintsQuoted} quote(s) checked, ${r.missingQuotes.length} missing`,
|
|
226
|
+
`${r.filesClaimed} file(s) claimed, ${r.filesChecked} looked for, ${r.missingFiles.length} gone`,
|
|
227
|
+
]);
|
|
228
|
+
setModal({ kind: 'result' });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (actionId === 'narrate') {
|
|
232
|
+
const { narrate, writeNarrative, requireArtifact } = await import('../compact/narrate.js');
|
|
233
|
+
const { resolveAuto, build } = await import('../llm/index.js');
|
|
234
|
+
await requireArtifact(outDir);
|
|
235
|
+
const backend = build(resolveAuto());
|
|
236
|
+
if (!backend)
|
|
237
|
+
throw new Error('narrate needs a model; set CCOMPACTOR_API_KEY and CCOMPACTOR_BASE_URL');
|
|
238
|
+
onProgress('narrate', `reading ${outDir}/handoff.md`);
|
|
239
|
+
const result = await narrate(outDir, backend);
|
|
240
|
+
await writeNarrative(outDir, result.narrative);
|
|
241
|
+
setLastArtifact(`${outDir}/handoff.md`);
|
|
247
242
|
setResults([
|
|
248
|
-
`${
|
|
249
|
-
|
|
243
|
+
`${result.artifactTokens} input token(s)`,
|
|
244
|
+
`written into ${outDir}/handoff.md`,
|
|
250
245
|
]);
|
|
251
|
-
|
|
246
|
+
setModal({ kind: 'result' });
|
|
252
247
|
return;
|
|
253
248
|
}
|
|
254
249
|
const format = actionId.startsWith('transcript') ? 'transcript' : 'handoff';
|
|
@@ -263,15 +258,12 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
263
258
|
}, onProgress);
|
|
264
259
|
const artifact = result.written[0] ?? `${outDir}/handoff.md`;
|
|
265
260
|
setLastArtifact(artifact);
|
|
266
|
-
setResults([
|
|
267
|
-
`${result.rendered.tokens} tokens in ${result.elapsedMs} ms`,
|
|
268
|
-
...result.written,
|
|
269
|
-
]);
|
|
261
|
+
setResults([`${result.rendered.tokens} tokens in ${result.elapsedMs} ms`, ...result.written]);
|
|
270
262
|
if (actionId === 'handoff' || actionId === 'handoff-run') {
|
|
271
263
|
const chosen = target ?? targets[0];
|
|
272
264
|
if (!chosen) {
|
|
273
265
|
setError('no launchable agent found on PATH');
|
|
274
|
-
|
|
266
|
+
setModal({ kind: 'result' });
|
|
275
267
|
return;
|
|
276
268
|
}
|
|
277
269
|
const launch = plan(chosen, artifact, process.cwd());
|
|
@@ -284,32 +276,23 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
284
276
|
return;
|
|
285
277
|
}
|
|
286
278
|
}
|
|
287
|
-
|
|
279
|
+
setModal({ kind: 'result' });
|
|
288
280
|
}
|
|
289
281
|
catch (caught) {
|
|
290
282
|
setError(caught.message);
|
|
291
|
-
|
|
283
|
+
setModal({ kind: 'result' });
|
|
292
284
|
}
|
|
293
285
|
finally {
|
|
294
286
|
busy.current = false;
|
|
295
287
|
}
|
|
296
288
|
}, [selected, outDir, targets, exit, onDone]);
|
|
297
|
-
const openActions = useCallback(() => {
|
|
298
|
-
setActionIndex(0);
|
|
299
|
-
setScreen('actions');
|
|
300
|
-
}, []);
|
|
301
|
-
const openPreview = useCallback(() => {
|
|
302
|
-
setPreviewScroll(0);
|
|
303
|
-
setScreen('preview');
|
|
304
|
-
}, []);
|
|
305
|
-
// ---- the top bar --------------------------------------------------------
|
|
306
289
|
const runSelectedAction = useCallback(() => {
|
|
307
290
|
const action = actions[actionIndex];
|
|
308
291
|
if (!action)
|
|
309
292
|
return;
|
|
310
293
|
if (action.id === 'handoff' || action.id === 'handoff-run') {
|
|
311
294
|
setTargetIndex(0);
|
|
312
|
-
|
|
295
|
+
setModal({ kind: 'target' });
|
|
313
296
|
return;
|
|
314
297
|
}
|
|
315
298
|
void run(action.id);
|
|
@@ -320,170 +303,263 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
320
303
|
if (action && target)
|
|
321
304
|
void run(action.id, target);
|
|
322
305
|
}, [actions, actionIndex, targetIndex, targets, run]);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
key: '0',
|
|
343
|
-
label: 'all agents',
|
|
344
|
-
run: () => {
|
|
345
|
-
setAgentsOff(new Set());
|
|
346
|
-
setCursor(0);
|
|
347
|
-
},
|
|
348
|
-
},
|
|
349
|
-
{ id: 'open', key: '↵', label: 'quick look', run: openPreview },
|
|
350
|
-
{ id: 'actions', key: 'a', label: 'actions', run: openActions },
|
|
351
|
-
{
|
|
352
|
-
id: 'quit',
|
|
353
|
-
key: 'q',
|
|
354
|
-
label: 'quit',
|
|
355
|
-
run: () => {
|
|
356
|
-
exit();
|
|
357
|
-
onDone(0);
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
];
|
|
306
|
+
const openActions = useCallback(() => {
|
|
307
|
+
setActionIndex(0);
|
|
308
|
+
setModal({ kind: 'actions' });
|
|
309
|
+
}, []);
|
|
310
|
+
const openPreview = useCallback(() => {
|
|
311
|
+
setPreviewScroll(0);
|
|
312
|
+
setModal({ kind: 'preview' });
|
|
313
|
+
}, []);
|
|
314
|
+
// ---- pages --------------------------------------------------------------
|
|
315
|
+
const loadArtifacts = useCallback(async () => {
|
|
316
|
+
const { readdir, stat } = await import('node:fs/promises');
|
|
317
|
+
const { join } = await import('node:path');
|
|
318
|
+
try {
|
|
319
|
+
const names = await readdir(outDir);
|
|
320
|
+
const entries = await Promise.all(names.map(async (name) => ({
|
|
321
|
+
name,
|
|
322
|
+
size: (await stat(join(outDir, name))).size,
|
|
323
|
+
})));
|
|
324
|
+
setArtifacts(entries.sort((a, b) => a.name.localeCompare(b.name)));
|
|
361
325
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
{ id: 'actions', key: 'a', label: 'actions', run: openActions },
|
|
365
|
-
{ id: 'back', key: 'esc', label: 'back to the list', run: () => setScreen('browse') },
|
|
366
|
-
];
|
|
326
|
+
catch {
|
|
327
|
+
setArtifacts([]);
|
|
367
328
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
329
|
+
}, [outDir]);
|
|
330
|
+
const showReport = useCallback(async (title, build) => {
|
|
331
|
+
setModal({ kind: 'report', title, lines: ['working …'] });
|
|
332
|
+
try {
|
|
333
|
+
setModal({ kind: 'report', title, lines: await build() });
|
|
373
334
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
335
|
+
catch (caught) {
|
|
336
|
+
setModal({ kind: 'report', title, lines: [`error: ${caught.message}`] });
|
|
337
|
+
}
|
|
338
|
+
}, []);
|
|
339
|
+
const doctorLines = useCallback(async () => {
|
|
340
|
+
const { adapters } = await import('../adapters/index.js');
|
|
341
|
+
const { resolveAuto, selectionLabel } = await import('../llm/index.js');
|
|
342
|
+
const { installations } = await import('../install.js');
|
|
343
|
+
const out = [];
|
|
344
|
+
for (const adapter of adapters()) {
|
|
345
|
+
const available = adapter.available();
|
|
346
|
+
const count = available ? (await adapter.list({ anyProject: true })).length : 0;
|
|
347
|
+
out.push(`${available ? '✓' : '·'} ${adapter.kind.padEnd(10)} ${adapter.store()} ${available ? `${count} session(s)` : 'not found'}`);
|
|
379
348
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
349
|
+
out.push('');
|
|
350
|
+
out.push('backends');
|
|
351
|
+
const auto = resolveAuto();
|
|
352
|
+
const key = (name) => (process.env[name] ? 'set' : 'not set');
|
|
353
|
+
out.push(` none always available`);
|
|
354
|
+
out.push(` auto resolves to ${selectionLabel(auto)}`);
|
|
355
|
+
out.push(` api:anthropic ANTHROPIC_API_KEY ${key('ANTHROPIC_API_KEY')}`);
|
|
356
|
+
out.push(` api:openai OPENAI_API_KEY ${key('OPENAI_API_KEY')}`);
|
|
357
|
+
out.push(` api:compat/<m> CCOMPACTOR_BASE_URL ${key('CCOMPACTOR_BASE_URL')}, CCOMPACTOR_API_KEY ${key('CCOMPACTOR_API_KEY')}`);
|
|
358
|
+
out.push('');
|
|
359
|
+
out.push('installs on PATH');
|
|
360
|
+
for (const entry of installations()) {
|
|
361
|
+
out.push(` ${entry.version ?? 'did not answer'} ${entry.path}${entry.current ? ' <- this one' : ''}`);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}, []);
|
|
365
|
+
// ---- the frame ----------------------------------------------------------
|
|
366
|
+
// The top bar is three blocks: the brand and the agent tabs on the left, the
|
|
367
|
+
// destination on the right, and whatever space is between them. The left block
|
|
368
|
+
// is laid out through `layoutCells` so the tab columns are real, and the pad is
|
|
369
|
+
// then whatever is left — computed from the laid-out width rather than from the
|
|
370
|
+
// sum of the strings, which is off by one per join and pushed the right-hand
|
|
371
|
+
// block off the end of the row.
|
|
372
|
+
const rightText = `out: ${outDir}${anyProject ? '' : ' · this project'}`;
|
|
373
|
+
const leftCells = [
|
|
374
|
+
{ id: 'brand', text: ' ccompactor ' },
|
|
375
|
+
{ id: 'gap', text: ' ' },
|
|
376
|
+
...tabCells(agents, undefined, columns).map((cell) => ({ id: cell.id, text: cell.text })),
|
|
393
377
|
];
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
378
|
+
const topSpans = layoutCells(leftCells, columns);
|
|
379
|
+
const leftWidth = topSpans.length === 0 ? 0 : topSpans[topSpans.length - 1].end + 1;
|
|
380
|
+
// Three blocks means two joins, and the pad has to leave room for both. One
|
|
381
|
+
// short and the row is a column too wide, which Ink wraps onto a second line
|
|
382
|
+
// and the frame scrolls.
|
|
383
|
+
let right = rightText;
|
|
384
|
+
let pad = columns - leftWidth - right.length - 2;
|
|
385
|
+
if (pad < 1) {
|
|
386
|
+
// The destination is the first thing to go: it is context, not a control.
|
|
387
|
+
right = '';
|
|
388
|
+
pad = columns - leftWidth - 1;
|
|
389
|
+
}
|
|
390
|
+
if (pad < 0)
|
|
391
|
+
pad = 0;
|
|
392
|
+
/** Bottom-bar buttons for the current page. */
|
|
393
|
+
/** Bottom-bar buttons for the current page. */
|
|
394
|
+
const navButtons = [
|
|
395
|
+
{
|
|
396
|
+
id: 'pane',
|
|
397
|
+
key: '←→',
|
|
398
|
+
label: 'pane',
|
|
399
|
+
run: () => setFocus((f) => (f === 'menu' ? 'content' : 'menu')),
|
|
400
|
+
},
|
|
401
|
+
{ id: 'up', key: '↑↓', label: 'move', run: () => move(1) },
|
|
402
|
+
{ id: 'select', key: '⏎', label: 'select', run: () => activate() },
|
|
403
|
+
{ id: 'filter', key: '/', label: 'filter', run: () => beginSearch() },
|
|
404
|
+
{ id: 'back', key: 'esc', label: 'back', run: () => back() },
|
|
405
|
+
{ id: 'help', key: '?', label: 'help', run: () => setModal({ kind: 'help' }) },
|
|
406
|
+
];
|
|
407
|
+
const actButtons = page === 'sessions'
|
|
408
|
+
? [
|
|
409
|
+
{ id: 'look', key: 'v', label: 'look', run: openPreview },
|
|
410
|
+
{ id: 'actions', key: 'a', label: 'actions', run: openActions },
|
|
411
|
+
]
|
|
412
|
+
: page === 'artifacts'
|
|
413
|
+
? [
|
|
414
|
+
{ id: 'verify', key: 'V', label: 'verify', run: () => void run('verify') },
|
|
415
|
+
{ id: 'reload', key: 'r', label: 'reload', run: () => void loadArtifacts() },
|
|
416
|
+
]
|
|
417
|
+
: page === 'skill'
|
|
418
|
+
? [
|
|
419
|
+
{ id: 'install', key: 'i', label: 'install', run: () => void doSkill('install') },
|
|
420
|
+
{ id: 'uninstall', key: 'u', label: 'uninstall', run: () => void doSkill('uninstall') },
|
|
421
|
+
{ id: 'path', key: 'p', label: 'path', run: () => void doSkill('path') },
|
|
422
|
+
]
|
|
423
|
+
: page === 'update'
|
|
424
|
+
? [
|
|
425
|
+
{ id: 'check', key: 'c', label: 'check', run: () => void doUpdate(true) },
|
|
426
|
+
{ id: 'apply', key: 'U', label: 'update', run: () => void doUpdate(false) },
|
|
427
|
+
]
|
|
428
|
+
: [];
|
|
429
|
+
const bar = bottomBar(navButtons.map((b) => ({ id: b.id, key: b.key, label: b.label })), actButtons.map((b) => ({ id: b.id, key: b.key, label: b.label })), columns, rows);
|
|
430
|
+
const barButtons = [...navButtons, ...actButtons];
|
|
431
|
+
const menuSpans = layoutCells(MENU.map((item) => ({ id: `menu:${item.id}`, text: ` ${item.label} ` })), SIDEBAR_WIDTH - 2);
|
|
432
|
+
function move(delta) {
|
|
433
|
+
if (modal) {
|
|
434
|
+
if (modal.kind === 'actions') {
|
|
435
|
+
setActionIndex((i) => Math.min(actions.length - 1, Math.max(0, i + delta)));
|
|
436
|
+
}
|
|
437
|
+
else if (modal.kind === 'target') {
|
|
438
|
+
setTargetIndex((i) => Math.min(targets.length - 1, Math.max(0, i + delta)));
|
|
439
|
+
}
|
|
408
440
|
return;
|
|
409
441
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return;
|
|
413
|
-
const agent = chip.id.slice('agent:'.length);
|
|
414
|
-
if (agent === 'all') {
|
|
415
|
-
setAgentsOff(new Set());
|
|
416
|
-
setCursor(0);
|
|
442
|
+
if (focus === 'menu') {
|
|
443
|
+
setMenuCursor((i) => Math.min(MENU.length - 1, Math.max(0, i + delta)));
|
|
417
444
|
return;
|
|
418
445
|
}
|
|
419
|
-
|
|
446
|
+
if (page === 'sessions') {
|
|
447
|
+
setCursor((c) => Math.min(filtered.length - 1, Math.max(0, c + delta)));
|
|
448
|
+
}
|
|
420
449
|
}
|
|
421
|
-
function
|
|
422
|
-
if (
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
clickCell(event.y, event.x);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
450
|
+
function openPage(next) {
|
|
451
|
+
if (next === 'exit') {
|
|
452
|
+
exit();
|
|
453
|
+
onDone(0);
|
|
454
|
+
return;
|
|
429
455
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
456
|
+
setPage(next);
|
|
457
|
+
setFocus('content');
|
|
458
|
+
setMenuCursor(menuIndex(next));
|
|
459
|
+
if (next === 'artifacts')
|
|
460
|
+
void loadArtifacts();
|
|
461
|
+
if (next === 'doctor')
|
|
462
|
+
void showReport('Doctor', doctorLines);
|
|
463
|
+
}
|
|
464
|
+
function activate() {
|
|
465
|
+
if (modal) {
|
|
466
|
+
if (modal.kind === 'actions')
|
|
467
|
+
return runSelectedAction();
|
|
468
|
+
if (modal.kind === 'target')
|
|
469
|
+
return runSelectedTarget();
|
|
470
|
+
if (modal.kind === 'report' || modal.kind === 'result' || modal.kind === 'help') {
|
|
471
|
+
return setModal(undefined);
|
|
445
472
|
}
|
|
446
473
|
return;
|
|
447
474
|
}
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
475
|
+
if (focus === 'menu')
|
|
476
|
+
return openPage(MENU[menuCursor]?.id ?? 'sessions');
|
|
477
|
+
if (page === 'sessions')
|
|
478
|
+
return openPreview();
|
|
479
|
+
if (page === 'artifacts')
|
|
480
|
+
void run('verify');
|
|
481
|
+
}
|
|
482
|
+
function beginSearch() {
|
|
483
|
+
if (page !== 'sessions')
|
|
453
484
|
return;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
485
|
+
setSearching(true);
|
|
486
|
+
setQuery('');
|
|
487
|
+
}
|
|
488
|
+
function back() {
|
|
489
|
+
if (modal)
|
|
490
|
+
return setModal(undefined);
|
|
491
|
+
if (searching)
|
|
492
|
+
return setSearching(false);
|
|
493
|
+
if (focus === 'content')
|
|
494
|
+
return setFocus('menu');
|
|
495
|
+
setFocus('content');
|
|
496
|
+
}
|
|
497
|
+
async function doSkill(action) {
|
|
498
|
+
await showReport(`Skill · ${action}`, async () => {
|
|
499
|
+
const { install, uninstall, skillTargets, payloadDir } = await import('../skill/index.js');
|
|
500
|
+
if (action === 'install')
|
|
501
|
+
return await install();
|
|
502
|
+
if (action === 'uninstall')
|
|
503
|
+
return await uninstall();
|
|
504
|
+
return [
|
|
505
|
+
`payload: ${payloadDir()}`,
|
|
506
|
+
...skillTargets().map((t) => `target: ${t.dir}`),
|
|
507
|
+
];
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async function doUpdate(check) {
|
|
511
|
+
await showReport(`Update${check ? ' · check' : ''}`, async () => {
|
|
512
|
+
const { update } = await import('../update/index.js');
|
|
513
|
+
const result = await update({ check });
|
|
514
|
+
return [
|
|
515
|
+
`${result.current} → ${result.latest} (${result.kind})`,
|
|
516
|
+
result.location,
|
|
517
|
+
'',
|
|
518
|
+
...result.message.split('\n'),
|
|
519
|
+
];
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
function setAgentFilter(agent) {
|
|
523
|
+
const known = agents.map(([name]) => name);
|
|
524
|
+
setAgentsOff(new Set(known.filter((name) => name !== agent)));
|
|
525
|
+
setCursor(0);
|
|
526
|
+
}
|
|
527
|
+
// ---- mouse --------------------------------------------------------------
|
|
528
|
+
function handleMouse(event) {
|
|
529
|
+
// A modal owns the pointer while it is open. This has to come first: written
|
|
530
|
+
// the other way round, the early return narrowed `modal` to `undefined` and
|
|
531
|
+
// the wheel could never reach the quick look it was scrolling.
|
|
532
|
+
if (modal) {
|
|
533
|
+
if (event.kind === 'wheel-up' || event.kind === 'wheel-down') {
|
|
534
|
+
const delta = event.kind === 'wheel-up' ? -3 : 3;
|
|
535
|
+
if (modal.kind === 'preview') {
|
|
536
|
+
setPreviewScroll((s) => Math.max(0, s + delta));
|
|
537
|
+
}
|
|
538
|
+
else if (modal.kind === 'actions') {
|
|
539
|
+
setActionIndex((i) => Math.min(actions.length - 1, Math.max(0, i + (delta > 0 ? 1 : -1))));
|
|
540
|
+
}
|
|
541
|
+
else if (modal.kind === 'target') {
|
|
542
|
+
setTargetIndex((i) => Math.min(targets.length - 1, Math.max(0, i + (delta > 0 ? 1 : -1))));
|
|
543
|
+
}
|
|
471
544
|
return;
|
|
472
545
|
}
|
|
473
|
-
if (event.kind === 'click') {
|
|
474
|
-
const
|
|
475
|
-
|
|
546
|
+
if (event.kind === 'click' && (modal.kind === 'actions' || modal.kind === 'target')) {
|
|
547
|
+
const itemTop = modalItemTop(modalContent().lines.length);
|
|
548
|
+
// Action dialogs put a detail line under every label, so the clickable
|
|
549
|
+
// stride is two rows, not one.
|
|
550
|
+
const raw = event.y - itemTop;
|
|
551
|
+
const row = modal.kind === 'actions' ? Math.floor(raw / 2) : raw;
|
|
552
|
+
const count = modal.kind === 'actions' ? actions.length : targets.length;
|
|
553
|
+
if (row < 0 || row >= count)
|
|
476
554
|
return;
|
|
477
|
-
|
|
478
|
-
// does everywhere else.
|
|
479
|
-
if (screen === 'actions') {
|
|
555
|
+
if (modal.kind === 'actions') {
|
|
480
556
|
setActionIndex(row);
|
|
481
557
|
const action = actions[row];
|
|
482
558
|
if (!action)
|
|
483
559
|
return;
|
|
484
560
|
if (action.id === 'handoff' || action.id === 'handoff-run') {
|
|
485
561
|
setTargetIndex(0);
|
|
486
|
-
|
|
562
|
+
setModal({ kind: 'target' });
|
|
487
563
|
return;
|
|
488
564
|
}
|
|
489
565
|
void run(action.id);
|
|
@@ -496,11 +572,54 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
496
572
|
void run(action.id, target);
|
|
497
573
|
}
|
|
498
574
|
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (event.kind === 'click') {
|
|
578
|
+
const barCell = bar.cells.find((c) => inCell(c, event.x));
|
|
579
|
+
if (event.y === bar.row && barCell) {
|
|
580
|
+
const button = barButtons.find((b) => b.id === barCell.id.replace(/^(nav|act):/, ''));
|
|
581
|
+
if (button)
|
|
582
|
+
button.run();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (event.y === ROW_TOP) {
|
|
586
|
+
const cell = topSpans.find((c) => inCell(c, event.x));
|
|
587
|
+
if (cell?.id === 'agent:all') {
|
|
588
|
+
setAgentsOff(new Set());
|
|
589
|
+
setCursor(0);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (cell?.id.startsWith('agent:')) {
|
|
593
|
+
setAgentFilter(cell.id.slice('agent:'.length));
|
|
594
|
+
}
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// The sidebar.
|
|
598
|
+
if (event.x <= SIDEBAR_WIDTH) {
|
|
599
|
+
const row = event.y - SIDEBAR_TOP;
|
|
600
|
+
const item = MENU[row];
|
|
601
|
+
if (item) {
|
|
602
|
+
setMenuCursor(row);
|
|
603
|
+
openPage(item.id);
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (page === 'sessions') {
|
|
608
|
+
const row = event.y - SIDEBAR_TOP + 1 + offset;
|
|
609
|
+
if (row >= 0 && row < filtered.length) {
|
|
610
|
+
setCursor(row);
|
|
611
|
+
setFocus('content');
|
|
612
|
+
openPreview();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (event.kind === 'wheel-up' || event.kind === 'wheel-down') {
|
|
618
|
+
const delta = event.kind === 'wheel-up' ? -3 : 3;
|
|
619
|
+
if (page === 'sessions')
|
|
620
|
+
setCursor((c) => Math.min(filtered.length - 1, Math.max(0, c + delta)));
|
|
499
621
|
}
|
|
500
622
|
}
|
|
501
|
-
// The listener is installed once and reads the handler through a ref. Written
|
|
502
|
-
// the other way it has to list every value the handler closes over, and a
|
|
503
|
-
// missed dependency silently freezes a click on a stale screen.
|
|
504
623
|
const mouseRef = useRef(handleMouse);
|
|
505
624
|
mouseRef.current = handleMouse;
|
|
506
625
|
useEffect(() => {
|
|
@@ -520,8 +639,7 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
520
639
|
useInput((input, key) => {
|
|
521
640
|
// Mouse sequences arrive on the same stream and Ink's key parser hands the
|
|
522
641
|
// unrecognised remainder through as typed text, so a click while the search
|
|
523
|
-
// box is open spelled `[<0;3;3M` into the query.
|
|
524
|
-
// for its own use; this stops the leftovers reaching a text field.
|
|
642
|
+
// box is open spelled `[<0;3;3M` into the query.
|
|
525
643
|
if (/\u001b?\[<\d+;\d+;\d+[Mm]/.test(input))
|
|
526
644
|
return;
|
|
527
645
|
if (key.ctrl && input === 'c') {
|
|
@@ -529,51 +647,8 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
529
647
|
onDone(0);
|
|
530
648
|
return;
|
|
531
649
|
}
|
|
532
|
-
if (
|
|
533
|
-
return;
|
|
534
|
-
if (screen === 'done') {
|
|
535
|
-
if (key.return || key.escape || input === 'q') {
|
|
536
|
-
setScreen('browse');
|
|
537
|
-
}
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
if (screen === 'actions') {
|
|
541
|
-
if (key.upArrow)
|
|
542
|
-
setActionIndex((i) => Math.max(0, i - 1));
|
|
543
|
-
if (key.downArrow)
|
|
544
|
-
setActionIndex((i) => Math.min(actions.length - 1, i + 1));
|
|
545
|
-
if (key.escape)
|
|
546
|
-
setScreen('browse');
|
|
547
|
-
if (key.return)
|
|
548
|
-
runSelectedAction();
|
|
650
|
+
if (modal?.kind === 'running')
|
|
549
651
|
return;
|
|
550
|
-
}
|
|
551
|
-
if (screen === 'target') {
|
|
552
|
-
if (key.upArrow)
|
|
553
|
-
setTargetIndex((i) => Math.max(0, i - 1));
|
|
554
|
-
if (key.downArrow)
|
|
555
|
-
setTargetIndex((i) => Math.min(targets.length - 1, i + 1));
|
|
556
|
-
if (key.escape)
|
|
557
|
-
setScreen('actions');
|
|
558
|
-
if (key.return)
|
|
559
|
-
runSelectedTarget();
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (screen === 'preview') {
|
|
563
|
-
if (key.escape || input === 'q') {
|
|
564
|
-
setScreen('browse');
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
const lines = selected ? (cache.get(`${selected.agent}:${selected.id}`)?.preview.length ?? 0) : 0;
|
|
568
|
-
if (key.upArrow)
|
|
569
|
-
setPreviewScroll((s) => Math.max(0, s - 1));
|
|
570
|
-
if (key.downArrow)
|
|
571
|
-
setPreviewScroll((s) => Math.min(Math.max(0, lines - 1), s + 1));
|
|
572
|
-
if (input === 'a' || input === 'h' || key.return)
|
|
573
|
-
openActions();
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
// browse
|
|
577
652
|
if (searching) {
|
|
578
653
|
if (key.return || key.escape)
|
|
579
654
|
setSearching(false);
|
|
@@ -585,123 +660,211 @@ export function App({ outDir, anyProject, onDone }) {
|
|
|
585
660
|
}
|
|
586
661
|
return;
|
|
587
662
|
}
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
onDone(0);
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
if (input === '/') {
|
|
594
|
-
setSearching(true);
|
|
595
|
-
setQuery('');
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
if (input === 'c') {
|
|
599
|
-
setAgentFilter('claude');
|
|
663
|
+
if (key.leftArrow || key.rightArrow || key.tab) {
|
|
664
|
+
setFocus((f) => (f === 'menu' ? 'content' : 'menu'));
|
|
600
665
|
return;
|
|
601
666
|
}
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (
|
|
607
|
-
|
|
667
|
+
if (key.upArrow)
|
|
668
|
+
return move(-1);
|
|
669
|
+
if (key.downArrow)
|
|
670
|
+
return move(1);
|
|
671
|
+
if (key.escape)
|
|
672
|
+
return back();
|
|
673
|
+
if (key.return)
|
|
674
|
+
return activate();
|
|
675
|
+
if (input === '?')
|
|
676
|
+
return setModal({ kind: 'help' });
|
|
677
|
+
if (input === 'q' && !modal) {
|
|
678
|
+
exit();
|
|
679
|
+
onDone(0);
|
|
608
680
|
return;
|
|
609
681
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
// meaning: `a` is actions everywhere, and `0` is the whole list.
|
|
682
|
+
if (input === '/')
|
|
683
|
+
return beginSearch();
|
|
613
684
|
if (input === '0') {
|
|
614
685
|
setAgentsOff(new Set());
|
|
615
686
|
setCursor(0);
|
|
616
687
|
return;
|
|
617
688
|
}
|
|
618
|
-
if (
|
|
619
|
-
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
689
|
+
if (input === 'c')
|
|
690
|
+
return setAgentFilter('claude');
|
|
691
|
+
if (input === 'x')
|
|
692
|
+
return setAgentFilter('codex');
|
|
693
|
+
if (input === 'p')
|
|
694
|
+
return setAgentFilter('pi');
|
|
695
|
+
// The bottom bar is the single definition of what a key does: the key
|
|
696
|
+
// handler looks the key up in the same list the buttons are drawn from, so
|
|
697
|
+
// a shortcut cannot do one thing while its button does another.
|
|
698
|
+
const act = actButtons.find((b) => b.key === input);
|
|
699
|
+
if (act)
|
|
700
|
+
act.run();
|
|
626
701
|
});
|
|
627
|
-
function setAgentFilter(agent) {
|
|
628
|
-
const known = agents.map(([name]) => name);
|
|
629
|
-
setAgentsOff(new Set(known.filter((name) => name !== agent)));
|
|
630
|
-
setCursor(0);
|
|
631
|
-
}
|
|
632
702
|
// ---- render -------------------------------------------------------------
|
|
633
|
-
if (loading)
|
|
634
|
-
return _jsx(Text, {
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (span.id === 'count') {
|
|
642
|
-
return (_jsx(Text, { color: THEME.muted, children: span.text }, span.id));
|
|
643
|
-
}
|
|
644
|
-
const agent = span.id.slice('agent:'.length);
|
|
645
|
-
const on = agent === 'all' ? agentsOff.size === 0 : !agentsOff.has(agent);
|
|
646
|
-
return (_jsx(Text, { ...(on
|
|
703
|
+
if (loading) {
|
|
704
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, backgroundColor: THEME.yellow, color: THEME.onYellow, children: ' ccompactor ' }), _jsx(Text, { color: THEME.yellowInk, children: loading })] }));
|
|
705
|
+
}
|
|
706
|
+
const menuWidth = SIDEBAR_WIDTH - 2;
|
|
707
|
+
const menu = (_jsx(Box, { flexDirection: "column", width: SIDEBAR_WIDTH, borderStyle: "round", borderColor: focus === 'menu' ? THEME.yellow : THEME.stripeDim, children: MENU.map((item, i) => {
|
|
708
|
+
const active = page === item.id;
|
|
709
|
+
const cursorHere = focus === 'menu' && menuCursor === i;
|
|
710
|
+
return (_jsxs(Text, { ...(active
|
|
647
711
|
? { backgroundColor: THEME.yellow, color: THEME.onYellow, bold: true }
|
|
648
|
-
: { color: THEME.muted }), children:
|
|
712
|
+
: { color: THEME.muted }), children: [cursorHere ? '❯' : ' ', item.label.padEnd(menuWidth - 1)] }, item.id));
|
|
649
713
|
}) }));
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
:
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: hazard(columns).map((cell, i) => (_jsx(Text, { color: cell.dim ? THEME.stripeDim : THEME.yellow, children: cell.ch }, i))) }), chips, actionBar, screen === 'browse' && (_jsxs(_Fragment, { children: [_jsx(Columns, { layout: layout, width: columns }), _jsx(Box, { flexDirection: "column", children: filtered.slice(offset, offset + visibleRows).map((ref, i) => {
|
|
663
|
-
const index = offset + i;
|
|
664
|
-
const active = index === cursor;
|
|
665
|
-
const meta = cache.get(`${ref.agent}:${ref.id}`);
|
|
666
|
-
return (_jsx(Row, { ref_: ref, active: active, layout: layout, details: meta }, ref.path));
|
|
667
|
-
}) })] })), screen === 'preview' && selected && (_jsx(Preview, { ref_: selected, details: cache.get(`${selected.agent}:${selected.id}`), scroll: previewScroll, height: visibleRows + 2, width: columns })), screen === 'actions' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: THEME.yellowInk, children: ["What do you want to do with ", selected?.agent, ":", selected?.id.slice(0, 8), "?"] }), actions.map((action, i) => (_jsxs(Text, { ...(i === actionIndex
|
|
668
|
-
? { backgroundColor: THEME.yellow, color: THEME.onYellow, bold: true }
|
|
669
|
-
: {}), children: [i === actionIndex ? '❯ ' : ' ', action.label, _jsxs(Text, { color: i === actionIndex ? THEME.onYellow : THEME.muted, children: [' ', action.detail] })] }, action.id)))] })), screen === 'target' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: THEME.yellowInk, children: "Continue this session in which agent?" }), targets.length === 0 && _jsx(Text, { color: THEME.muted, children: "none found on PATH" }), targets.map((target, i) => (_jsxs(Text, { ...(i === targetIndex
|
|
670
|
-
? { backgroundColor: THEME.yellow, color: THEME.onYellow, bold: true }
|
|
671
|
-
: {}), children: [i === targetIndex ? '❯ ' : ' ', target] }, target)))] })), screen === 'running' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, color: THEME.yellowInk, children: ["working on ", selected?.agent, ":", selected?.id.slice(0, 8), " \u2026 ", elapsedLabel()] }), progress.slice(-6).map((line, i) => (_jsxs(Text, { color: THEME.muted, children: [' ', line] }, i)))] })), screen === 'done' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [error ? (_jsxs(Text, { color: "#FF6B6B", children: ["error: ", error] })) : (_jsx(Text, { bold: true, color: THEME.yellowInk, children: "done" })), results.map((line, i) => (_jsxs(Text, { color: THEME.muted, children: [' ', line] }, i))), lastArtifact && (_jsxs(Text, { color: THEME.muted, children: [' ', "read it: ", lastArtifact] })), _jsx(Text, { color: THEME.muted, children: "press enter to go back" })] })), status.length > 0 && _jsx(Text, { color: THEME.yellowInk, children: status }), searching && screen === 'browse' && (_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: THEME.yellow, children: ["search:", ' '] }), _jsx(Text, { children: query }), _jsx(Text, { color: THEME.yellowInk, children: "\u258F" })] })), screen !== 'done' && screen !== 'running' && hints.length > 0 && (_jsx(Text, { color: THEME.muted, children: hints }))] }));
|
|
672
|
-
function elapsedLabel() {
|
|
673
|
-
return '';
|
|
714
|
+
const contentWidth = Math.max(20, columns - SIDEBAR_WIDTH - 1);
|
|
715
|
+
const content = (_jsx(Box, { flexDirection: "column", width: contentWidth, borderStyle: "round", borderColor: focus === 'content' ? THEME.yellow : THEME.stripeDim, children: contentBody() }));
|
|
716
|
+
function contentBody() {
|
|
717
|
+
if (page === 'sessions')
|
|
718
|
+
return sessionsPage();
|
|
719
|
+
if (page === 'artifacts') {
|
|
720
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: THEME.muted, bold: true, children: [' ', artifacts.length === 0 ? `nothing in ${outDir}` : `${outDir} — ${artifacts.length} file(s)`, ' '] }), artifacts.map((file) => (_jsxs(Text, { color: THEME.muted, children: [' ', file.name.padEnd(24), sizeLabel(file.size).padStart(10)] }, file.name))), _jsxs(Text, { color: THEME.muted, children: [' ', "V verifies the artifact against the transcript; r reloads this list", ' '] })] }));
|
|
721
|
+
}
|
|
722
|
+
if (page === 'settings') {
|
|
723
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: THEME.muted, bold: true, children: [' ', "Where things are written", ' '] }), _jsxs(Text, { children: [' ', "output directory ", ' ', _jsx(Text, { color: THEME.yellowInk, children: outDir })] }), _jsxs(Text, { children: [' ', "session scope ", ' ', _jsx(Text, { color: THEME.yellowInk, children: anyProject ? 'every project' : 'this project only' })] }), _jsxs(Text, { children: [' ', "working directory", ' ', _jsx(Text, { color: THEME.yellowInk, children: process.cwd() })] }), _jsxs(Text, { color: THEME.muted, children: [' ', "Change these with --out and --project on the command line.", ' '] })] }));
|
|
724
|
+
}
|
|
725
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: THEME.muted, bold: true, children: [' ', MENU[menuIndex(page)]?.label ?? page, ' '] }), _jsx(Text, { color: THEME.muted, children: " press enter or click the menu to load this page " })] }));
|
|
674
726
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
727
|
+
function sessionsPage() {
|
|
728
|
+
// One width, used by the header and every row. An agent id is 36 characters
|
|
729
|
+
// and a codex id is 60, so a fixed column either wastes the difference or
|
|
730
|
+
// lets the long ones run into the date.
|
|
731
|
+
const idWidth = Math.max(12, Math.min(38, contentWidth - 34));
|
|
732
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: THEME.muted, bold: true, children: [' ', searching ? `search: ${query}▏` : `${filtered.length} of ${sessions.length} session(s)`, agentsOff.size > 0 && !searching ? ' · filtered' : '', ' '] }), _jsxs(Text, { color: THEME.muted, children: [' ', 'AGENT'.padEnd(8), 'SESSION ID'.padEnd(idWidth + 2), 'MODIFIED'] }), filtered.slice(offset, offset + visibleRows).map((ref, i) => {
|
|
733
|
+
const index = offset + i;
|
|
734
|
+
const active = index === cursor && focus === 'content';
|
|
735
|
+
const details = cache.get(`${ref.agent}:${ref.id}`);
|
|
736
|
+
return (_jsxs(Text, { ...(active ? { backgroundColor: THEME.yellow, color: THEME.onYellow, bold: true } : {}), children: [active ? '❯ ' : ' ', ref.agent.padEnd(8), (ref.id.length > idWidth ? `…${ref.id.slice(-(idWidth - 1))}` : ref.id).padEnd(idWidth), ' ', new Date(ref.mtime).toISOString().slice(0, 16).replace('T', ' '), details && contentWidth > 74 ? ` ${details.messages} msg` : ''] }, ref.path));
|
|
737
|
+
}), filtered.length === 0 && (_jsx(Text, { color: THEME.muted, children: " nothing matches. Press esc to clear, or / to search again. " }))] }));
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* What a dialog contains.
|
|
741
|
+
*
|
|
742
|
+
* One function, used by the drawing *and* by the click mapping, so the row a
|
|
743
|
+
* dialog item is painted on is the row it answers on. Written as two places
|
|
744
|
+
* this is the bug the clickable bar already had once.
|
|
745
|
+
*/
|
|
746
|
+
function modalContent() {
|
|
747
|
+
const width = Math.max(40, Math.min(columns - 8, 96));
|
|
748
|
+
const inner = width - 6;
|
|
749
|
+
const lines = [];
|
|
750
|
+
let title = '';
|
|
751
|
+
let hints = 'esc or ⏎ close';
|
|
752
|
+
let actionable = 0;
|
|
753
|
+
if (!modal)
|
|
754
|
+
return { title, lines, hints, actionable };
|
|
755
|
+
if (modal.kind === 'help') {
|
|
756
|
+
title = 'Keys';
|
|
757
|
+
lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', "\u2190\u2192 or tab move between the menu and the content"] }, "h1"), _jsxs(Text, { color: THEME.muted, children: [' ', "\u2191\u2193 move within the focused pane"] }, "h2"), _jsxs(Text, { color: THEME.muted, children: [' ', "\u23CE select \u00B7 / filter \u00B7 esc back \u00B7 ? help \u00B7 q quit"] }, "h3"), _jsxs(Text, { color: THEME.muted, children: [' ', "Every cell of the top, side and bottom bars is clickable,"] }, "h4"), _jsxs(Text, { color: THEME.muted, children: [' ', "and the mouse wheel scrolls the list and the quick look."] }, "h5"));
|
|
758
|
+
}
|
|
759
|
+
else if (modal.kind === 'actions') {
|
|
760
|
+
title = `What to do with ${selected?.agent}:${selected?.id.slice(0, 8)}`;
|
|
761
|
+
hints = '↑↓ choose · ⏎ run · esc close · click an item to run it';
|
|
762
|
+
actions.forEach((action, i) => {
|
|
763
|
+
const active = i === actionIndex;
|
|
764
|
+
lines.push(_jsxs(Text, { ...(active ? { backgroundColor: THEME.yellow, color: THEME.onYellow, bold: true } : {}), children: [active ? '❯ ' : ' ', action.label] }, action.id));
|
|
765
|
+
lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', action.detail.slice(0, inner - 4)] }, `${action.id}-d`));
|
|
766
|
+
});
|
|
767
|
+
actionable = actions.length;
|
|
768
|
+
}
|
|
769
|
+
else if (modal.kind === 'target') {
|
|
770
|
+
title = 'Continue this session in which agent?';
|
|
771
|
+
hints = '↑↓ choose · ⏎ continue · esc back';
|
|
772
|
+
if (targets.length === 0) {
|
|
773
|
+
lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', "none found on PATH"] }, "none"));
|
|
774
|
+
}
|
|
775
|
+
targets.forEach((target, i) => {
|
|
776
|
+
const active = i === targetIndex;
|
|
777
|
+
lines.push(_jsxs(Text, { ...(active ? { backgroundColor: THEME.yellow, color: THEME.onYellow, bold: true } : {}), children: [active ? '❯ ' : ' ', target] }, target));
|
|
778
|
+
});
|
|
779
|
+
actionable = targets.length;
|
|
780
|
+
}
|
|
781
|
+
else if (modal.kind === 'running') {
|
|
782
|
+
title = `working on ${selected?.agent}:${selected?.id.slice(0, 8)} …`;
|
|
783
|
+
hints = 'this closes itself when the work is done';
|
|
784
|
+
for (const line of progress.slice(-8)) {
|
|
785
|
+
lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', line.slice(0, inner)] }, line));
|
|
786
|
+
}
|
|
787
|
+
if (progress.length === 0) {
|
|
788
|
+
lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', "starting \u2026"] }, "start"));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
else if (modal.kind === 'result') {
|
|
792
|
+
title = error ? 'Something went wrong' : 'Done';
|
|
793
|
+
if (error) {
|
|
794
|
+
lines.push(_jsxs(Text, { color: "#FF6B6B", children: [' ', error.slice(0, inner)] }, "err"));
|
|
795
|
+
}
|
|
796
|
+
results.forEach((line, i) => lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', line.slice(0, inner)] }, i)));
|
|
797
|
+
if (lastArtifact) {
|
|
798
|
+
lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', "read it: ", lastArtifact] }, "art"));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
else if (modal.kind === 'report') {
|
|
802
|
+
title = modal.title;
|
|
803
|
+
for (const line of modal.lines) {
|
|
804
|
+
lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', line.slice(0, inner)] }, line));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
else if (modal.kind === 'preview') {
|
|
808
|
+
const details = selected ? cache.get(`${selected.agent}:${selected.id}`) : undefined;
|
|
809
|
+
title = selected ? `${selected.agent}:${selected.id}` : 'quick look';
|
|
810
|
+
hints = 'wheel or ↑↓ scroll · a actions · esc close';
|
|
811
|
+
if (!details) {
|
|
812
|
+
lines.push(_jsxs(Text, { color: THEME.yellow, children: [' ', "reading \u2026"] }, "l"));
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', details.messages, " msg \u00B7 ", details.userTurns, " user turns \u00B7 ", details.toolCalls, " tool calls \u00B7", ' ', details.tokens, " tokens \u00B7 ", details.compactBoundaries, " compaction(s)"] }, "m"));
|
|
816
|
+
if (details.cwd) {
|
|
817
|
+
lines.push(_jsxs(Text, { color: THEME.muted, children: [' ', "cwd: ", details.cwd.slice(0, inner)] }, "c"));
|
|
818
|
+
}
|
|
819
|
+
const room = Math.max(2, bodyHeight - 8);
|
|
820
|
+
const all = details.preview;
|
|
821
|
+
const shown = all.slice(Math.max(0, all.length - room - previewScroll), all.length - previewScroll);
|
|
822
|
+
shown.forEach((line, i) => lines.push(_jsxs(Text, { children: [_jsx(Text, { color: line.role === 'user' ? THEME.yellow : THEME.muted, bold: line.role === 'user', children: line.role === 'user' ? ' user ' : line.role === 'agent' ? ' agent ' : ' tool ' }), _jsx(Text, { children: line.text.slice(0, inner - 8) })] }, `${line.evt}-${i}`)));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return { title, lines, hints, actionable };
|
|
702
826
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
827
|
+
/**
|
|
828
|
+
* The row the first clickable line of a dialog lands on.
|
|
829
|
+
*
|
|
830
|
+
* Derived from the same numbers the drawing uses rather than guessed: top bar
|
|
831
|
+
* (1) + the padding above the box + the border + the title.
|
|
832
|
+
*/
|
|
833
|
+
function modalItemTop(lineCount) {
|
|
834
|
+
const box = lineCount + 4;
|
|
835
|
+
const padTop = Math.max(0, Math.floor((bodyHeight - box) / 2));
|
|
836
|
+
return padTop + 4;
|
|
837
|
+
}
|
|
838
|
+
/** A dialog, centred in the body. The frame stays visible around it. */
|
|
839
|
+
function modalBox() {
|
|
840
|
+
const { title, lines, hints } = modalContent();
|
|
841
|
+
const width = Math.max(40, Math.min(columns - 8, 96));
|
|
842
|
+
const room = Math.max(3, bodyHeight - 6);
|
|
843
|
+
const shown = lines.slice(0, room);
|
|
844
|
+
const box = shown.length + 4;
|
|
845
|
+
const padTop = Math.max(0, Math.floor((bodyHeight - box) / 2));
|
|
846
|
+
const padLeft = Math.max(0, Math.floor((columns - width) / 2));
|
|
847
|
+
return (_jsxs(Box, { flexDirection: "column", height: bodyHeight, children: [Array.from({ length: padTop }, (_, i) => (_jsx(Text, { children: " " }, `pad${i}`))), _jsxs(Box, { marginLeft: padLeft, flexDirection: "column", width: Math.min(width, columns - padLeft), borderStyle: "double", borderColor: THEME.yellow, children: [_jsx(Text, { bold: true, backgroundColor: THEME.yellow, color: THEME.onYellow, children: ` ${title} `.slice(0, width - 2).padEnd(width - 2) }), shown, _jsx(Text, { color: THEME.muted, children: ` ${hints}`.slice(0, width - 2).padEnd(width - 2) })] })] }));
|
|
848
|
+
}
|
|
849
|
+
return (
|
|
850
|
+
// One row short of the terminal. A frame exactly `rows` tall is written with
|
|
851
|
+
// a trailing newline, which scrolls the top bar off the screen.
|
|
852
|
+
_jsxs(Box, { flexDirection: "column", height: Math.max(6, rows - 1), children: [_jsxs(Box, { children: [topSpans.map((cell, i) => {
|
|
853
|
+
const isTab = cell.id.startsWith('agent:');
|
|
854
|
+
const agent = cell.id.slice('agent:'.length);
|
|
855
|
+
const on = agent === 'all' ? agentsOff.size === 0 : !agentsOff.has(agent);
|
|
856
|
+
const separator = i > 0 ? ' ' : '';
|
|
857
|
+
if (cell.id === 'brand') {
|
|
858
|
+
return (_jsx(Text, { bold: true, backgroundColor: THEME.yellow, color: THEME.onYellow, children: cell.text }, cell.id));
|
|
859
|
+
}
|
|
860
|
+
return (_jsx(Text, { ...(isTab && on
|
|
861
|
+
? { backgroundColor: THEME.yellow, color: THEME.onYellow, bold: true }
|
|
862
|
+
: { color: THEME.muted }), children: separator + cell.text }, `${cell.id}-${i}`));
|
|
863
|
+
}), pad > 0 && _jsx(Text, { color: THEME.muted, children: ' '.repeat(pad) }), right.length > 0 && (_jsx(Text, { color: THEME.muted, children: ` ${right.slice(0, Math.max(0, columns - leftWidth - pad - 1))}` }))] }), modal ? (modalBox()) : (_jsxs(Box, { height: bodyHeight, children: [menu, content] })), _jsx(Box, { children: bar.cells.map((cell, i) => {
|
|
864
|
+
const isLabel = cell.id === 'navlabel' || cell.id === 'actlabel';
|
|
865
|
+
return (_jsx(Text, { ...(isLabel
|
|
866
|
+
? { backgroundColor: THEME.yellow, color: THEME.onYellow, bold: true }
|
|
867
|
+
: { color: THEME.muted }), children: (i > 0 && !isLabel ? '' : '') + cell.text }, `${cell.id}-${i}`));
|
|
868
|
+
}) }), status.length > 0 && _jsx(Text, { color: THEME.yellowInk, children: status })] }));
|
|
706
869
|
}
|
|
707
870
|
//# sourceMappingURL=App.js.map
|