@vincentbanana/bananacli 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/ui-server.js +162 -2
- package/dist/ui/app.js +449 -270
- package/dist/ui/index.html +79 -46
- package/dist/ui/style.css +404 -0
- package/package.json +1 -1
package/dist/server/ui-server.js
CHANGED
|
@@ -84,6 +84,111 @@ let activeAssetsDir = null;
|
|
|
84
84
|
function assetRoot() {
|
|
85
85
|
return activeAssetsDir ?? resolveAssetsDir();
|
|
86
86
|
}
|
|
87
|
+
/** 行级 diff:小文件用 LCS,大文件退化成整段替换,避免把服务卡住。 */
|
|
88
|
+
function lineDiff(before, after) {
|
|
89
|
+
const left = before ? before.split(/\r?\n/) : [];
|
|
90
|
+
const right = after ? after.split(/\r?\n/) : [];
|
|
91
|
+
const n = left.length;
|
|
92
|
+
const m = right.length;
|
|
93
|
+
const ops = [];
|
|
94
|
+
let additions = 0;
|
|
95
|
+
let deletions = 0;
|
|
96
|
+
if (n * m > 1_200_000) {
|
|
97
|
+
for (const text of left) {
|
|
98
|
+
ops.push({ type: 'del', text });
|
|
99
|
+
deletions += 1;
|
|
100
|
+
}
|
|
101
|
+
for (const text of right) {
|
|
102
|
+
ops.push({ type: 'add', text });
|
|
103
|
+
additions += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const width = m + 1;
|
|
108
|
+
const table = new Uint32Array((n + 1) * width);
|
|
109
|
+
for (let i = n - 1; i >= 0; i -= 1) {
|
|
110
|
+
for (let j = m - 1; j >= 0; j -= 1) {
|
|
111
|
+
const diag = table[(i + 1) * width + j + 1] ?? 0;
|
|
112
|
+
const down = table[(i + 1) * width + j] ?? 0;
|
|
113
|
+
const rightward = table[i * width + j + 1] ?? 0;
|
|
114
|
+
table[i * width + j] = left[i] === right[j] ? diag + 1 : Math.max(down, rightward);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let i = 0;
|
|
118
|
+
let j = 0;
|
|
119
|
+
while (i < n && j < m) {
|
|
120
|
+
const a = left[i] ?? '';
|
|
121
|
+
const b = right[j] ?? '';
|
|
122
|
+
const down = table[(i + 1) * width + j] ?? 0;
|
|
123
|
+
const rightward = table[i * width + j + 1] ?? 0;
|
|
124
|
+
if (a === b) {
|
|
125
|
+
ops.push({ type: 'ctx', text: a });
|
|
126
|
+
i += 1;
|
|
127
|
+
j += 1;
|
|
128
|
+
}
|
|
129
|
+
else if (down >= rightward) {
|
|
130
|
+
ops.push({ type: 'del', text: a });
|
|
131
|
+
deletions += 1;
|
|
132
|
+
i += 1;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
ops.push({ type: 'add', text: b });
|
|
136
|
+
additions += 1;
|
|
137
|
+
j += 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
while (i < n) {
|
|
141
|
+
ops.push({ type: 'del', text: left[i++] ?? '' });
|
|
142
|
+
deletions += 1;
|
|
143
|
+
}
|
|
144
|
+
while (j < m) {
|
|
145
|
+
ops.push({ type: 'add', text: right[j++] ?? '' });
|
|
146
|
+
additions += 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// 只保留改动附近 3 行上下文,跳过的部分用省略行代替
|
|
150
|
+
const keep = new Set();
|
|
151
|
+
ops.forEach((op, index) => {
|
|
152
|
+
if (op.type === 'ctx')
|
|
153
|
+
return;
|
|
154
|
+
for (let k = Math.max(0, index - 3); k <= Math.min(ops.length - 1, index + 3); k += 1)
|
|
155
|
+
keep.add(k);
|
|
156
|
+
});
|
|
157
|
+
const lines = [];
|
|
158
|
+
let oldLine = 1;
|
|
159
|
+
let newLine = 1;
|
|
160
|
+
let lastKept = true;
|
|
161
|
+
let truncated = lines.length >= 800;
|
|
162
|
+
ops.forEach((op, index) => {
|
|
163
|
+
if (!keep.has(index)) {
|
|
164
|
+
if (lastKept && lines.length < 800) {
|
|
165
|
+
lines.push({ type: 'ctx', text: '…', oldLine: null, newLine: null });
|
|
166
|
+
}
|
|
167
|
+
lastKept = false;
|
|
168
|
+
if (op.type !== 'add')
|
|
169
|
+
oldLine += 1;
|
|
170
|
+
if (op.type !== 'del')
|
|
171
|
+
newLine += 1;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
lastKept = true;
|
|
175
|
+
if (lines.length >= 800) {
|
|
176
|
+
truncated = true;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
lines.push({
|
|
180
|
+
type: op.type,
|
|
181
|
+
text: op.text,
|
|
182
|
+
oldLine: op.type === 'add' ? null : oldLine,
|
|
183
|
+
newLine: op.type === 'del' ? null : newLine,
|
|
184
|
+
});
|
|
185
|
+
if (op.type !== 'add')
|
|
186
|
+
oldLine += 1;
|
|
187
|
+
if (op.type !== 'del')
|
|
188
|
+
newLine += 1;
|
|
189
|
+
});
|
|
190
|
+
return { lines, additions, deletions, truncated };
|
|
191
|
+
}
|
|
87
192
|
/** 启动只监听回环地址的本地 GUI 服务;token 保护所有接口。 */
|
|
88
193
|
export async function startUiServer(options) {
|
|
89
194
|
const { config, store } = options;
|
|
@@ -217,8 +322,12 @@ export async function startUiServer(options) {
|
|
|
217
322
|
}
|
|
218
323
|
let session = turn?.session ??
|
|
219
324
|
(activeId ? await store.load(activeId) : null) ??
|
|
220
|
-
(await store.lastActive().then(async (id) => (id ? await store.load(id) : null)))
|
|
221
|
-
|
|
325
|
+
(await store.lastActive().then(async (id) => (id ? await store.load(id) : null)));
|
|
326
|
+
if (!session) {
|
|
327
|
+
// 空会话也立刻落盘:否则每个请求都会重新 create,会话 id 一直变
|
|
328
|
+
session = store.create();
|
|
329
|
+
await store.save(session);
|
|
330
|
+
}
|
|
222
331
|
activeId = session.id;
|
|
223
332
|
if (path === '/api/info' && req.method === 'GET')
|
|
224
333
|
return json(res, 200, await info(session));
|
|
@@ -354,6 +463,57 @@ export async function startUiServer(options) {
|
|
|
354
463
|
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
355
464
|
}
|
|
356
465
|
}
|
|
466
|
+
if (path === '/api/changes' && req.method === 'GET') {
|
|
467
|
+
try {
|
|
468
|
+
const store = backups(session.id);
|
|
469
|
+
const manifest = await store.load();
|
|
470
|
+
const turns = [...manifest.turns].reverse().slice(0, 8);
|
|
471
|
+
const payload = [];
|
|
472
|
+
for (const turn of turns) {
|
|
473
|
+
const files = [];
|
|
474
|
+
for (const change of turn.changes.slice(0, 40)) {
|
|
475
|
+
let before = '';
|
|
476
|
+
let after = '';
|
|
477
|
+
if (change.backup) {
|
|
478
|
+
try {
|
|
479
|
+
before = await readFile(join(store.dir, turn.turnId, change.backup), 'utf8');
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
before = '';
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const info = await stat(change.path);
|
|
487
|
+
if (info.isFile() && info.size <= MAX_FILE_BYTES)
|
|
488
|
+
after = await readFile(change.path, 'utf8');
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
after = '';
|
|
492
|
+
}
|
|
493
|
+
const diff = lineDiff(before, after);
|
|
494
|
+
files.push({
|
|
495
|
+
path: change.relative,
|
|
496
|
+
absolute: change.path,
|
|
497
|
+
existed: change.existed,
|
|
498
|
+
missing: after === '' && change.existed,
|
|
499
|
+
additions: diff.additions,
|
|
500
|
+
deletions: diff.deletions,
|
|
501
|
+
truncated: diff.truncated,
|
|
502
|
+
lines: diff.lines,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
if (files.length)
|
|
506
|
+
payload.push({ turnId: turn.turnId, at: turn.at, files });
|
|
507
|
+
}
|
|
508
|
+
return json(res, 200, {
|
|
509
|
+
turns: payload,
|
|
510
|
+
totalFiles: payload.reduce((sum, turn) => sum + turn.files.length, 0),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
357
517
|
if (path === '/api/commands' && req.method === 'GET') {
|
|
358
518
|
return json(res, 200, {
|
|
359
519
|
commands: commands.map((command) => ({
|