@lardexx/arenacode 2.11.5 → 2.11.7
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 +40 -3
- package/package.json +2 -2
- package/src/cli.js +0 -1
- package/src/diff.js +203 -0
- package/src/tui/app.js +177 -28
- package/src/tui/theme.js +39 -0
- package/src/tui/widgets.js +72 -1
package/README.md
CHANGED
|
@@ -110,6 +110,7 @@ How the copy is built and tested: [`docs/TUI.md`](docs/TUI.md).
|
|
|
110
110
|
| `enter` | send · `shift+enter` newline |
|
|
111
111
|
| `esc` | interrupt the turn · clear the input · quit |
|
|
112
112
|
| `tab` | switch mode: Build → Plan → Computer |
|
|
113
|
+
| `/sessions` (or `ctrl+x l`) | switch between sessions: every timeline is a slot, `n` opens a new one |
|
|
113
114
|
| `ctrl+p` | command palette (`↑↓` select, `enter` run, `esc` close) |
|
|
114
115
|
| `/` | slash commands with an inline list above the prompt |
|
|
115
116
|
| `@file` | attach a file from the open folder (uploaded to `uploads/`) |
|
|
@@ -118,7 +119,7 @@ How the copy is built and tested: [`docs/TUI.md`](docs/TUI.md).
|
|
|
118
119
|
| `!command` | run a command locally |
|
|
119
120
|
| `↑` / `↓` | prompt history (or move through the slash list) |
|
|
120
121
|
| `pgup` / `pgdn` | scroll the transcript |
|
|
121
|
-
| `ctrl+x` then | `q` quit · `n` new session · `l`
|
|
122
|
+
| `ctrl+x` then | `q` quit · `n` new session · `l` switch session · `t` theme · `a` Ask and Proceed / Proceed · `d` tool output · `s` stop |
|
|
122
123
|
| `ctrl+c` | interrupt, then quit |
|
|
123
124
|
|
|
124
125
|
**The same four flows as the desktop client.** The Windows app is the reference for how a task reaches the bridge,
|
|
@@ -220,8 +221,8 @@ Later, `/settings` (or the command palette, `ctrl+p`) reopens them inside the in
|
|
|
220
221
|
| `arenacode --help` | the options below |
|
|
221
222
|
| `arenacode --version` | the version of the installed package |
|
|
222
223
|
|
|
223
|
-
Everything else — URL and API key
|
|
224
|
-
file uploads
|
|
224
|
+
Everything else — URL and API key (`/settings`), sessions (`/sessions`), the task queue on the bridge
|
|
225
|
+
(`/queue`), Stop, file uploads — is inside the interface. Up to 2.11.2 these were subcommands (`init`, `config`, `ping`,
|
|
225
226
|
`doctor`, `ask`, `chat`, `ui`, `tasks`, `status`, `events`, `stop`, `files`, `cat`, `attach`, `handshake`);
|
|
226
227
|
they were removed on request, and the terminal has to be interactive: in a pipe the client prints why and exits.
|
|
227
228
|
|
|
@@ -292,6 +293,42 @@ What happens when:
|
|
|
292
293
|
the send-once-per-session rule and the git exclusion; the integration test checks that a real task creates the
|
|
293
294
|
sheet and that the bridge receives it.
|
|
294
295
|
|
|
296
|
+
### File changes
|
|
297
|
+
|
|
298
|
+
A `write` or a `replace` is not just a line in the transcript: the card shows the **diff**, the way the desktop
|
|
299
|
+
client's `<diff>` widget does. Header with the file and the `+n −m` counts, then the unified rows — old line
|
|
300
|
+
number, new line number, the sign, the text — painted on the diff backgrounds of the active theme
|
|
301
|
+
(`arenacode`, `tokyonight`, `catppuccin`, `gruvbox`, `nord`, colours taken from opencode's own theme assets).
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
┃ ← Modifico il file: src/app.js +2 −1
|
|
305
|
+
┃ @@ -1,4 +1,5 @@
|
|
306
|
+
┃ 1 1 one
|
|
307
|
+
┃ 2 2 two
|
|
308
|
+
┃ 3 3 three
|
|
309
|
+
┃ 4 -four
|
|
310
|
+
┃ 4 +FOUR
|
|
311
|
+
┃ 5 +4.5
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- Hunks keep three lines of context; distant changes become separate hunks with a `⋯` between them.
|
|
315
|
+
- A new file is all additions, a deleted one all deletions; text that did not change draws no card at all.
|
|
316
|
+
- The diff is computed **locally for the display** and never travels to the bridge: the agent still receives the
|
|
317
|
+
same `{ ok, path, size }` it always received.
|
|
318
|
+
- `ctrl+x d` hides the block (it toggles every card); at most 20 rows are drawn, then `… n more lines`, and a
|
|
319
|
+
binary file gets no diff.
|
|
320
|
+
|
|
321
|
+
### Sessions
|
|
322
|
+
|
|
323
|
+
Every timeline in the interface is a **session**: its own transcript, its own task id, its own mode. `/sessions`
|
|
324
|
+
(or `ctrl+x l`) opens the list — the current one is marked with `●` and its label is the first instruction you
|
|
325
|
+
typed in it; `enter` switches to the selected session, `n` (or the `+ new session` row) starts a fresh one.
|
|
326
|
+
`ctrl+x n` is the shortcut for a new session.
|
|
327
|
+
|
|
328
|
+
Sessions live in memory for the run of the client, the way the terminal client always behaved: nothing is
|
|
329
|
+
written to disk, and the transcript of a session that was never used stays empty. The bridge keeps the tasks
|
|
330
|
+
themselves — `/queue` shows them with their state and id.
|
|
331
|
+
|
|
295
332
|
### Shells
|
|
296
333
|
|
|
297
334
|
Terminal cards run in a real shell, and which one is not fixed:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lardexx/arenacode",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.7",
|
|
4
4
|
"description": "Terminal client for ArenaCode: talks to an arena-bridge, streams the Agent Mode timeline, and runs local commands, file operations and computer automation on your PC (windows-use.dll on Windows, Xlib/XTest on Linux, osascript on macOS). Node port of the ArenaCode desktop client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"vendor/"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "node test/cli-surface.js && node test/shell.js && node test/context.js && node test/integration.js && node test/parity.js && node test/automation.js && node test/native.js"
|
|
23
|
+
"test": "node test/cli-surface.js && node test/sessions.js && node test/diff.js && node test/shell.js && node test/context.js && node test/integration.js && node test/parity.js && node test/automation.js && node test/native.js"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"arenacode",
|
package/src/cli.js
CHANGED
|
@@ -379,7 +379,6 @@ async function main(argv) {
|
|
|
379
379
|
const { flags, positional } = parseArgs(argv);
|
|
380
380
|
const command = positional[0];
|
|
381
381
|
|
|
382
|
-
// Only the flags: `arenacode help` and `arenacode version` are not subcommands either.
|
|
383
382
|
if (flags.version) {
|
|
384
383
|
write(VERSION);
|
|
385
384
|
return 0;
|
package/src/diff.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Line diff for the file cards, shaped like the `<diff>` widget the desktop client shows:
|
|
4
|
+
// unified, with `@@` hunk headers, three lines of context and the two line numbers
|
|
5
|
+
// (opencode's diffs go through the same idea: hunks + `+`/`-` rows + line numbers).
|
|
6
|
+
//
|
|
7
|
+
// The result is only for display: it never travels to the bridge with the tool output.
|
|
8
|
+
|
|
9
|
+
// n*m guard: a 20 000 x 20 000 table is 400 M cells, not something to build in a terminal.
|
|
10
|
+
const CELLS_MAX = 4_000_000;
|
|
11
|
+
|
|
12
|
+
/** Lines of a text, ignoring a single trailing newline (a file "ends with \\n", not with a line). */
|
|
13
|
+
function splitLines(text) {
|
|
14
|
+
const s = String(text ?? '');
|
|
15
|
+
if (!s) return [];
|
|
16
|
+
const lines = s.split('\n');
|
|
17
|
+
if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop();
|
|
18
|
+
return lines;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Longest common subsequence over two arrays of lines.
|
|
23
|
+
* Returns rows of { type: 'ctx'|'del'|'add', a, b } with 0-based indices.
|
|
24
|
+
*/
|
|
25
|
+
function lcsRows(a, b) {
|
|
26
|
+
const n = a.length;
|
|
27
|
+
const m = b.length;
|
|
28
|
+
const rows = [];
|
|
29
|
+
|
|
30
|
+
if (n === 0 && m === 0) return rows;
|
|
31
|
+
if (n === 0) return b.map((_, j) => ({ type: 'add', a: -1, b: j }));
|
|
32
|
+
if (m === 0) return a.map((_, i) => ({ type: 'del', a: i, b: -1 }));
|
|
33
|
+
|
|
34
|
+
if (n * m > CELLS_MAX) {
|
|
35
|
+
// too big for a table: honest linear answer, every old line goes, every new line comes
|
|
36
|
+
for (let i = 0; i < n; i += 1) rows.push({ type: 'del', a: i, b: -1 });
|
|
37
|
+
for (let j = 0; j < m; j += 1) rows.push({ type: 'add', a: -1, b: j });
|
|
38
|
+
return rows;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// lcs[i][j] = length of the LCS of a[i..] and b[j..]
|
|
42
|
+
const width = m + 1;
|
|
43
|
+
const lcs = new Uint32Array((n + 1) * width);
|
|
44
|
+
for (let i = n - 1; i >= 0; i -= 1) {
|
|
45
|
+
for (let j = m - 1; j >= 0; j -= 1) {
|
|
46
|
+
lcs[i * width + j] =
|
|
47
|
+
a[i] === b[j]
|
|
48
|
+
? lcs[(i + 1) * width + j + 1] + 1
|
|
49
|
+
: Math.max(lcs[(i + 1) * width + j], lcs[i * width + j + 1]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let i = 0;
|
|
54
|
+
let j = 0;
|
|
55
|
+
while (i < n && j < m) {
|
|
56
|
+
if (a[i] === b[j]) {
|
|
57
|
+
rows.push({ type: 'ctx', a: i, b: j });
|
|
58
|
+
i += 1;
|
|
59
|
+
j += 1;
|
|
60
|
+
} else if (lcs[(i + 1) * width + j] >= lcs[i * width + j + 1]) {
|
|
61
|
+
rows.push({ type: 'del', a: i, b: -1 });
|
|
62
|
+
i += 1;
|
|
63
|
+
} else {
|
|
64
|
+
rows.push({ type: 'add', a: -1, b: j });
|
|
65
|
+
j += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
while (i < n) {
|
|
69
|
+
rows.push({ type: 'del', a: i, b: -1 });
|
|
70
|
+
i += 1;
|
|
71
|
+
}
|
|
72
|
+
while (j < m) {
|
|
73
|
+
rows.push({ type: 'add', a: -1, b: j });
|
|
74
|
+
j += 1;
|
|
75
|
+
}
|
|
76
|
+
return rows;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Groups rows into hunks, keeping `context` unchanged lines around each change. */
|
|
80
|
+
function hunks(rows, context) {
|
|
81
|
+
const keep = new Set();
|
|
82
|
+
rows.forEach((row, index) => {
|
|
83
|
+
if (row.type === 'ctx') return;
|
|
84
|
+
for (let k = Math.max(0, index - context); k <= Math.min(rows.length - 1, index + context); k += 1) {
|
|
85
|
+
keep.add(k);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const out = [];
|
|
90
|
+
let previous = -2;
|
|
91
|
+
rows.forEach((row, index) => {
|
|
92
|
+
if (!keep.has(index)) return;
|
|
93
|
+
if (index !== previous + 1 && previous !== -2) out.push({ type: 'gap' }); // between two distant hunks
|
|
94
|
+
out.push(row);
|
|
95
|
+
previous = index;
|
|
96
|
+
});
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Unified diff of two texts.
|
|
102
|
+
* @returns {{additions:number, deletions:number, rows:Array, changed:boolean}}
|
|
103
|
+
* rows: { type:'ctx'|'add'|'del'|'gap'|'hunk', oldNo:number|null, newNo:number|null, text:string }
|
|
104
|
+
*/
|
|
105
|
+
function lineDiff(before, after, { context = 3 } = {}) {
|
|
106
|
+
const a = splitLines(before);
|
|
107
|
+
const b = splitLines(after);
|
|
108
|
+
const raw = lcsRows(a, b);
|
|
109
|
+
|
|
110
|
+
let additions = 0;
|
|
111
|
+
let deletions = 0;
|
|
112
|
+
for (const row of raw) {
|
|
113
|
+
if (row.type === 'add') additions += 1;
|
|
114
|
+
else if (row.type === 'del') deletions += 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const rows = [];
|
|
118
|
+
for (const row of hunks(raw, Math.max(0, context))) {
|
|
119
|
+
if (row.type === 'gap') {
|
|
120
|
+
rows.push({ type: 'gap', oldNo: null, newNo: null, text: '…' });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (row.type === 'ctx') {
|
|
124
|
+
rows.push({ type: 'ctx', oldNo: row.a + 1, newNo: row.b + 1, text: a[row.a] });
|
|
125
|
+
} else if (row.type === 'del') {
|
|
126
|
+
rows.push({ type: 'del', oldNo: row.a + 1, newNo: null, text: a[row.a] });
|
|
127
|
+
} else {
|
|
128
|
+
rows.push({ type: 'add', oldNo: null, newNo: row.b + 1, text: b[row.b] });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// `@@ -oldStart,oldCount +newStart,newCount @@` before each run of kept rows
|
|
133
|
+
const withHeaders = [];
|
|
134
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
135
|
+
const row = rows[index];
|
|
136
|
+
const startsHunk = row.type !== 'gap' && (index === 0 || rows[index - 1].type === 'gap' || rows[index - 1].type === 'hunk');
|
|
137
|
+
if (startsHunk) {
|
|
138
|
+
let oldStart = null;
|
|
139
|
+
let newStart = null;
|
|
140
|
+
let oldCount = 0;
|
|
141
|
+
let newCount = 0;
|
|
142
|
+
for (let k = index; k < rows.length && rows[k].type !== 'gap'; k += 1) {
|
|
143
|
+
const r = rows[k];
|
|
144
|
+
if (r.oldNo !== null) {
|
|
145
|
+
if (oldStart === null) oldStart = r.oldNo;
|
|
146
|
+
oldCount += 1;
|
|
147
|
+
}
|
|
148
|
+
if (r.newNo !== null) {
|
|
149
|
+
if (newStart === null) newStart = r.newNo;
|
|
150
|
+
newCount += 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
withHeaders.push({
|
|
154
|
+
type: 'hunk',
|
|
155
|
+
oldNo: null,
|
|
156
|
+
newNo: null,
|
|
157
|
+
text: `@@ -${oldStart === null ? 0 : oldStart},${oldCount} +${newStart === null ? 0 : newStart},${newCount} @@`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
withHeaders.push(row);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { additions, deletions, rows: withHeaders, changed: additions > 0 || deletions > 0 };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `+12 −3` for the card header. */
|
|
167
|
+
function summary(diff) {
|
|
168
|
+
if (!diff) return '';
|
|
169
|
+
return `+${diff.additions} −${diff.deletions}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Renders rows as plain text lines (no colour) — used by the tests and by any
|
|
174
|
+
* non-TTY consumer that wants to show a change.
|
|
175
|
+
*/
|
|
176
|
+
function renderRows(rows, { width = 0, max = Infinity } = {}) {
|
|
177
|
+
const out = [];
|
|
178
|
+
const numbers = rows.some((r) => r.oldNo !== null || r.newNo !== null);
|
|
179
|
+
const oldWidth = numbers ? Math.max(3, ...rows.map((r) => String(r.oldNo ?? '').length)) : 0;
|
|
180
|
+
const newWidth = numbers ? Math.max(3, ...rows.map((r) => String(r.newNo ?? '').length)) : 0;
|
|
181
|
+
for (const row of rows.slice(0, max)) {
|
|
182
|
+
if (row.type === 'hunk') {
|
|
183
|
+
out.push(numbers ? `${' '.repeat(oldWidth)} ${' '.repeat(newWidth)} ${row.text}` : row.text);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (row.type === 'gap') {
|
|
187
|
+
out.push(' '.repeat(oldWidth + newWidth + 3) + '⋯');
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const sign = row.type === 'add' ? '+' : row.type === 'del' ? '-' : ' ';
|
|
191
|
+
const oldNo = row.oldNo === null ? '' : String(row.oldNo);
|
|
192
|
+
const newNo = row.newNo === null ? '' : String(row.newNo);
|
|
193
|
+
let text = numbers
|
|
194
|
+
? `${oldNo.padStart(oldWidth)} ${newNo.padStart(newWidth)} ${sign}${row.text}`
|
|
195
|
+
: `${sign}${row.text}`;
|
|
196
|
+
if (width > 0 && text.length > width) text = `${text.slice(0, Math.max(1, width - 1))}…`;
|
|
197
|
+
out.push(text);
|
|
198
|
+
}
|
|
199
|
+
if (rows.length > max) out.push(`… ${rows.length - max} more lines`);
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = { splitLines, lineDiff, summary, renderRows };
|
package/src/tui/app.js
CHANGED
|
@@ -15,6 +15,7 @@ const { getTheme, themeNames } = require('./theme');
|
|
|
15
15
|
const { Bridge } = require('../bridge');
|
|
16
16
|
const { buildMeta, readProjectContext } = require('../meta');
|
|
17
17
|
const { runShell, runFsOp, resolveInRoot, detectShell, availableShells, shellCommand } = require('../local');
|
|
18
|
+
const { lineDiff } = require('../diff');
|
|
18
19
|
const { readConfig, patchConfig, deviceId, resolveUrl, resolveKey, openFolder, configPath, VERSION } = require('../config');
|
|
19
20
|
const { ensureContextFile, touchSession } = require('../context');
|
|
20
21
|
|
|
@@ -80,6 +81,12 @@ class App {
|
|
|
80
81
|
lastError: null,
|
|
81
82
|
};
|
|
82
83
|
|
|
84
|
+
// Sessions: every timeline is a slot the user can switch back to (`/sessions`).
|
|
85
|
+
this.sessions = [];
|
|
86
|
+
this.sessionIndex = 0;
|
|
87
|
+
this.sessionSeq = 0;
|
|
88
|
+
this.newSession({ silent: true });
|
|
89
|
+
|
|
83
90
|
this.term = new Term();
|
|
84
91
|
this.parser = new KeyParser((key) => this.onKey(key));
|
|
85
92
|
this.localQueue = [];
|
|
@@ -279,16 +286,11 @@ class App {
|
|
|
279
286
|
case 'q':
|
|
280
287
|
return this.quit();
|
|
281
288
|
case 'n':
|
|
282
|
-
|
|
283
|
-
st.seqItems.clear();
|
|
284
|
-
st.taskId = null;
|
|
285
|
-
st.since = 0;
|
|
286
|
-
this.toast('new session');
|
|
287
|
-
return undefined;
|
|
289
|
+
return this.newSession();
|
|
288
290
|
case 'h':
|
|
289
291
|
return this.openHelp();
|
|
290
292
|
case 'l':
|
|
291
|
-
return this.
|
|
293
|
+
return this.openSessions();
|
|
292
294
|
case 't': {
|
|
293
295
|
const names = themeNames();
|
|
294
296
|
const next = names[(names.indexOf(this.themeName) + 1) % names.length];
|
|
@@ -427,6 +429,76 @@ class App {
|
|
|
427
429
|
return undefined;
|
|
428
430
|
}
|
|
429
431
|
|
|
432
|
+
// ---------------------------------------------------------------- sessions
|
|
433
|
+
|
|
434
|
+
/** Copies the live timeline into the active session slot. */
|
|
435
|
+
saveSession() {
|
|
436
|
+
const st = this.state;
|
|
437
|
+
const slot = this.sessions[this.sessionIndex];
|
|
438
|
+
if (!slot) return;
|
|
439
|
+
slot.items = st.items;
|
|
440
|
+
slot.seqItems = st.seqItems;
|
|
441
|
+
slot.taskId = st.taskId;
|
|
442
|
+
slot.since = st.since;
|
|
443
|
+
slot.mode = st.mode;
|
|
444
|
+
if (!slot.label) {
|
|
445
|
+
const first = st.items.find((i) => i.kind === 'user' && i.text);
|
|
446
|
+
if (first) slot.label = String(first.text).replace(/@\S+/g, '').replace(/\s+/g, ' ').trim().slice(0, 48);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Loads a slot into the live state. */
|
|
451
|
+
applySession() {
|
|
452
|
+
const st = this.state;
|
|
453
|
+
const slot = this.sessions[this.sessionIndex];
|
|
454
|
+
st.items = slot ? slot.items : [];
|
|
455
|
+
st.seqItems = slot ? slot.seqItems : new Map();
|
|
456
|
+
st.taskId = slot ? slot.taskId : null;
|
|
457
|
+
st.since = slot ? slot.since : 0;
|
|
458
|
+
st.scroll = 0;
|
|
459
|
+
st.overlay = null;
|
|
460
|
+
st.approval = null;
|
|
461
|
+
st.status = 'idle';
|
|
462
|
+
this.invalidate();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
newSession({ silent = false } = {}) {
|
|
466
|
+
this.saveSession();
|
|
467
|
+
this.sessionSeq += 1;
|
|
468
|
+
this.sessions.push({
|
|
469
|
+
id: this.sessionSeq,
|
|
470
|
+
label: '',
|
|
471
|
+
items: [],
|
|
472
|
+
seqItems: new Map(),
|
|
473
|
+
taskId: null,
|
|
474
|
+
since: 0,
|
|
475
|
+
mode: this.state.mode,
|
|
476
|
+
startedAt: Date.now(),
|
|
477
|
+
});
|
|
478
|
+
this.sessionIndex = this.sessions.length - 1;
|
|
479
|
+
this.applySession();
|
|
480
|
+
if (!silent) this.toast(`session ${this.sessionSeq} · new`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
activateSession(index) {
|
|
484
|
+
if (index < 0 || index >= this.sessions.length || index === this.sessionIndex) {
|
|
485
|
+
this.state.overlay = null;
|
|
486
|
+
this.invalidate();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this.saveSession();
|
|
490
|
+
this.sessionIndex = index;
|
|
491
|
+
this.applySession();
|
|
492
|
+
const slot = this.sessions[index];
|
|
493
|
+
this.toast(`session ${slot.id}${slot.label ? ` · ${slot.label}` : ''}`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async openSessions() {
|
|
497
|
+
this.saveSession();
|
|
498
|
+
this.state.overlay = { type: 'sessions', index: this.sessionIndex };
|
|
499
|
+
this.invalidate();
|
|
500
|
+
}
|
|
501
|
+
|
|
430
502
|
// ---------------------------------------------------------------- overlays
|
|
431
503
|
openPalette() {
|
|
432
504
|
this.state.overlay = { type: 'palette', query: '', index: 0 };
|
|
@@ -439,7 +511,7 @@ class App {
|
|
|
439
511
|
}
|
|
440
512
|
|
|
441
513
|
async openTasks() {
|
|
442
|
-
this.state.overlay = { type: '
|
|
514
|
+
this.state.overlay = { type: 'queue', loading: true, tasks: [], index: 0 };
|
|
443
515
|
this.invalidate();
|
|
444
516
|
const res = await this.bridge.listTasks('all');
|
|
445
517
|
if (res.status === 200) {
|
|
@@ -462,7 +534,8 @@ class App {
|
|
|
462
534
|
this.state.since = 0;
|
|
463
535
|
this.toast('new session');
|
|
464
536
|
} },
|
|
465
|
-
{ title: '
|
|
537
|
+
{ title: 'Sessions (switch between sessions)', hint: 'ctrl+x l', slash: '/sessions', run: () => this.openSessions() },
|
|
538
|
+
{ title: 'Queue: tasks on the bridge', hint: '', slash: '/queue', run: () => this.openTasks() },
|
|
466
539
|
{ title: 'Toggle detail', hint: 'ctrl+x d', run: () => {
|
|
467
540
|
this.state.details = !this.state.details;
|
|
468
541
|
this.toast(`details ${this.state.details ? 'on' : 'off'}`);
|
|
@@ -485,13 +558,14 @@ class App {
|
|
|
485
558
|
if (h) this.push({ kind: 'system', text: `bridge v${h.version} · ${h.executor_mode} · claimable ${h.tasks.claimable}` });
|
|
486
559
|
else this.toast('bridge unreachable', 'error');
|
|
487
560
|
} },
|
|
488
|
-
{ title: 'Settings', slash: '/settings', hint: '', run: () => this.openSettings() },
|
|
561
|
+
{ title: 'Settings: bridge URL and API key', slash: '/settings', hint: '', run: () => this.openSettings() },
|
|
489
562
|
{ title: 'Help', hint: 'f1', run: () => this.openHelp() },
|
|
490
563
|
{ title: 'Quit', hint: 'ctrl+x q', run: () => this.quit() },
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
564
|
+
].map((cmd) => ({
|
|
565
|
+
...cmd,
|
|
566
|
+
slash: cmd.slash || '/' + cmd.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, ''),
|
|
567
|
+
group: 'Commands',
|
|
568
|
+
}));
|
|
495
569
|
}
|
|
496
570
|
|
|
497
571
|
/** `/help`, `/new`, `/tasks` … — opencode-style slash commands */
|
|
@@ -502,7 +576,9 @@ class App {
|
|
|
502
576
|
};
|
|
503
577
|
return [
|
|
504
578
|
{ slash: '/new', title: 'Start a new session', hint: 'ctrl+x n', group: 'Session', run: by('New session') },
|
|
505
|
-
{ slash: '/
|
|
579
|
+
{ slash: '/sessions', title: 'Switch between sessions', hint: 'ctrl+x l', group: 'Session', run: () => this.openSessions() },
|
|
580
|
+
{ slash: '/settings', title: 'Bridge URL and API key', hint: '', group: 'Session', run: () => this.openSettings() },
|
|
581
|
+
{ slash: '/queue', title: 'Tasks on the bridge', hint: '', group: 'Session', run: () => this.openTasks() },
|
|
506
582
|
{ slash: '/mode', title: 'Switch agent / plan', hint: 'tab', group: 'Session', run: by('Switch mode') },
|
|
507
583
|
{ slash: '/ask', title: 'Ask and Proceed: ask before commands, files and plans', hint: '', group: 'Session', run: () => this.setApproval('ask') },
|
|
508
584
|
{ slash: '/proceed', title: 'Proceed: approve the plan and run everything without asking', hint: '', group: 'Session', run: () => this.setApproval('proceed') },
|
|
@@ -654,7 +730,23 @@ class App {
|
|
|
654
730
|
return undefined;
|
|
655
731
|
}
|
|
656
732
|
|
|
657
|
-
if (ov.type === '
|
|
733
|
+
if (ov.type === 'sessions') {
|
|
734
|
+
const rows = this.sessions.length + 1; // + "new session"
|
|
735
|
+
if (key.name === 'up') ov.index = Math.max(0, ov.index - 1);
|
|
736
|
+
else if (key.name === 'down') ov.index = Math.min(rows - 1, ov.index + 1);
|
|
737
|
+
else if (key.name === 'enter') {
|
|
738
|
+
if (ov.index >= this.sessions.length) this.newSession();
|
|
739
|
+
else this.activateSession(ov.index);
|
|
740
|
+
this.state.overlay = null;
|
|
741
|
+
} else if (key.name === 'n') {
|
|
742
|
+
this.state.overlay = null;
|
|
743
|
+
this.newSession();
|
|
744
|
+
}
|
|
745
|
+
this.invalidate();
|
|
746
|
+
return undefined;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (ov.type === 'queue') {
|
|
658
750
|
if (key.name === 'up') {
|
|
659
751
|
ov.index = Math.max(0, ov.index - 1);
|
|
660
752
|
} else if (key.name === 'down') {
|
|
@@ -699,6 +791,12 @@ class App {
|
|
|
699
791
|
return undefined;
|
|
700
792
|
}
|
|
701
793
|
|
|
794
|
+
/** A list dialog draws group rows too: the selection index must skip them. */
|
|
795
|
+
sessionRowIndex(index) {
|
|
796
|
+
const at = Math.max(0, Math.min(this.sessions.length, index));
|
|
797
|
+
return 1 + at + (at >= this.sessions.length ? 1 : 0);
|
|
798
|
+
}
|
|
799
|
+
|
|
702
800
|
filteredCommands() {
|
|
703
801
|
const ov = this.state.overlay;
|
|
704
802
|
const all = this.commands();
|
|
@@ -818,6 +916,7 @@ class App {
|
|
|
818
916
|
}
|
|
819
917
|
st.taskId = res.json.task_id;
|
|
820
918
|
session.remember(st.taskId, st.mode);
|
|
919
|
+
this.saveSession();
|
|
821
920
|
st.since = 0;
|
|
822
921
|
st.seqItems.clear();
|
|
823
922
|
}
|
|
@@ -1067,12 +1166,42 @@ class App {
|
|
|
1067
1166
|
}
|
|
1068
1167
|
item.state = 'running';
|
|
1069
1168
|
this.invalidate();
|
|
1169
|
+
|
|
1170
|
+
// A file change is worth seeing: read the old text now, the new one after the write,
|
|
1171
|
+
// and put the diff on the card. It is display only — the bridge gets what it always got.
|
|
1172
|
+
const trackDiff = (op === 'write' || op === 'replace') && !payload.append;
|
|
1173
|
+
const target = trackDiff && payload.path ? resolveInRoot(this.folder, payload.path) : null;
|
|
1174
|
+
const readText = (file) => {
|
|
1175
|
+
try {
|
|
1176
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
1177
|
+
return text.includes('\u0000') ? null : text; // a binary file has no diff
|
|
1178
|
+
} catch {
|
|
1179
|
+
return '';
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
const before = target ? readText(target) : null;
|
|
1183
|
+
|
|
1070
1184
|
let result;
|
|
1071
1185
|
try {
|
|
1072
1186
|
result = runFsOp(op, payload, this.folder);
|
|
1073
1187
|
} catch (e) {
|
|
1074
1188
|
result = { ok: false, error: e.message };
|
|
1075
1189
|
}
|
|
1190
|
+
|
|
1191
|
+
if (target && result.ok && before !== null && (op === 'write' || op === 'replace')) {
|
|
1192
|
+
const after = readText(target);
|
|
1193
|
+
if (after !== null) {
|
|
1194
|
+
const change = lineDiff(before ?? '', after);
|
|
1195
|
+
if (change.changed) {
|
|
1196
|
+
item.diff = {
|
|
1197
|
+
file: payload.path || '',
|
|
1198
|
+
additions: change.additions,
|
|
1199
|
+
deletions: change.deletions,
|
|
1200
|
+
rows: change.rows,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1076
1205
|
await this.bridge.localResult(taskId, { rid: payload.rid, output: JSON.stringify(result), code: result.ok ? 0 : 1 });
|
|
1077
1206
|
item.state = result.ok ? 'done' : 'failed';
|
|
1078
1207
|
item.meta = result.ok ? (result.size ? `${result.size} B` : op) : result.error;
|
|
@@ -1445,16 +1574,11 @@ class App {
|
|
|
1445
1574
|
|
|
1446
1575
|
slashList() {
|
|
1447
1576
|
const q = this.state.input.slice(1).toLowerCase().split(' ')[0];
|
|
1448
|
-
//
|
|
1449
|
-
//
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
if (!c.slash || seen.has(c.slash)) return false;
|
|
1454
|
-
seen.add(c.slash);
|
|
1455
|
-
return true;
|
|
1456
|
-
});
|
|
1457
|
-
return all.filter((c) => !q || c.slash.replace('/', '').startsWith(q)).slice(0, 7);
|
|
1577
|
+
// Only the curated list: merging commands() in brought the auto-generated names
|
|
1578
|
+
// (/new-session, /bridge-status-ping …) next to the good ones and showed /settings twice.
|
|
1579
|
+
return this.slashCommands()
|
|
1580
|
+
.filter((c) => !q || c.slash.replace('/', '').startsWith(q))
|
|
1581
|
+
.slice(0, 7);
|
|
1458
1582
|
}
|
|
1459
1583
|
|
|
1460
1584
|
overlaySlash(frame, W, H, list) {
|
|
@@ -1514,12 +1638,35 @@ class App {
|
|
|
1514
1638
|
items.push({ label, hint: key, group: '' });
|
|
1515
1639
|
}
|
|
1516
1640
|
dialog = D.listDialog(t, { W, size: 'large', title: 'Help', filter: '', items, index: -1, footer: [['esc', 'close']] });
|
|
1641
|
+
} else if (ov.type === 'sessions') {
|
|
1642
|
+
const items = [{ group: 'Sessions' }];
|
|
1643
|
+
this.sessions.forEach((slot, i) => {
|
|
1644
|
+
const running = i === this.sessionIndex;
|
|
1645
|
+
const label = slot.label || (slot.taskId ? `task ${slot.taskId}` : 'empty session');
|
|
1646
|
+
const hint = `${slot.items.length ? `${slot.items.length} lines` : ''}${running ? ' · current' : ''}`.trim();
|
|
1647
|
+
items.push({ label: `${running ? '●' : ' '} ${slot.id} ${R.vclip(label, 44)}`, hint, grouped: true });
|
|
1648
|
+
});
|
|
1649
|
+
items.push({ group: '' });
|
|
1650
|
+
items.push({ label: '+ new session', hint: 'n', grouped: true });
|
|
1651
|
+
dialog = D.listDialog(t, {
|
|
1652
|
+
W,
|
|
1653
|
+
size: 'large',
|
|
1654
|
+
title: 'Sessions',
|
|
1655
|
+
filter: '',
|
|
1656
|
+
items,
|
|
1657
|
+
index: this.sessionRowIndex(ov.index),
|
|
1658
|
+
footer: [
|
|
1659
|
+
['enter', 'switch'],
|
|
1660
|
+
['n', 'new'],
|
|
1661
|
+
['esc', 'close'],
|
|
1662
|
+
],
|
|
1663
|
+
});
|
|
1517
1664
|
} else {
|
|
1518
1665
|
const items = [{ group: 'Queue' }];
|
|
1519
1666
|
if (ov.loading) items.push({ label: 'loading…', dim: true });
|
|
1520
1667
|
else if (!ov.tasks.length) items.push({ label: 'no tasks', dim: true });
|
|
1521
1668
|
else for (const task of ov.tasks.slice(0, Math.max(4, H - 12))) items.push({ label: `${task.state} ${R.vclip(task.id, 22)} ${R.vclip(String(task.instruction).split('\n').slice(-1)[0], 40)}`, grouped: true });
|
|
1522
|
-
dialog = D.listDialog(t, { W, size: 'large', title: '
|
|
1669
|
+
dialog = D.listDialog(t, { W, size: 'large', title: 'Queue', filter: '', items, index: -1, footer: [['esc', 'close']] });
|
|
1523
1670
|
}
|
|
1524
1671
|
|
|
1525
1672
|
const y = D.dialogY(H, dialog.lines.length);
|
|
@@ -1596,7 +1743,9 @@ function HELP_ROWS() {
|
|
|
1596
1743
|
['Prompt', '!', 'run a command locally'],
|
|
1597
1744
|
['Prompt', 'up / down', 'history'],
|
|
1598
1745
|
['Session', 'ctrl+x n', 'new session'],
|
|
1599
|
-
['Session', 'ctrl+x l', '
|
|
1746
|
+
['Session', 'ctrl+x l', 'switch session'],
|
|
1747
|
+
['Session', '/sessions', 'switch between sessions'],
|
|
1748
|
+
['Session', '/queue', 'tasks on the bridge'],
|
|
1600
1749
|
['Session', 'ctrl+x a', 'Ask and Proceed / Proceed'],
|
|
1601
1750
|
['Session', 'ctrl+x t', 'switch theme'],
|
|
1602
1751
|
['Session', '/settings', 'bridge URL and API key'],
|
package/src/tui/theme.js
CHANGED
|
@@ -119,6 +119,17 @@ function makeTheme(name, c) {
|
|
|
119
119
|
hex: c,
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
+
// Diff palette: the literal values come from opencode's own theme assets where it has
|
|
123
|
+
// them; the rest is derived so every theme can draw the same widget.
|
|
124
|
+
t.diff = {
|
|
125
|
+
add: { fg: c.diffAdded, bg: c.diffAddedBg || mix(c.background, c.diffAdded, 0.22), noBg: c.diffAddedLineNumberBg || c.backgroundElement },
|
|
126
|
+
del: { fg: c.diffRemoved, bg: c.diffRemovedBg || mix(c.background, c.diffRemoved, 0.22), noBg: c.diffRemovedLineNumberBg || c.backgroundElement },
|
|
127
|
+
ctx: { fg: c.textMuted, bg: c.diffContextBg || c.backgroundElement, noBg: c.diffContextBg || c.backgroundElement },
|
|
128
|
+
number: c.diffLineNumber || c.textMuted,
|
|
129
|
+
signAdd: c.diffHighlightAdded || c.diffAdded,
|
|
130
|
+
signDel: c.diffHighlightRemoved || c.diffRemoved,
|
|
131
|
+
};
|
|
132
|
+
|
|
122
133
|
/** foreground + background at once, used by the prompt box and dialogs */
|
|
123
134
|
t.both = (color, on) => (s) => `${fgSeq(color)}${bgSeq(on)}${s}${RESET}`;
|
|
124
135
|
/** selection row: primary background, background foreground (opencode's active item) */
|
|
@@ -148,6 +159,13 @@ const PALETTES = {
|
|
|
148
159
|
borderSubtle: '#3c3c3c',
|
|
149
160
|
diffAdded: '#4fd6be',
|
|
150
161
|
diffRemoved: '#c53b53',
|
|
162
|
+
diffAddedBg: '#20303b',
|
|
163
|
+
diffRemovedBg: '#37222c',
|
|
164
|
+
diffLineNumber: '#8f8f8f',
|
|
165
|
+
diffHighlightAdded: '#b8db87',
|
|
166
|
+
diffHighlightRemoved: '#e26a75',
|
|
167
|
+
diffAddedLineNumberBg: '#1b2b34',
|
|
168
|
+
diffRemovedLineNumberBg: '#2d1f26',
|
|
151
169
|
},
|
|
152
170
|
tokyonight: {
|
|
153
171
|
primary: '#82aaff',
|
|
@@ -167,6 +185,13 @@ const PALETTES = {
|
|
|
167
185
|
borderSubtle: '#545c7e',
|
|
168
186
|
diffAdded: '#4fd6be',
|
|
169
187
|
diffRemoved: '#c53b53',
|
|
188
|
+
diffAddedBg: '#20303b',
|
|
189
|
+
diffRemovedBg: '#37222c',
|
|
190
|
+
diffLineNumber: '#8f909a',
|
|
191
|
+
diffHighlightAdded: '#b8db87',
|
|
192
|
+
diffHighlightRemoved: '#e26a75',
|
|
193
|
+
diffAddedLineNumberBg: '#1b2b34',
|
|
194
|
+
diffRemovedLineNumberBg: '#2d1f26',
|
|
170
195
|
},
|
|
171
196
|
catppuccin: {
|
|
172
197
|
primary: '#89b4fa',
|
|
@@ -186,6 +211,10 @@ const PALETTES = {
|
|
|
186
211
|
borderSubtle: '#585b70',
|
|
187
212
|
diffAdded: '#a6e3a1',
|
|
188
213
|
diffRemoved: '#f38ba8',
|
|
214
|
+
diffAddedBg: '#24312b',
|
|
215
|
+
diffRemovedBg: '#3c2a32',
|
|
216
|
+
diffAddedLineNumberBg: '#1e2a25',
|
|
217
|
+
diffRemovedLineNumberBg: '#32232a',
|
|
189
218
|
},
|
|
190
219
|
gruvbox: {
|
|
191
220
|
primary: '#83a598',
|
|
@@ -205,6 +234,11 @@ const PALETTES = {
|
|
|
205
234
|
borderSubtle: '#504945',
|
|
206
235
|
diffAdded: '#98971a',
|
|
207
236
|
diffRemoved: '#cc241d',
|
|
237
|
+
diffAddedBg: '#32302f',
|
|
238
|
+
diffRemovedBg: '#322929',
|
|
239
|
+
diffLineNumber: '#a8a29e',
|
|
240
|
+
diffAddedLineNumberBg: '#2a2827',
|
|
241
|
+
diffRemovedLineNumberBg: '#2a2222',
|
|
208
242
|
},
|
|
209
243
|
nord: {
|
|
210
244
|
primary: '#88c0d0',
|
|
@@ -224,6 +258,11 @@ const PALETTES = {
|
|
|
224
258
|
borderSubtle: '#434c5e',
|
|
225
259
|
diffAdded: '#a3be8c',
|
|
226
260
|
diffRemoved: '#bf616a',
|
|
261
|
+
diffAddedBg: '#3B4252',
|
|
262
|
+
diffRemovedBg: '#3B4252',
|
|
263
|
+
diffLineNumber: '#a9aeb6',
|
|
264
|
+
diffAddedLineNumberBg: '#3B4252',
|
|
265
|
+
diffRemovedLineNumberBg: '#3B4252',
|
|
227
266
|
},
|
|
228
267
|
};
|
|
229
268
|
|
package/src/tui/widgets.js
CHANGED
|
@@ -170,6 +170,55 @@ function toolIcon(name) {
|
|
|
170
170
|
* Render the session timeline: user blocks with a left border, assistant parts
|
|
171
171
|
* indented by three cells, tool rows, thinking rows and the closing `▣` line.
|
|
172
172
|
*/
|
|
173
|
+
/**
|
|
174
|
+
* A change drawn like the desktop client's `<diff>`: a header with the file and the
|
|
175
|
+
* `+n −m` counts, then the unified rows — two line numbers, the sign, the text — each
|
|
176
|
+
* painted with its own background. Context rows stay quiet so the change reads at once.
|
|
177
|
+
*/
|
|
178
|
+
function diffLines(t, diff, { width = 80, max = 24 } = {}) {
|
|
179
|
+
const D = t.diff;
|
|
180
|
+
const rows = Array.isArray(diff && diff.rows) ? diff.rows : [];
|
|
181
|
+
if (!rows.length) return [];
|
|
182
|
+
const numbers = rows.some((r) => r.oldNo !== null || r.newNo !== null || r.type === 'hunk');
|
|
183
|
+
const oldW = rows.reduce((m, r) => Math.max(m, String(r.oldNo ?? '').length), 2);
|
|
184
|
+
const newW = rows.reduce((m, r) => Math.max(m, String(r.newNo ?? '').length), 2);
|
|
185
|
+
const gutter = numbers ? oldW + 1 + newW + 1 : 0; // "12 13 "
|
|
186
|
+
const textW = Math.max(8, width - gutter - 1);
|
|
187
|
+
const paint = (bg, fg, text, padTo) => {
|
|
188
|
+
const pad = Math.max(0, padTo - R.vlen(text));
|
|
189
|
+
return `${bgSeq(bg)}${fgSeq(fg)}${text}${spaces(pad)}${RESET}`;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const out = [];
|
|
193
|
+
for (const row of rows.slice(0, max)) {
|
|
194
|
+
if (row.type === 'gap') {
|
|
195
|
+
out.push(paint(D.ctx.noBg, D.number, spaces(gutter) + '⋯', width));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (row.type === 'hunk') {
|
|
199
|
+
out.push(paint(D.ctx.noBg, D.number, spaces(gutter) + R.vclip(row.text, textW), width));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const kind = row.type === 'add' ? 'add' : row.type === 'del' ? 'del' : 'ctx';
|
|
203
|
+
const cell = kind === 'add' ? D.add : kind === 'del' ? D.del : D.ctx;
|
|
204
|
+
const sign = kind === 'add' ? '+' : kind === 'del' ? '-' : ' ';
|
|
205
|
+
const signColor = kind === 'add' ? D.signAdd : kind === 'del' ? D.signDel : D.ctx.fg;
|
|
206
|
+
const nums = numbers
|
|
207
|
+
? `${String(row.oldNo ?? '').padStart(oldW)} ${String(row.newNo ?? '').padStart(newW)} `
|
|
208
|
+
: '';
|
|
209
|
+
const body = R.vclip(row.text ?? '', textW);
|
|
210
|
+
const head = `${nums}${sign}`;
|
|
211
|
+
// the numbers sit on the row background, only the sign is highlighted
|
|
212
|
+
const line =
|
|
213
|
+
`${bgSeq(cell.bg)}${fgSeq(D.number)}${nums}${fgSeq(signColor)}${sign}${fgSeq(cell.fg)}${body}`;
|
|
214
|
+
out.push(`${line}${spaces(Math.max(0, width - R.vlen(head + body)))}${RESET}`);
|
|
215
|
+
}
|
|
216
|
+
if (rows.length > max) {
|
|
217
|
+
out.push(paint(D.ctx.noBg, D.number, spaces(gutter) + `… ${rows.length - max} more lines`, width));
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
173
222
|
function chatLines(t, items, opts = {}) {
|
|
174
223
|
const { width, frame = 0, agent = 'Build', model = '', details = true } = opts;
|
|
175
224
|
const out = [];
|
|
@@ -234,7 +283,28 @@ function chatLines(t, items, opts = {}) {
|
|
|
234
283
|
const label = item.label || 'step';
|
|
235
284
|
const meta = item.meta ? ` (${item.meta})` : '';
|
|
236
285
|
pushBlank();
|
|
237
|
-
if (item.
|
|
286
|
+
if (item.diff && details && item.diff.rows && item.diff.rows.length) {
|
|
287
|
+
const bg = bgSeq(t.c.backgroundPanel);
|
|
288
|
+
const bar = `${fgSeq(t.c.background)}┃${RESET}`;
|
|
289
|
+
const inner = width - 3;
|
|
290
|
+
out.push(bar + bgWrap(bg, '', width - 1));
|
|
291
|
+
const counts = `+${item.diff.additions} −${item.diff.deletions}`;
|
|
292
|
+
const name = item.diff.file && !String(label).includes(item.diff.file) ? ` ${item.diff.file}` : '';
|
|
293
|
+
const headText = R.vclip(`${R.strip(label)}${name}`, Math.max(8, inner - R.vlen(counts) - 6));
|
|
294
|
+
out.push(
|
|
295
|
+
bar +
|
|
296
|
+
bgWrap(
|
|
297
|
+
bg,
|
|
298
|
+
spaces(2) +
|
|
299
|
+
`${fgSeq(color)}${icon}${RESET} ${fgSeq(t.c.text)}${headText}${RESET}` +
|
|
300
|
+
` ${t.diffAdded(`+${item.diff.additions}`)} ${t.diffRemoved(`−${item.diff.deletions}`)}`,
|
|
301
|
+
width - 1,
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
for (const line of diffLines(t, item.diff, { width: inner, max: 20 })) {
|
|
305
|
+
out.push(bar + line + spaces(0));
|
|
306
|
+
}
|
|
307
|
+
} else if (item.detail && details) {
|
|
238
308
|
// block tool: left border, panel background, command then output
|
|
239
309
|
const bg = bgSeq(t.c.backgroundPanel);
|
|
240
310
|
const bar = `${fgSeq(t.c.background)}┃${RESET}`;
|
|
@@ -372,6 +442,7 @@ function sessionScreen(t, opts) {
|
|
|
372
442
|
module.exports = {
|
|
373
443
|
promptBlock,
|
|
374
444
|
chatLines,
|
|
445
|
+
diffLines,
|
|
375
446
|
footerLine,
|
|
376
447
|
tipLine,
|
|
377
448
|
notifyLine,
|