@lardexx/arenacode 2.11.6 → 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 +25 -0
- package/package.json +2 -2
- package/src/diff.js +203 -0
- package/src/tui/app.js +31 -0
- package/src/tui/theme.js +39 -0
- package/src/tui/widgets.js +72 -1
package/README.md
CHANGED
|
@@ -293,6 +293,31 @@ What happens when:
|
|
|
293
293
|
the send-once-per-session rule and the git exclusion; the integration test checks that a real task creates the
|
|
294
294
|
sheet and that the bridge receives it.
|
|
295
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
|
+
|
|
296
321
|
### Sessions
|
|
297
322
|
|
|
298
323
|
Every timeline in the interface is a **session**: its own transcript, its own task id, its own mode. `/sessions`
|
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/sessions.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/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
|
|
|
@@ -1165,12 +1166,42 @@ class App {
|
|
|
1165
1166
|
}
|
|
1166
1167
|
item.state = 'running';
|
|
1167
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
|
+
|
|
1168
1184
|
let result;
|
|
1169
1185
|
try {
|
|
1170
1186
|
result = runFsOp(op, payload, this.folder);
|
|
1171
1187
|
} catch (e) {
|
|
1172
1188
|
result = { ok: false, error: e.message };
|
|
1173
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
|
+
}
|
|
1174
1205
|
await this.bridge.localResult(taskId, { rid: payload.rid, output: JSON.stringify(result), code: result.ok ? 0 : 1 });
|
|
1175
1206
|
item.state = result.ok ? 'done' : 'failed';
|
|
1176
1207
|
item.meta = result.ok ? (result.size ? `${result.size} B` : op) : result.error;
|
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,
|