@steerable/agent-cli 0.6.60 → 0.6.61
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/tui/children.d.ts +17 -0
- package/dist/tui/children.js +74 -0
- package/dist/tui/clipboard.d.ts +14 -0
- package/dist/tui/clipboard.js +58 -0
- package/dist/tui/commands.d.ts +9 -0
- package/dist/tui/commands.js +41 -0
- package/dist/tui/editor.d.ts +18 -0
- package/dist/tui/editor.js +201 -0
- package/dist/tui/export-chat.d.ts +4 -0
- package/dist/tui/export-chat.js +7 -0
- package/dist/tui/files.d.ts +23 -0
- package/dist/tui/files.js +63 -0
- package/dist/tui/keys.d.ts +26 -0
- package/dist/tui/keys.js +52 -10
- package/dist/tui/prompt-history.d.ts +21 -0
- package/dist/tui/prompt-history.js +57 -0
- package/dist/tui/run.d.ts +2 -0
- package/dist/tui/run.js +18 -0
- package/dist/tui/screen.d.ts +34 -1
- package/dist/tui/screen.js +122 -6
- package/dist/tui/scroll.d.ts +6 -0
- package/dist/tui/scroll.js +28 -0
- package/dist/tui/search.d.ts +8 -0
- package/dist/tui/search.js +38 -0
- package/dist/tui/session.d.ts +59 -1
- package/dist/tui/session.js +841 -34
- package/dist/tui/todos.d.ts +6 -0
- package/dist/tui/todos.js +47 -0
- package/dist/tui/transcript.d.ts +31 -0
- package/dist/tui/transcript.js +146 -0
- package/dist/tui/usage.d.ts +9 -0
- package/dist/tui/usage.js +13 -0
- package/dist/tui/view.d.ts +11 -0
- package/dist/tui/view.js +156 -15
- package/package.json +4 -4
package/dist/tui/session.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
1
6
|
import { getKeybindings } from '@earendil-works/pi-tui';
|
|
2
7
|
import { listBusyChatIds } from '@steerable/agent-client';
|
|
8
|
+
import { saveAttachmentFiles } from '@steerable/agent-shell/attachments';
|
|
9
|
+
import { applyChildEvent, childLines } from './children.js';
|
|
10
|
+
import { readSystemClipboard, saveClipboardImage, writeSystemClipboard } from './clipboard.js';
|
|
11
|
+
import { completeSlash, slashAt } from './commands.js';
|
|
12
|
+
import { clearDraft, createDraft, editDraft, insertText } from './editor.js';
|
|
13
|
+
import { attachmentMessage, completeFiles, isImagePath, mentionAt, } from './files.js';
|
|
14
|
+
import { markdownChat } from './export-chat.js';
|
|
3
15
|
import { ensureAgentKeybindings } from './keys.js';
|
|
16
|
+
import { createPromptHistory, leavePromptHistory, onFirstLine, onLastLine, recallPrompt, rememberPrompt, } from './prompt-history.js';
|
|
4
17
|
import { renderScreen } from './screen.js';
|
|
18
|
+
import { searchHits, searchLabel } from './search.js';
|
|
19
|
+
import { transcriptEdge } from './scroll.js';
|
|
20
|
+
import { todosFromAction } from './todos.js';
|
|
21
|
+
import { historyRows, toolOutput, toolStatus } from './transcript.js';
|
|
22
|
+
import { formatUsage } from './usage.js';
|
|
5
23
|
export class AgentTui {
|
|
6
24
|
client;
|
|
7
25
|
options;
|
|
@@ -16,7 +34,24 @@ export class AgentTui {
|
|
|
16
34
|
ask = null;
|
|
17
35
|
readOnly = false;
|
|
18
36
|
help = false;
|
|
19
|
-
|
|
37
|
+
buffer = createDraft();
|
|
38
|
+
prompts = createPromptHistory();
|
|
39
|
+
paste = null;
|
|
40
|
+
picks = null;
|
|
41
|
+
pickIndex = 0;
|
|
42
|
+
pickGeneration = 0;
|
|
43
|
+
attachments = [];
|
|
44
|
+
queue = [];
|
|
45
|
+
sending = false;
|
|
46
|
+
search = null;
|
|
47
|
+
scroll = null;
|
|
48
|
+
panel = null;
|
|
49
|
+
mode = 'agent';
|
|
50
|
+
permission = 'ask';
|
|
51
|
+
todos = [];
|
|
52
|
+
liveGeneration = 0;
|
|
53
|
+
children = [];
|
|
54
|
+
cwd;
|
|
20
55
|
status = '';
|
|
21
56
|
stopped = false;
|
|
22
57
|
turnAbort = null;
|
|
@@ -24,6 +59,7 @@ export class AgentTui {
|
|
|
24
59
|
constructor(client, options) {
|
|
25
60
|
this.client = client;
|
|
26
61
|
this.options = options;
|
|
62
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
27
63
|
ensureAgentKeybindings();
|
|
28
64
|
}
|
|
29
65
|
async open() {
|
|
@@ -51,15 +87,36 @@ export class AgentTui {
|
|
|
51
87
|
return this.screen();
|
|
52
88
|
}
|
|
53
89
|
handleInput(data) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
this.applyInput(data);
|
|
91
|
+
this.touch();
|
|
92
|
+
this.scroll = null;
|
|
93
|
+
}
|
|
94
|
+
applyInput(data) {
|
|
95
|
+
if (this.paste !== null || data.includes('\x1b[200~')) {
|
|
96
|
+
if (!this.composing())
|
|
97
|
+
this.paste = null;
|
|
98
|
+
else
|
|
99
|
+
this.consumePaste(data);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const chars = [...data];
|
|
103
|
+
if (chars.length > 1 && !data.includes('\x1b')) {
|
|
104
|
+
for (let index = 0; index < chars.length; index += 1) {
|
|
105
|
+
const char = chars[index] ?? '';
|
|
106
|
+
if (char === '\r' && chars[index + 1] === '\n') {
|
|
107
|
+
this.applyInput('\r');
|
|
108
|
+
index += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
this.applyInput(char);
|
|
112
|
+
}
|
|
57
113
|
return;
|
|
58
114
|
}
|
|
59
115
|
const keys = getKeybindings();
|
|
60
116
|
if (keys.matches(data, 'agent.interrupt')) {
|
|
61
117
|
if (this.turnAbort && !this.turnAbort.signal.aborted) {
|
|
62
118
|
this.turnAbort.abort();
|
|
119
|
+
this.queue = [];
|
|
63
120
|
this.status = '已中断';
|
|
64
121
|
return;
|
|
65
122
|
}
|
|
@@ -108,28 +165,164 @@ export class AgentTui {
|
|
|
108
165
|
this.showChats();
|
|
109
166
|
return;
|
|
110
167
|
}
|
|
168
|
+
if (keys.matches(data, 'agent.tool.toggle')) {
|
|
169
|
+
this.toggleFold();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const edge = this.composing() ? transcriptEdge(data) : null;
|
|
173
|
+
if (edge) {
|
|
174
|
+
this.scroll = edge;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (this.search && this.composing() && this.editSearch(data))
|
|
178
|
+
return;
|
|
179
|
+
if (this.composing() && keys.matches(data, 'tui.altScreen.search')) {
|
|
180
|
+
this.search = { query: '', index: 0 };
|
|
181
|
+
this.panel = null;
|
|
182
|
+
this.picks = null;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (this.panel && keys.matches(data, 'tui.select.cancel')) {
|
|
186
|
+
this.panel = null;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (this.composing() && keys.matches(data, 'agent.queue.pull')) {
|
|
190
|
+
this.pullQueue();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (this.composing() && keys.matches(data, 'agent.copy.reply')) {
|
|
194
|
+
void this.copyReply();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (this.composing() && keys.matches(data, 'agent.editor')) {
|
|
198
|
+
void this.editDraftExternal();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
111
201
|
if (this.ask && keys.matches(data, 'tui.input.submit')) {
|
|
112
202
|
const requestId = this.ask.requestId;
|
|
113
|
-
const answer = this.
|
|
114
|
-
this.
|
|
203
|
+
const answer = this.buffer.text;
|
|
204
|
+
clearDraft(this.buffer);
|
|
115
205
|
this.ask = null;
|
|
116
206
|
void this.client.answerAsk(requestId, { text: answer });
|
|
117
207
|
return;
|
|
118
208
|
}
|
|
119
|
-
if (keys.matches(data, 'tui.
|
|
120
|
-
this.
|
|
209
|
+
if (this.picks && keys.matches(data, 'tui.select.cancel')) {
|
|
210
|
+
this.picks = null;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (this.picks && keys.matches(data, 'tui.input.tab')) {
|
|
214
|
+
this.acceptPick();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (this.picks && keys.matches(data, 'tui.select.confirm')) {
|
|
218
|
+
const pick = this.picks[this.pickIndex];
|
|
219
|
+
if (!pick || this.buffer.text.trim() !== pick.insert.trim()) {
|
|
220
|
+
this.acceptPick();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (this.picks && keys.matches(data, 'tui.select.up')) {
|
|
225
|
+
this.pickIndex = Math.max(0, this.pickIndex - 1);
|
|
226
|
+
this.markPicks();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (this.picks && keys.matches(data, 'tui.select.down')) {
|
|
230
|
+
this.pickIndex = Math.min(this.picks.length - 1, this.pickIndex + 1);
|
|
231
|
+
this.markPicks();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (keys.matches(data, 'tui.editor.deleteCharBackward')
|
|
235
|
+
&& this.buffer.text.length === 0
|
|
236
|
+
&& this.buffer.cursor === 0
|
|
237
|
+
&& this.attachments.length > 0) {
|
|
238
|
+
this.attachments.pop();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (this.composing() && keys.matches(data, 'agent.clipboard.paste')) {
|
|
242
|
+
void this.pasteClipboard();
|
|
121
243
|
return;
|
|
122
244
|
}
|
|
123
|
-
if (
|
|
245
|
+
if (this.browseHistory(data))
|
|
246
|
+
return;
|
|
247
|
+
const edited = editDraft(this.buffer, data);
|
|
248
|
+
if (edited === 'submit') {
|
|
124
249
|
void this.submit();
|
|
125
250
|
return;
|
|
126
251
|
}
|
|
127
|
-
if (
|
|
128
|
-
this.
|
|
252
|
+
if (edited === 'edited')
|
|
253
|
+
leavePromptHistory(this.prompts);
|
|
254
|
+
this.refreshPicks();
|
|
255
|
+
}
|
|
256
|
+
async pasteClipboard() {
|
|
257
|
+
const read = this.options.readClipboard ?? readSystemClipboard;
|
|
258
|
+
let payload;
|
|
259
|
+
try {
|
|
260
|
+
payload = await read();
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
void error;
|
|
264
|
+
this.status = '剪贴板不可用';
|
|
265
|
+
this.touch();
|
|
129
266
|
return;
|
|
130
267
|
}
|
|
131
|
-
|
|
132
|
-
|
|
268
|
+
const bytes = payload.image;
|
|
269
|
+
if (bytes && bytes.length > 0) {
|
|
270
|
+
const saved = await saveClipboardImage(bytes);
|
|
271
|
+
if (!saved) {
|
|
272
|
+
this.status = '剪贴板里的图片格式不支持';
|
|
273
|
+
this.touch();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
this.attachments.push(saved);
|
|
277
|
+
this.status = `已附加 ${saved.name}`;
|
|
278
|
+
this.touch();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (payload.text) {
|
|
282
|
+
insertText(this.buffer, payload.text);
|
|
283
|
+
this.refreshPicks();
|
|
284
|
+
this.touch();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
browseHistory(data) {
|
|
288
|
+
if (!this.composing())
|
|
289
|
+
return false;
|
|
290
|
+
const keys = getKeybindings();
|
|
291
|
+
const previous = keys.matches(data, 'tui.editor.historyPrevious');
|
|
292
|
+
const next = keys.matches(data, 'tui.editor.historyNext');
|
|
293
|
+
const up = keys.matches(data, 'tui.editor.cursorUp') && onFirstLine(this.buffer.text, this.buffer.cursor);
|
|
294
|
+
const down = keys.matches(data, 'tui.editor.cursorDown')
|
|
295
|
+
&& this.prompts.index >= 0
|
|
296
|
+
&& onLastLine(this.buffer.text, this.buffer.cursor);
|
|
297
|
+
if (!previous && !next && !up && !down)
|
|
298
|
+
return false;
|
|
299
|
+
const direction = previous || up ? -1 : 1;
|
|
300
|
+
const moved = recallPrompt(this.prompts, this.buffer, direction);
|
|
301
|
+
if (!moved && !previous && !next)
|
|
302
|
+
return false;
|
|
303
|
+
this.picks = null;
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
composing() {
|
|
307
|
+
return !this.approval && !this.help && !this.chats;
|
|
308
|
+
}
|
|
309
|
+
consumePaste(data) {
|
|
310
|
+
let chunk = data;
|
|
311
|
+
if (this.paste === null) {
|
|
312
|
+
this.paste = '';
|
|
313
|
+
chunk = chunk.replaceAll('\x1b[200~', '');
|
|
314
|
+
}
|
|
315
|
+
this.paste += chunk;
|
|
316
|
+
const end = this.paste.indexOf('\x1b[201~');
|
|
317
|
+
if (end < 0)
|
|
318
|
+
return;
|
|
319
|
+
const text = this.paste.slice(0, end).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
320
|
+
const rest = this.paste.slice(end + '\x1b[201~'.length);
|
|
321
|
+
this.paste = null;
|
|
322
|
+
if (text.length > 0)
|
|
323
|
+
insertText(this.buffer, text);
|
|
324
|
+
if (rest.length > 0)
|
|
325
|
+
this.applyInput(rest);
|
|
133
326
|
}
|
|
134
327
|
screen() {
|
|
135
328
|
return {
|
|
@@ -142,18 +335,145 @@ export class AgentTui {
|
|
|
142
335
|
chats: this.chats,
|
|
143
336
|
readOnly: this.readOnly,
|
|
144
337
|
help: this.help,
|
|
145
|
-
draft: this.
|
|
338
|
+
draft: this.buffer.text,
|
|
339
|
+
cursor: this.buffer.cursor,
|
|
146
340
|
status: this.status,
|
|
341
|
+
picks: this.pickRows(),
|
|
342
|
+
attachments: this.attachments.map((file) => file.name),
|
|
343
|
+
children: this.children,
|
|
344
|
+
queued: this.queue.map((item) => item.text || item.files.map((file) => file.name).join(' ')),
|
|
345
|
+
search: this.searchBar().label,
|
|
346
|
+
searchFocus: this.searchBar().focus,
|
|
347
|
+
scroll: this.scroll,
|
|
348
|
+
panel: this.panel,
|
|
349
|
+
mode: this.mode,
|
|
350
|
+
permission: this.permission,
|
|
351
|
+
todos: this.todos,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
searchBar() {
|
|
355
|
+
if (!this.search)
|
|
356
|
+
return { label: null, focus: null };
|
|
357
|
+
const hits = searchHits(this.lines, this.search.query);
|
|
358
|
+
const index = hits.length === 0 ? 0 : this.search.index % hits.length;
|
|
359
|
+
return {
|
|
360
|
+
label: searchLabel(this.search.query, hits, index),
|
|
361
|
+
focus: hits[index]?.line ?? null,
|
|
147
362
|
};
|
|
148
363
|
}
|
|
364
|
+
editSearch(data) {
|
|
365
|
+
if (!this.search)
|
|
366
|
+
return false;
|
|
367
|
+
const keys = getKeybindings();
|
|
368
|
+
if (keys.matches(data, 'tui.altScreen.search') || keys.matches(data, 'tui.altScreen.searchClose')) {
|
|
369
|
+
this.search = null;
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
const hits = searchHits(this.lines, this.search.query);
|
|
373
|
+
if (keys.matches(data, 'tui.altScreen.searchNext')) {
|
|
374
|
+
if (hits.length > 0)
|
|
375
|
+
this.search.index = (this.search.index + 1) % hits.length;
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
if (keys.matches(data, 'tui.altScreen.searchPrevious')) {
|
|
379
|
+
if (hits.length > 0)
|
|
380
|
+
this.search.index = (this.search.index - 1 + hits.length) % hits.length;
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
if (keys.matches(data, 'tui.editor.deleteCharBackward')) {
|
|
384
|
+
this.search.query = [...this.search.query].slice(0, -1).join('');
|
|
385
|
+
this.search.index = 0;
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
if ([...data].length === 1 && data >= ' ' && !data.includes('\x1b')) {
|
|
389
|
+
this.search.query += data;
|
|
390
|
+
this.search.index = 0;
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
pullQueue() {
|
|
396
|
+
const item = this.queue.pop();
|
|
397
|
+
if (!item)
|
|
398
|
+
return;
|
|
399
|
+
if (this.buffer.text.length > 0) {
|
|
400
|
+
this.queue.push(item);
|
|
401
|
+
this.status = '输入框里还有字';
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.buffer.text = item.text;
|
|
405
|
+
this.buffer.cursor = [...item.text].length;
|
|
406
|
+
this.attachments.push(...item.files);
|
|
407
|
+
this.status = '已取回';
|
|
408
|
+
}
|
|
409
|
+
pickRows() {
|
|
410
|
+
if (!this.picks || this.picks.length === 0)
|
|
411
|
+
return null;
|
|
412
|
+
return this.picks.map((pick, index) => ({ label: pick.label, selected: index === this.pickIndex }));
|
|
413
|
+
}
|
|
414
|
+
markPicks() {
|
|
415
|
+
if (!this.picks)
|
|
416
|
+
return;
|
|
417
|
+
if (this.pickIndex >= this.picks.length)
|
|
418
|
+
this.pickIndex = Math.max(0, this.picks.length - 1);
|
|
419
|
+
}
|
|
420
|
+
refreshPicks() {
|
|
421
|
+
if (!this.composing()) {
|
|
422
|
+
this.picks = null;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const slash = slashAt(this.buffer.text, this.buffer.cursor);
|
|
426
|
+
if (slash) {
|
|
427
|
+
this.pickGeneration += 1;
|
|
428
|
+
const commands = completeSlash(slash.query);
|
|
429
|
+
this.picks = commands.length > 0 ? commands : null;
|
|
430
|
+
this.markPicks();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const token = mentionAt(this.buffer.text, this.buffer.cursor);
|
|
434
|
+
if (!token) {
|
|
435
|
+
this.picks = null;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const generation = ++this.pickGeneration;
|
|
439
|
+
const query = token.query;
|
|
440
|
+
void completeFiles(this.cwd, query).then((files) => {
|
|
441
|
+
if (generation !== this.pickGeneration)
|
|
442
|
+
return;
|
|
443
|
+
if (!mentionAt(this.buffer.text, this.buffer.cursor))
|
|
444
|
+
return;
|
|
445
|
+
this.picks = files.length > 0 ? files : [{ label: '无匹配', insert: '', directory: false }];
|
|
446
|
+
this.markPicks();
|
|
447
|
+
this.touch();
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
acceptPick() {
|
|
451
|
+
const pick = this.picks?.[this.pickIndex];
|
|
452
|
+
const token = slashAt(this.buffer.text, this.buffer.cursor) ?? mentionAt(this.buffer.text, this.buffer.cursor);
|
|
453
|
+
if (!pick?.insert || !token) {
|
|
454
|
+
this.picks = null;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const next = `${this.buffer.text.slice(0, token.start)}${pick.insert}${this.buffer.text.slice(this.buffer.cursor)}`;
|
|
458
|
+
this.buffer.text = next;
|
|
459
|
+
this.buffer.cursor = token.start + pick.insert.length;
|
|
460
|
+
if (pick.directory)
|
|
461
|
+
this.refreshPicks();
|
|
462
|
+
else
|
|
463
|
+
this.picks = null;
|
|
464
|
+
}
|
|
149
465
|
showChats() {
|
|
150
466
|
const index = this.rows.findIndex((chat) => chat.id === this.chatId);
|
|
151
467
|
this.selected = index >= 0 ? index : 0;
|
|
152
468
|
this.chats = this.rows.map((chat, row) => ({ ...chat, selected: row === this.selected }));
|
|
153
469
|
void this.loadChats().then((rows) => {
|
|
154
470
|
this.rows = rows;
|
|
155
|
-
if (this.chats)
|
|
156
|
-
|
|
471
|
+
if (!this.chats)
|
|
472
|
+
return;
|
|
473
|
+
if (this.selected >= rows.length)
|
|
474
|
+
this.selected = Math.max(0, rows.length - 1);
|
|
475
|
+
this.chats = rows.map((chat, row) => ({ ...chat, selected: row === this.selected }));
|
|
476
|
+
this.touch();
|
|
157
477
|
});
|
|
158
478
|
}
|
|
159
479
|
async loadChats() {
|
|
@@ -184,17 +504,31 @@ export class AgentTui {
|
|
|
184
504
|
const records = messages.status === 200
|
|
185
505
|
? (messages.data.messages ?? [])
|
|
186
506
|
: [];
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
507
|
+
this.todos = [];
|
|
508
|
+
this.lines = historyRows(records).map((row) => {
|
|
509
|
+
if (row.kind !== 'tool')
|
|
510
|
+
return { kind: row.kind, text: row.text };
|
|
511
|
+
const next = todosFromAction(row.action.tool, row.action.arguments, row.action.result);
|
|
512
|
+
if (next)
|
|
513
|
+
this.todos = next;
|
|
514
|
+
return toolLine(row.action);
|
|
515
|
+
});
|
|
191
516
|
this.help = false;
|
|
517
|
+
this.search = null;
|
|
518
|
+
this.panel = null;
|
|
519
|
+
this.children = [];
|
|
520
|
+
this.picks = null;
|
|
521
|
+
this.attachments = [];
|
|
192
522
|
this.touch();
|
|
523
|
+
this.armLive(id);
|
|
193
524
|
}
|
|
194
525
|
async submit() {
|
|
195
|
-
const text = this.
|
|
196
|
-
this.
|
|
197
|
-
|
|
526
|
+
const text = this.buffer.text.trim();
|
|
527
|
+
clearDraft(this.buffer);
|
|
528
|
+
this.picks = null;
|
|
529
|
+
if (text)
|
|
530
|
+
rememberPrompt(this.prompts, text);
|
|
531
|
+
if (!text && this.attachments.length === 0)
|
|
198
532
|
return;
|
|
199
533
|
if (text.startsWith('/')) {
|
|
200
534
|
await this.slash(text);
|
|
@@ -204,10 +538,50 @@ export class AgentTui {
|
|
|
204
538
|
this.status = '只读 · 另一个进程正在运行';
|
|
205
539
|
return;
|
|
206
540
|
}
|
|
207
|
-
this.
|
|
208
|
-
|
|
541
|
+
if (this.permission === 'read') {
|
|
542
|
+
this.status = '权限 只读';
|
|
543
|
+
this.touch();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const files = this.attachments.splice(0);
|
|
547
|
+
if (this.sending) {
|
|
548
|
+
this.queue.push({ text, files });
|
|
549
|
+
this.touch();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
this.sending = true;
|
|
553
|
+
try {
|
|
554
|
+
await this.sendPrepared(text, files);
|
|
555
|
+
while (!this.stopped && this.queue.length > 0) {
|
|
556
|
+
const next = this.queue.shift();
|
|
557
|
+
if (!next)
|
|
558
|
+
break;
|
|
559
|
+
await this.sendPrepared(next.text, next.files);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
this.sending = false;
|
|
564
|
+
this.touch();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async sendPrepared(text, files) {
|
|
568
|
+
const prepared = await this.prepareOwned(text, files);
|
|
569
|
+
this.lines.push({ kind: 'user', text: prepared.message });
|
|
570
|
+
this.touch();
|
|
571
|
+
this.pending = this.runTurn(prepared.message, prepared.images);
|
|
209
572
|
await this.pending;
|
|
210
573
|
}
|
|
574
|
+
async prepareOwned(text, files) {
|
|
575
|
+
const added = this.attachments.splice(0);
|
|
576
|
+
this.attachments = files;
|
|
577
|
+
try {
|
|
578
|
+
return await this.prepareMessage(text);
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
const failed = this.attachments;
|
|
582
|
+
this.attachments = [...added, ...failed];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
211
585
|
async slash(text) {
|
|
212
586
|
const [command, ...rest] = text.slice(1).split(/\s+/);
|
|
213
587
|
if (command === 'help') {
|
|
@@ -215,6 +589,10 @@ export class AgentTui {
|
|
|
215
589
|
this.touch();
|
|
216
590
|
return;
|
|
217
591
|
}
|
|
592
|
+
if (command === 'attach') {
|
|
593
|
+
await this.attach(rest.join(' '));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
218
596
|
if (command === 'clear') {
|
|
219
597
|
this.lines = [];
|
|
220
598
|
this.status = '';
|
|
@@ -249,31 +627,179 @@ export class AgentTui {
|
|
|
249
627
|
this.touch();
|
|
250
628
|
return;
|
|
251
629
|
}
|
|
630
|
+
if (command === 'status') {
|
|
631
|
+
const summary = await this.client.request('GET', '/api/v2/usage/summary', undefined);
|
|
632
|
+
this.status = summary.status === 200 ? formatUsage(summary.data) : '用量不可用';
|
|
633
|
+
this.touch();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (command === 'skills') {
|
|
637
|
+
await this.showList('GET', '/api/v2/chat-agents/skills', 'skills', (row) => `技能 ${textField(row, 'name')}${textField(row, 'description') ? ` ${textField(row, 'description')}` : ''}`, '没有技能');
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (command === 'mcp') {
|
|
641
|
+
await this.showList('GET', '/api/v2/mcp/servers', 'servers', (row) => `MCP ${textField(row, 'name') || textField(row, 'id')} ${numberField(row, 'toolCount')} 个工具`, '没有 MCP');
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (command === 'export') {
|
|
645
|
+
await this.exportChat();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (command === 'compact') {
|
|
649
|
+
await this.compactChat();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (command === 'plan') {
|
|
653
|
+
this.mode = this.mode === 'plan' ? 'agent' : 'plan';
|
|
654
|
+
this.status = this.mode === 'plan' ? '计划模式' : '对话模式';
|
|
655
|
+
this.touch();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (command === 'tasks') {
|
|
659
|
+
await this.showTasks();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (command === 'fork') {
|
|
663
|
+
await this.forkChat();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (command === 'rewind') {
|
|
667
|
+
await this.rewindChat();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (command === 'permissions') {
|
|
671
|
+
this.permission = this.permission === 'ask' ? 'auto' : this.permission === 'auto' ? 'read' : 'ask';
|
|
672
|
+
this.status = this.permission === 'ask' ? '权限 询问' : this.permission === 'auto' ? '权限 本会话自动' : '权限 只读';
|
|
673
|
+
this.touch();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (command === 'copy') {
|
|
677
|
+
await this.copyReply();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (command === 'editor') {
|
|
681
|
+
await this.editDraftExternal();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
252
684
|
this.status = `unknown command /${command}`;
|
|
253
685
|
this.touch();
|
|
254
686
|
}
|
|
255
|
-
async
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
this.
|
|
259
|
-
|
|
687
|
+
async attach(target) {
|
|
688
|
+
if (this.readOnly) {
|
|
689
|
+
this.status = '只读 · 另一个进程正在运行';
|
|
690
|
+
this.touch();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (!target) {
|
|
694
|
+
this.status = this.attachments.length > 0
|
|
695
|
+
? this.attachments.map((file) => file.name).join(' ')
|
|
696
|
+
: '用法 /attach <路径>';
|
|
260
697
|
this.touch();
|
|
698
|
+
return;
|
|
261
699
|
}
|
|
700
|
+
const full = path.resolve(this.cwd, target);
|
|
701
|
+
let fileStat;
|
|
702
|
+
try {
|
|
703
|
+
fileStat = await fs.stat(full);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
this.status = '找不到文件';
|
|
707
|
+
this.touch();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (!fileStat.isFile()) {
|
|
711
|
+
this.status = '不是文件';
|
|
712
|
+
this.touch();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const name = path.basename(full);
|
|
716
|
+
this.attachments.push({ name, path: full });
|
|
717
|
+
this.status = `已附加 ${name}`;
|
|
718
|
+
this.touch();
|
|
719
|
+
}
|
|
720
|
+
async prepareMessage(text) {
|
|
721
|
+
if (this.attachments.length === 0)
|
|
722
|
+
return { message: text, images: [] };
|
|
723
|
+
await this.ensureChat();
|
|
724
|
+
const chatId = this.chatId ?? '';
|
|
725
|
+
const save = this.options.saveAttachments ?? defaultSaveAttachments;
|
|
726
|
+
const saved = await save(chatId, this.attachments);
|
|
727
|
+
const stored = [];
|
|
728
|
+
const kept = [];
|
|
729
|
+
saved.forEach((file, index) => {
|
|
730
|
+
if (file.path && !file.error)
|
|
731
|
+
stored.push(file);
|
|
732
|
+
else {
|
|
733
|
+
const original = this.attachments[index];
|
|
734
|
+
if (original)
|
|
735
|
+
kept.push(original);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
this.attachments = kept;
|
|
739
|
+
if (kept.length > 0) {
|
|
740
|
+
this.status = saved.filter((file) => file.error).map((file) => file.error).join(' ');
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
message: attachmentMessage(text, stored),
|
|
744
|
+
images: stored.filter((file) => isImagePath(file.path)).map((file) => ({ path: file.path, name: file.name })),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
async ensureChat() {
|
|
748
|
+
if (this.chatId)
|
|
749
|
+
return;
|
|
750
|
+
const created = await this.client.request('POST', '/api/v2/chats/new', {});
|
|
751
|
+
this.chatId = created.data.chatId ?? 'draft';
|
|
752
|
+
this.title = this.chatId;
|
|
753
|
+
}
|
|
754
|
+
async runTurn(text, images = []) {
|
|
755
|
+
await this.ensureChat();
|
|
756
|
+
if (!this.chatId)
|
|
757
|
+
return;
|
|
262
758
|
const controller = new AbortController();
|
|
263
759
|
this.turnAbort = controller;
|
|
760
|
+
this.children = [];
|
|
264
761
|
this.status = '';
|
|
265
762
|
try {
|
|
266
|
-
|
|
763
|
+
const payload = { message: text };
|
|
764
|
+
if (images.length > 0)
|
|
765
|
+
payload.images = images;
|
|
766
|
+
if (this.mode === 'plan')
|
|
767
|
+
payload.mode = 'plan';
|
|
768
|
+
for await (const event of this.client.stream(`/api/v2/chats/${encodeURIComponent(this.chatId)}/send`, payload, controller.signal)) {
|
|
267
769
|
this.observe(event, controller.signal);
|
|
268
770
|
this.touch();
|
|
269
771
|
}
|
|
270
772
|
}
|
|
271
773
|
finally {
|
|
774
|
+
for (const line of this.lines)
|
|
775
|
+
line.streaming = false;
|
|
776
|
+
if (this.children.length > 0) {
|
|
777
|
+
for (const line of childLines(this.children))
|
|
778
|
+
this.lines.push({ kind: 'tree', text: line });
|
|
779
|
+
this.children = [];
|
|
780
|
+
this.touch();
|
|
781
|
+
}
|
|
782
|
+
if (controller.signal.aborted)
|
|
783
|
+
this.queue = [];
|
|
272
784
|
if (this.turnAbort === controller)
|
|
273
785
|
this.turnAbort = null;
|
|
274
786
|
}
|
|
275
787
|
}
|
|
276
788
|
observe(event, signal) {
|
|
789
|
+
if (String(event.type) === 'executed_actions' && Array.isArray(event.actions)) {
|
|
790
|
+
this.applyActions(event.actions);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (String(event.type) === 'orchestration_child') {
|
|
794
|
+
this.children = applyChildEvent(this.children, {
|
|
795
|
+
kind: event.kind,
|
|
796
|
+
childId: event.childId,
|
|
797
|
+
task: event.task,
|
|
798
|
+
profile: event.profile,
|
|
799
|
+
depth: event.depth,
|
|
800
|
+
});
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
277
803
|
if (event.type === 'tool_call') {
|
|
278
804
|
const payload = event.payload ?? {};
|
|
279
805
|
const name = typeof payload.name === 'string' ? payload.name : 'tool';
|
|
@@ -287,12 +813,26 @@ export class AgentTui {
|
|
|
287
813
|
tool.status = '✓';
|
|
288
814
|
return;
|
|
289
815
|
}
|
|
816
|
+
if (String(event.type) === 'reasoning' && typeof event.content === 'string') {
|
|
817
|
+
const last = this.lines[this.lines.length - 1];
|
|
818
|
+
if (last?.kind === 'reasoning') {
|
|
819
|
+
last.text = `${last.text ?? ''}${event.content}`;
|
|
820
|
+
last.streaming = true;
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
this.lines.push({ kind: 'reasoning', text: event.content, streaming: true });
|
|
824
|
+
}
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
290
827
|
if (event.type === 'content' && typeof event.content === 'string') {
|
|
291
828
|
const last = this.lines[this.lines.length - 1];
|
|
292
|
-
if (last?.kind === 'assistant')
|
|
829
|
+
if (last?.kind === 'assistant') {
|
|
293
830
|
last.text = `${last.text ?? ''}${event.content}`;
|
|
294
|
-
|
|
295
|
-
|
|
831
|
+
last.streaming = true;
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
this.lines.push({ kind: 'assistant', text: event.content, streaming: true });
|
|
835
|
+
}
|
|
296
836
|
return;
|
|
297
837
|
}
|
|
298
838
|
if (event.type === 'error' && !signal.aborted) {
|
|
@@ -307,6 +847,11 @@ export class AgentTui {
|
|
|
307
847
|
const payload = event.payload;
|
|
308
848
|
if (!payload.requestId)
|
|
309
849
|
continue;
|
|
850
|
+
if (this.permission === 'auto') {
|
|
851
|
+
void this.client.decideApproval(payload.requestId, 'allow_for_session');
|
|
852
|
+
this.touch();
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
310
855
|
this.approval = {
|
|
311
856
|
requestId: payload.requestId,
|
|
312
857
|
toolName: payload.toolName || 'tool',
|
|
@@ -321,6 +866,196 @@ export class AgentTui {
|
|
|
321
866
|
this.touch();
|
|
322
867
|
}
|
|
323
868
|
}
|
|
869
|
+
applyActions(actions) {
|
|
870
|
+
for (const action of actions) {
|
|
871
|
+
if (!action || typeof action !== 'object')
|
|
872
|
+
continue;
|
|
873
|
+
const nextTodos = todosFromAction(action.tool, action.arguments, action.result);
|
|
874
|
+
if (nextTodos)
|
|
875
|
+
this.todos = nextTodos;
|
|
876
|
+
const next = toolLine(action);
|
|
877
|
+
const index = next.id
|
|
878
|
+
? this.lines.findIndex((line) => line.kind === 'tool' && line.id === next.id)
|
|
879
|
+
: -1;
|
|
880
|
+
if (index >= 0) {
|
|
881
|
+
next.open = this.lines[index]?.open;
|
|
882
|
+
this.lines[index] = next;
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
this.lines.push(next);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
toggleFold() {
|
|
890
|
+
const open = this.lines.find((line) => (line.kind === 'tool' || line.kind === 'reasoning') && line.open);
|
|
891
|
+
if (open) {
|
|
892
|
+
open.open = false;
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
for (let index = this.lines.length - 1; index >= 0; index -= 1) {
|
|
896
|
+
const line = this.lines[index];
|
|
897
|
+
if (line?.kind === 'tool' && line.status !== '…') {
|
|
898
|
+
line.open = true;
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (line?.kind === 'reasoning' && (line.text ?? '').length > 0) {
|
|
902
|
+
line.open = true;
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
armLive(chatId) {
|
|
908
|
+
const generation = ++this.liveGeneration;
|
|
909
|
+
void this.pollLive(chatId, generation);
|
|
910
|
+
}
|
|
911
|
+
async pollLive(chatId, generation) {
|
|
912
|
+
if (this.stopped || this.chatId !== chatId || generation !== this.liveGeneration)
|
|
913
|
+
return;
|
|
914
|
+
const live = await this.client.request('GET', `/api/v2/chats/${encodeURIComponent(chatId)}/live-stream`, undefined);
|
|
915
|
+
if (this.stopped || this.chatId !== chatId || generation !== this.liveGeneration)
|
|
916
|
+
return;
|
|
917
|
+
if (live.status !== 200 || !live.data || typeof live.data !== 'object')
|
|
918
|
+
return;
|
|
919
|
+
const data = live.data;
|
|
920
|
+
if (data.active !== true) {
|
|
921
|
+
if (this.status === '运行中')
|
|
922
|
+
this.status = '';
|
|
923
|
+
this.touch();
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
this.status = '运行中';
|
|
927
|
+
if (typeof data.content === 'string' && data.content.length > 0) {
|
|
928
|
+
const streaming = [...this.lines].reverse().find((line) => line.kind === 'assistant' && line.streaming);
|
|
929
|
+
if (streaming)
|
|
930
|
+
streaming.text = data.content;
|
|
931
|
+
else if (!this.lines.some((line) => line.kind === 'assistant' && line.text === data.content)) {
|
|
932
|
+
this.lines.push({ kind: 'assistant', text: data.content, streaming: true });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (Array.isArray(data.executedActions))
|
|
936
|
+
this.applyActions(data.executedActions);
|
|
937
|
+
this.touch();
|
|
938
|
+
const wait = this.options.liveIntervalMs ?? 1000;
|
|
939
|
+
await new Promise((resolve) => {
|
|
940
|
+
setTimeout(resolve, wait);
|
|
941
|
+
});
|
|
942
|
+
await this.pollLive(chatId, generation);
|
|
943
|
+
}
|
|
944
|
+
async showList(method, requestPath, key, format, empty) {
|
|
945
|
+
const listed = await this.client.request(method, requestPath, undefined);
|
|
946
|
+
const rows = listed.status === 200 && listed.data && typeof listed.data === 'object'
|
|
947
|
+
? listed.data[key]
|
|
948
|
+
: [];
|
|
949
|
+
const lines = Array.isArray(rows)
|
|
950
|
+
? rows.filter((row) => !!row && typeof row === 'object').map(format)
|
|
951
|
+
: [];
|
|
952
|
+
this.panel = lines.length > 0 ? lines : [empty];
|
|
953
|
+
this.touch();
|
|
954
|
+
}
|
|
955
|
+
async showTasks() {
|
|
956
|
+
if (!this.chatId) {
|
|
957
|
+
this.status = '还没有会话';
|
|
958
|
+
this.touch();
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const listed = await this.client.request('GET', `/api/v2/chats/${encodeURIComponent(this.chatId)}/tasks`, undefined);
|
|
962
|
+
const rows = listed.status === 200 && listed.data && typeof listed.data === 'object'
|
|
963
|
+
? listed.data.tasks ?? []
|
|
964
|
+
: [];
|
|
965
|
+
this.panel = rows.length > 0
|
|
966
|
+
? rows.map((task) => `后台 ${task.task ?? ''} ${taskStatus(task.status)}`)
|
|
967
|
+
: ['没有后台任务'];
|
|
968
|
+
this.touch();
|
|
969
|
+
}
|
|
970
|
+
async exportChat() {
|
|
971
|
+
const messages = this.lines
|
|
972
|
+
.filter((line) => line.kind === 'user' || line.kind === 'assistant')
|
|
973
|
+
.map((line) => ({ role: line.kind, content: line.text ?? '' }));
|
|
974
|
+
const file = path.join(this.options.exportDir ?? os.tmpdir(), `${this.chatId ?? 'chat'}.md`);
|
|
975
|
+
await fs.writeFile(file, markdownChat(this.title, messages));
|
|
976
|
+
this.status = `已导出 ${file}`;
|
|
977
|
+
this.touch();
|
|
978
|
+
}
|
|
979
|
+
async compactChat() {
|
|
980
|
+
if (!this.chatId)
|
|
981
|
+
return;
|
|
982
|
+
const result = await this.client.request('POST', `/api/v2/chats/${encodeURIComponent(this.chatId)}/compact`, {});
|
|
983
|
+
const compacted = result.status === 200 ? numberField(result.data, 'compacted') : 0;
|
|
984
|
+
if (result.status !== 200) {
|
|
985
|
+
this.status = '压缩失败';
|
|
986
|
+
this.touch();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (compacted === 0) {
|
|
990
|
+
this.status = '没有可压缩的记录';
|
|
991
|
+
this.touch();
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
await this.openChat(this.chatId);
|
|
995
|
+
this.status = `已压缩 ${compacted} 条`;
|
|
996
|
+
this.touch();
|
|
997
|
+
}
|
|
998
|
+
async forkChat() {
|
|
999
|
+
if (!this.chatId)
|
|
1000
|
+
return;
|
|
1001
|
+
const created = await this.client.request('POST', `/api/v2/chats/${encodeURIComponent(this.chatId)}/fork`, {});
|
|
1002
|
+
const chatId = created.status === 200 ? textField(created.data, 'chatId') : '';
|
|
1003
|
+
if (!chatId) {
|
|
1004
|
+
this.status = '分叉失败';
|
|
1005
|
+
this.touch();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
await this.openChat(chatId);
|
|
1009
|
+
this.status = '已分叉';
|
|
1010
|
+
this.touch();
|
|
1011
|
+
}
|
|
1012
|
+
async rewindChat() {
|
|
1013
|
+
if (!this.chatId)
|
|
1014
|
+
return;
|
|
1015
|
+
const result = await this.client.request('POST', `/api/v2/chats/${encodeURIComponent(this.chatId)}/rewind`, {});
|
|
1016
|
+
const removed = result.status === 200 ? numberField(result.data, 'removed') : 0;
|
|
1017
|
+
if (result.status !== 200 || removed === 0) {
|
|
1018
|
+
this.status = '没有可回退的回合';
|
|
1019
|
+
this.touch();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
await this.openChat(this.chatId);
|
|
1023
|
+
this.status = '已回退';
|
|
1024
|
+
this.touch();
|
|
1025
|
+
}
|
|
1026
|
+
async copyReply() {
|
|
1027
|
+
const text = [...this.lines].reverse().find((line) => line.kind === 'assistant' && (line.text ?? '').length > 0)?.text ?? '';
|
|
1028
|
+
if (!text) {
|
|
1029
|
+
this.status = '没有可复制的回答';
|
|
1030
|
+
this.touch();
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const write = this.options.writeClipboard ?? writeSystemClipboard;
|
|
1034
|
+
try {
|
|
1035
|
+
await write(text);
|
|
1036
|
+
this.status = '已复制';
|
|
1037
|
+
}
|
|
1038
|
+
catch (error) {
|
|
1039
|
+
void error;
|
|
1040
|
+
this.status = '剪贴板不可用';
|
|
1041
|
+
}
|
|
1042
|
+
this.touch();
|
|
1043
|
+
}
|
|
1044
|
+
async editDraftExternal() {
|
|
1045
|
+
const edit = this.options.editInEditor ?? editWithEditor;
|
|
1046
|
+
try {
|
|
1047
|
+
const next = await edit(this.buffer.text);
|
|
1048
|
+
this.buffer.text = next.replace(/\r\n/g, '\n');
|
|
1049
|
+
this.buffer.cursor = [...this.buffer.text].length;
|
|
1050
|
+
leavePromptHistory(this.prompts);
|
|
1051
|
+
this.refreshPicks();
|
|
1052
|
+
}
|
|
1053
|
+
catch (error) {
|
|
1054
|
+
void error;
|
|
1055
|
+
this.status = '编辑器不可用';
|
|
1056
|
+
}
|
|
1057
|
+
this.touch();
|
|
1058
|
+
}
|
|
324
1059
|
async decide(kind) {
|
|
325
1060
|
const request = this.approval;
|
|
326
1061
|
if (!request)
|
|
@@ -330,6 +1065,8 @@ export class AgentTui {
|
|
|
330
1065
|
await this.client.decideApproval(request.requestId, kind);
|
|
331
1066
|
}
|
|
332
1067
|
touch() {
|
|
1068
|
+
if (this.stopped)
|
|
1069
|
+
return;
|
|
333
1070
|
this.options.onChange?.();
|
|
334
1071
|
}
|
|
335
1072
|
exit() {
|
|
@@ -356,6 +1093,76 @@ function approvalKind(data) {
|
|
|
356
1093
|
return 'abort';
|
|
357
1094
|
return null;
|
|
358
1095
|
}
|
|
1096
|
+
async function defaultSaveAttachments(chatId, files) {
|
|
1097
|
+
const saved = await saveAttachmentFiles(chatId, files);
|
|
1098
|
+
return saved.files.map((file) => ({
|
|
1099
|
+
name: file.name,
|
|
1100
|
+
path: file.path,
|
|
1101
|
+
...(file.error ? { error: file.error } : {}),
|
|
1102
|
+
}));
|
|
1103
|
+
}
|
|
1104
|
+
function toolLine(action) {
|
|
1105
|
+
const name = typeof action.tool === 'string' && action.tool ? action.tool : 'tool';
|
|
1106
|
+
const title = typeof action.view?.title === 'string' ? action.view.title : '';
|
|
1107
|
+
return {
|
|
1108
|
+
kind: 'tool',
|
|
1109
|
+
id: typeof action.id === 'string' ? action.id : undefined,
|
|
1110
|
+
name,
|
|
1111
|
+
args: title && title !== name ? title : toolArgs(action.arguments),
|
|
1112
|
+
status: toolStatus(action),
|
|
1113
|
+
output: toolOutput(action),
|
|
1114
|
+
...(diffOf(action) ? { diff: diffOf(action) } : {}),
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
function diffOf(action) {
|
|
1118
|
+
const result = action.result;
|
|
1119
|
+
if (!result || typeof result !== 'object')
|
|
1120
|
+
return undefined;
|
|
1121
|
+
const diff = result.diff;
|
|
1122
|
+
return typeof diff === 'string' && diff.length > 0 ? diff : undefined;
|
|
1123
|
+
}
|
|
1124
|
+
function textField(value, key) {
|
|
1125
|
+
if (!value || typeof value !== 'object')
|
|
1126
|
+
return '';
|
|
1127
|
+
const field = value[key];
|
|
1128
|
+
return typeof field === 'string' ? field : '';
|
|
1129
|
+
}
|
|
1130
|
+
function numberField(value, key) {
|
|
1131
|
+
if (!value || typeof value !== 'object')
|
|
1132
|
+
return 0;
|
|
1133
|
+
const field = value[key];
|
|
1134
|
+
return typeof field === 'number' && Number.isFinite(field) ? field : 0;
|
|
1135
|
+
}
|
|
1136
|
+
function taskStatus(status) {
|
|
1137
|
+
if (status === 'running')
|
|
1138
|
+
return '进行中';
|
|
1139
|
+
if (status === 'completed')
|
|
1140
|
+
return '完成';
|
|
1141
|
+
if (status === 'failed')
|
|
1142
|
+
return '失败';
|
|
1143
|
+
if (status === 'blocked')
|
|
1144
|
+
return '等待';
|
|
1145
|
+
return status ?? '';
|
|
1146
|
+
}
|
|
1147
|
+
async function editWithEditor(text) {
|
|
1148
|
+
const editor = process.env.VISUAL || process.env.EDITOR;
|
|
1149
|
+
if (!editor)
|
|
1150
|
+
throw new Error('editor unavailable');
|
|
1151
|
+
const file = path.join(os.tmpdir(), `steerable-draft-${randomUUID().slice(0, 8)}.md`);
|
|
1152
|
+
await fs.writeFile(file, text);
|
|
1153
|
+
const code = await new Promise((resolve, reject) => {
|
|
1154
|
+
const child = spawn(editor, [file], { stdio: 'inherit' });
|
|
1155
|
+
child.on('error', reject);
|
|
1156
|
+
child.on('exit', (status) => resolve(status ?? 1));
|
|
1157
|
+
});
|
|
1158
|
+
const next = await fs.readFile(file, 'utf8');
|
|
1159
|
+
await fs.unlink(file).catch((error) => {
|
|
1160
|
+
void error;
|
|
1161
|
+
});
|
|
1162
|
+
if (code !== 0)
|
|
1163
|
+
throw new Error(`editor exited ${code}`);
|
|
1164
|
+
return next;
|
|
1165
|
+
}
|
|
359
1166
|
function toolArgs(value) {
|
|
360
1167
|
if (typeof value === 'string')
|
|
361
1168
|
return value;
|