@steerable/agent-cli 0.6.74 → 0.7.2
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/approve.d.ts +17 -5
- package/dist/approve.js +35 -16
- package/dist/bin.js +6 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +129 -26
- package/dist/copy.d.ts +15 -0
- package/dist/copy.js +16 -0
- package/dist/help.d.ts +11 -0
- package/dist/help.js +35 -5
- package/dist/node-version.d.ts +17 -0
- package/dist/node-version.js +40 -0
- package/dist/project-binding.d.ts +35 -0
- package/dist/project-binding.js +151 -0
- package/dist/rules.d.ts +8 -0
- package/dist/rules.js +12 -0
- package/dist/run.d.ts +1 -1
- package/dist/run.js +272 -14
- package/dist/tui/commands.js +1 -0
- package/dist/tui/run.d.ts +4 -0
- package/dist/tui/run.js +17 -2
- package/dist/tui/screen.d.ts +8 -0
- package/dist/tui/screen.js +20 -1
- package/dist/tui/session.d.ts +21 -0
- package/dist/tui/session.js +293 -19
- package/dist/tui/transcript.d.ts +4 -0
- package/dist/tui/transcript.js +6 -1
- package/dist/tui/view.js +5 -3
- package/package.json +9 -5
package/dist/tui/screen.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Box, CURSOR_MARKER, Text } from '@earendil-works/pi-tui';
|
|
2
|
+
import { cliText } from '../copy.js';
|
|
2
3
|
import { childLines } from './children.js';
|
|
3
4
|
import { splitAtCursor } from './editor.js';
|
|
4
5
|
import { keyLabel } from './keys.js';
|
|
@@ -64,6 +65,7 @@ export function helpLines() {
|
|
|
64
65
|
'/tasks 后台任务',
|
|
65
66
|
'/fork 分叉',
|
|
66
67
|
'/rewind 回退',
|
|
68
|
+
'/rules 项目规则',
|
|
67
69
|
'/permissions 会话权限',
|
|
68
70
|
'/copy 复制回答',
|
|
69
71
|
'/editor 外部编辑器',
|
|
@@ -145,11 +147,27 @@ function screenLines(screen) {
|
|
|
145
147
|
if (screen.search)
|
|
146
148
|
lines.push(screen.search);
|
|
147
149
|
lines.push(draftLine(screen.draft, screen.cursor));
|
|
150
|
+
const project = projectStatusLine(screen);
|
|
151
|
+
if (project)
|
|
152
|
+
lines.push(project);
|
|
148
153
|
if (screen.status)
|
|
149
154
|
lines.push(screen.status);
|
|
150
155
|
lines.push(footerText());
|
|
151
156
|
return lines;
|
|
152
157
|
}
|
|
158
|
+
/** 状态行最左侧是无人值守提示,后面用竖线和项目、规则接在同一行。 */
|
|
159
|
+
export function projectStatusLine(screen) {
|
|
160
|
+
let approval = '';
|
|
161
|
+
if (screen.permission === 'auto' && screen.approvalPolicy === 'allow-all') {
|
|
162
|
+
approval = cliText('tui.approvalClosed');
|
|
163
|
+
}
|
|
164
|
+
else if (screen.permission !== 'auto' && screen.approvalPolicy === 'allow-read') {
|
|
165
|
+
approval = cliText('tui.approvalRead');
|
|
166
|
+
}
|
|
167
|
+
return [approval, screen.projectLabel, screen.ruleLabel]
|
|
168
|
+
.filter((line) => line && line.length > 0)
|
|
169
|
+
.join(' │ ');
|
|
170
|
+
}
|
|
153
171
|
export function bannerLines(screen) {
|
|
154
172
|
const lines = [];
|
|
155
173
|
if (!screen.help && !screen.chats) {
|
|
@@ -163,8 +181,9 @@ export function bannerLines(screen) {
|
|
|
163
181
|
}
|
|
164
182
|
if (screen.mode === 'plan')
|
|
165
183
|
lines.push('模式 计划');
|
|
166
|
-
if (screen.permission === 'auto')
|
|
184
|
+
if (screen.permission === 'auto' && screen.approvalPolicy !== 'allow-all') {
|
|
167
185
|
lines.push('权限 本会话自动');
|
|
186
|
+
}
|
|
168
187
|
if (screen.permission === 'read')
|
|
169
188
|
lines.push('权限 只读');
|
|
170
189
|
for (const row of screen.panel ?? [])
|
package/dist/tui/session.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Component } from '@earendil-works/pi-tui';
|
|
2
2
|
import { type AgentClient } from '@steerable/agent-client';
|
|
3
|
+
import { type ApprovalSpec } from '../approve.js';
|
|
3
4
|
import { type ClipboardRead } from './clipboard.js';
|
|
4
5
|
import { type SavedFile } from './files.js';
|
|
5
6
|
import { type TuiScreen } from './screen.js';
|
|
@@ -16,6 +17,9 @@ export interface AgentTuiOptions {
|
|
|
16
17
|
writeClipboard?: (text: string) => Promise<void>;
|
|
17
18
|
editInEditor?: (text: string) => Promise<string>;
|
|
18
19
|
exportDir?: string;
|
|
20
|
+
projectId?: string;
|
|
21
|
+
projectLabel?: string;
|
|
22
|
+
approvalPolicy?: ApprovalSpec;
|
|
19
23
|
liveIntervalMs?: number;
|
|
20
24
|
onExit: () => void;
|
|
21
25
|
onChange?: () => void;
|
|
@@ -49,6 +53,13 @@ export declare class AgentTui implements Component {
|
|
|
49
53
|
private panel;
|
|
50
54
|
private mode;
|
|
51
55
|
private permission;
|
|
56
|
+
private approvalPolicy;
|
|
57
|
+
private readonly pendingAuto;
|
|
58
|
+
private markAutoTool;
|
|
59
|
+
private ensureEgressLine;
|
|
60
|
+
private latestAskLine;
|
|
61
|
+
private unboundAskLine;
|
|
62
|
+
private takePendingAuto;
|
|
52
63
|
private todos;
|
|
53
64
|
private goal;
|
|
54
65
|
private loops;
|
|
@@ -58,6 +69,7 @@ export declare class AgentTui implements Component {
|
|
|
58
69
|
private children;
|
|
59
70
|
private readonly cwd;
|
|
60
71
|
private status;
|
|
72
|
+
private ruleLabel;
|
|
61
73
|
private stopped;
|
|
62
74
|
private turnAbort;
|
|
63
75
|
private pending;
|
|
@@ -72,6 +84,11 @@ export declare class AgentTui implements Component {
|
|
|
72
84
|
private pasteClipboard;
|
|
73
85
|
private browseHistory;
|
|
74
86
|
private composing;
|
|
87
|
+
/**
|
|
88
|
+
* Insert text from a bracketed paste the terminal already split out.
|
|
89
|
+
* OpenTUI delivers that as a paste event, not as the raw `\x1b[200~` sequence.
|
|
90
|
+
*/
|
|
91
|
+
acceptDecodedPaste(text: string): void;
|
|
75
92
|
private consumePaste;
|
|
76
93
|
private screen;
|
|
77
94
|
private searchBar;
|
|
@@ -94,7 +111,11 @@ export declare class AgentTui implements Component {
|
|
|
94
111
|
private manageLoop;
|
|
95
112
|
private attach;
|
|
96
113
|
private prepareMessage;
|
|
114
|
+
private newChatBody;
|
|
97
115
|
private ensureChat;
|
|
116
|
+
private refreshRules;
|
|
117
|
+
private showRules;
|
|
118
|
+
private ruleContext;
|
|
98
119
|
private runTurn;
|
|
99
120
|
private observe;
|
|
100
121
|
private watch;
|
package/dist/tui/session.js
CHANGED
|
@@ -6,6 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import { getKeybindings } from '@earendil-works/pi-tui';
|
|
7
7
|
import { listBusyChatIds } from '@steerable/agent-client';
|
|
8
8
|
import { saveAttachmentFiles } from '@steerable/agent-shell/attachments';
|
|
9
|
+
import { autoAnswer, decisionFor } from '../approve.js';
|
|
10
|
+
import { cliText } from '../copy.js';
|
|
9
11
|
import { applyChildEvent, childLines } from './children.js';
|
|
10
12
|
import { readSystemClipboard, saveClipboardImage, writeSystemClipboard } from './clipboard.js';
|
|
11
13
|
import { completeSlash, slashAt } from './commands.js';
|
|
@@ -16,9 +18,10 @@ import { ensureAgentKeybindings } from './keys.js';
|
|
|
16
18
|
import { createPromptHistory, leavePromptHistory, onFirstLine, onLastLine, recallPrompt, rememberPrompt, } from './prompt-history.js';
|
|
17
19
|
import { renderScreen } from './screen.js';
|
|
18
20
|
import { searchHits, searchLabel } from './search.js';
|
|
21
|
+
import { ruleStatusLine } from '../rules.js';
|
|
19
22
|
import { transcriptEdge } from './scroll.js';
|
|
20
23
|
import { todosFromAction } from './todos.js';
|
|
21
|
-
import { historyRows, toolOutput, toolStatus } from './transcript.js';
|
|
24
|
+
import { formatDuration, historyRows, toolOutput, toolStatus } from './transcript.js';
|
|
22
25
|
import { formatUsage } from './usage.js';
|
|
23
26
|
export class AgentTui {
|
|
24
27
|
client;
|
|
@@ -49,6 +52,60 @@ export class AgentTui {
|
|
|
49
52
|
panel = null;
|
|
50
53
|
mode = 'agent';
|
|
51
54
|
permission = 'ask';
|
|
55
|
+
approvalPolicy = { policy: 'ask', answerAsk: false };
|
|
56
|
+
pendingAuto = new Map();
|
|
57
|
+
markAutoTool(name) {
|
|
58
|
+
const line = [...this.lines].reverse().find((item) => item.kind === 'tool' && item.name === name);
|
|
59
|
+
if (line && !line.auto && !sessionAllowStatus(line.status)) {
|
|
60
|
+
line.auto = true;
|
|
61
|
+
line.status = appendAuto(line.status);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.pendingAuto.set(name, (this.pendingAuto.get(name) ?? 0) + 1);
|
|
65
|
+
}
|
|
66
|
+
ensureEgressLine(args) {
|
|
67
|
+
const target = egressTarget(args);
|
|
68
|
+
const existing = [...this.lines].reverse().find((line) => line.kind === 'tool' && line.name === 'network_egress');
|
|
69
|
+
if (existing && !existing.auto) {
|
|
70
|
+
if (!existing.args && target)
|
|
71
|
+
existing.args = target;
|
|
72
|
+
if (!existing.status || existing.status === '…')
|
|
73
|
+
existing.status = '✓';
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.lines.push({
|
|
77
|
+
kind: 'tool',
|
|
78
|
+
name: 'network_egress',
|
|
79
|
+
args: target,
|
|
80
|
+
status: '✓',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
latestAskLine() {
|
|
84
|
+
for (let index = this.lines.length - 1; index >= 0; index -= 1) {
|
|
85
|
+
const line = this.lines[index];
|
|
86
|
+
if (line?.kind === 'tool' && line.name === 'ask_user')
|
|
87
|
+
return line;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
unboundAskLine() {
|
|
92
|
+
for (let index = this.lines.length - 1; index >= 0; index -= 1) {
|
|
93
|
+
const line = this.lines[index];
|
|
94
|
+
if (line?.kind === 'tool' && line.name === 'ask_user' && !line.id)
|
|
95
|
+
return index;
|
|
96
|
+
}
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
takePendingAuto(name) {
|
|
100
|
+
const pending = this.pendingAuto.get(name) ?? 0;
|
|
101
|
+
if (pending <= 0)
|
|
102
|
+
return false;
|
|
103
|
+
if (pending === 1)
|
|
104
|
+
this.pendingAuto.delete(name);
|
|
105
|
+
else
|
|
106
|
+
this.pendingAuto.set(name, pending - 1);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
52
109
|
todos = [];
|
|
53
110
|
goal = null;
|
|
54
111
|
loops = [];
|
|
@@ -58,6 +115,7 @@ export class AgentTui {
|
|
|
58
115
|
children = [];
|
|
59
116
|
cwd;
|
|
60
117
|
status = '';
|
|
118
|
+
ruleLabel = '';
|
|
61
119
|
stopped = false;
|
|
62
120
|
turnAbort = null;
|
|
63
121
|
pending = Promise.resolve();
|
|
@@ -65,6 +123,11 @@ export class AgentTui {
|
|
|
65
123
|
this.client = client;
|
|
66
124
|
this.options = options;
|
|
67
125
|
this.cwd = options.cwd ?? process.cwd();
|
|
126
|
+
if (options.approvalPolicy) {
|
|
127
|
+
this.approvalPolicy = options.approvalPolicy;
|
|
128
|
+
if (options.approvalPolicy.policy === 'allow-all')
|
|
129
|
+
this.permission = 'auto';
|
|
130
|
+
}
|
|
68
131
|
ensureAgentKeybindings();
|
|
69
132
|
}
|
|
70
133
|
async open() {
|
|
@@ -75,9 +138,16 @@ export class AgentTui {
|
|
|
75
138
|
this.modelName = model;
|
|
76
139
|
}
|
|
77
140
|
this.rows = await this.loadChats();
|
|
78
|
-
const
|
|
141
|
+
const projectId = this.options.projectId;
|
|
142
|
+
const current = projectId
|
|
143
|
+
? this.rows.find((chat) => chat.projectId === projectId)
|
|
144
|
+
: this.rows[0];
|
|
79
145
|
if (current)
|
|
80
146
|
await this.openChat(current.id);
|
|
147
|
+
else if (projectId) {
|
|
148
|
+
await this.ensureChat();
|
|
149
|
+
await this.refreshRules();
|
|
150
|
+
}
|
|
81
151
|
void this.watch();
|
|
82
152
|
this.touch();
|
|
83
153
|
}
|
|
@@ -253,7 +323,7 @@ export class AgentTui {
|
|
|
253
323
|
return;
|
|
254
324
|
const edited = editDraft(this.buffer, data);
|
|
255
325
|
if (edited === 'submit') {
|
|
256
|
-
|
|
326
|
+
this.pending = this.submit();
|
|
257
327
|
return;
|
|
258
328
|
}
|
|
259
329
|
if (edited === 'edited')
|
|
@@ -313,6 +383,20 @@ export class AgentTui {
|
|
|
313
383
|
composing() {
|
|
314
384
|
return !this.approval && !this.help && !this.chats;
|
|
315
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Insert text from a bracketed paste the terminal already split out.
|
|
388
|
+
* OpenTUI delivers that as a paste event, not as the raw `\x1b[200~` sequence.
|
|
389
|
+
*/
|
|
390
|
+
acceptDecodedPaste(text) {
|
|
391
|
+
if (!this.composing())
|
|
392
|
+
return;
|
|
393
|
+
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
394
|
+
if (normalized.length === 0)
|
|
395
|
+
return;
|
|
396
|
+
insertText(this.buffer, normalized);
|
|
397
|
+
this.refreshPicks();
|
|
398
|
+
this.touch();
|
|
399
|
+
}
|
|
316
400
|
consumePaste(data) {
|
|
317
401
|
let chunk = data;
|
|
318
402
|
if (this.paste === null) {
|
|
@@ -323,11 +407,10 @@ export class AgentTui {
|
|
|
323
407
|
const end = this.paste.indexOf('\x1b[201~');
|
|
324
408
|
if (end < 0)
|
|
325
409
|
return;
|
|
326
|
-
const text = this.paste.slice(0, end)
|
|
410
|
+
const text = this.paste.slice(0, end);
|
|
327
411
|
const rest = this.paste.slice(end + '\x1b[201~'.length);
|
|
328
412
|
this.paste = null;
|
|
329
|
-
|
|
330
|
-
insertText(this.buffer, text);
|
|
413
|
+
this.acceptDecodedPaste(text);
|
|
331
414
|
if (rest.length > 0)
|
|
332
415
|
this.applyInput(rest);
|
|
333
416
|
}
|
|
@@ -345,6 +428,8 @@ export class AgentTui {
|
|
|
345
428
|
draft: this.buffer.text,
|
|
346
429
|
cursor: this.buffer.cursor,
|
|
347
430
|
status: this.status,
|
|
431
|
+
...(this.options.projectLabel ? { projectLabel: this.options.projectLabel } : {}),
|
|
432
|
+
...(this.ruleLabel ? { ruleLabel: this.ruleLabel } : {}),
|
|
348
433
|
picks: this.pickRows(),
|
|
349
434
|
attachments: this.attachments.map((file) => file.name),
|
|
350
435
|
children: this.children,
|
|
@@ -355,6 +440,7 @@ export class AgentTui {
|
|
|
355
440
|
panel: this.panel,
|
|
356
441
|
mode: this.mode,
|
|
357
442
|
permission: this.permission,
|
|
443
|
+
approvalPolicy: this.approvalPolicy.policy,
|
|
358
444
|
todos: this.todos,
|
|
359
445
|
goal: this.goal,
|
|
360
446
|
loops: this.loops,
|
|
@@ -496,6 +582,7 @@ export class AgentTui {
|
|
|
496
582
|
title: chat.title || chat.id,
|
|
497
583
|
busy: busy.has(chat.id),
|
|
498
584
|
selected: false,
|
|
585
|
+
projectId: chat.projectId ?? null,
|
|
499
586
|
}));
|
|
500
587
|
}
|
|
501
588
|
markSelection() {
|
|
@@ -530,6 +617,7 @@ export class AgentTui {
|
|
|
530
617
|
this.attachments = [];
|
|
531
618
|
await this.refreshGoal(id);
|
|
532
619
|
await this.refreshLoops(id);
|
|
620
|
+
await this.refreshRules();
|
|
533
621
|
this.touch();
|
|
534
622
|
this.armLive(id);
|
|
535
623
|
}
|
|
@@ -622,7 +710,7 @@ export class AgentTui {
|
|
|
622
710
|
return;
|
|
623
711
|
}
|
|
624
712
|
if (command === 'new') {
|
|
625
|
-
const created = await this.client.request('POST', '/api/v2/chats/new',
|
|
713
|
+
const created = await this.client.request('POST', '/api/v2/chats/new', this.newChatBody());
|
|
626
714
|
const chatId = created.data.chatId;
|
|
627
715
|
if (!chatId) {
|
|
628
716
|
this.status = 'failed to create chat';
|
|
@@ -697,7 +785,18 @@ export class AgentTui {
|
|
|
697
785
|
await this.rewindChat();
|
|
698
786
|
return;
|
|
699
787
|
}
|
|
788
|
+
if (command === 'rules') {
|
|
789
|
+
await this.showRules();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
700
792
|
if (command === 'permissions') {
|
|
793
|
+
if (this.approvalPolicy.policy === 'allow-all' || this.approvalPolicy.policy === 'allow-read') {
|
|
794
|
+
this.approvalPolicy = { policy: 'ask', answerAsk: false };
|
|
795
|
+
this.permission = 'ask';
|
|
796
|
+
this.status = '权限 询问';
|
|
797
|
+
this.touch();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
701
800
|
this.permission = this.permission === 'ask' ? 'auto' : this.permission === 'auto' ? 'read' : 'ask';
|
|
702
801
|
this.status = this.permission === 'ask' ? '权限 询问' : this.permission === 'auto' ? '权限 本会话自动' : '权限 只读';
|
|
703
802
|
this.touch();
|
|
@@ -822,13 +921,46 @@ export class AgentTui {
|
|
|
822
921
|
images: stored.filter((file) => isImagePath(file.path)).map((file) => ({ path: file.path, name: file.name })),
|
|
823
922
|
};
|
|
824
923
|
}
|
|
924
|
+
newChatBody() {
|
|
925
|
+
return this.options.projectId ? { projectId: this.options.projectId } : {};
|
|
926
|
+
}
|
|
825
927
|
async ensureChat() {
|
|
826
928
|
if (this.chatId)
|
|
827
929
|
return;
|
|
828
|
-
const created = await this.client.request('POST', '/api/v2/chats/new',
|
|
930
|
+
const created = await this.client.request('POST', '/api/v2/chats/new', this.newChatBody());
|
|
829
931
|
this.chatId = created.data.chatId ?? 'draft';
|
|
830
932
|
this.title = this.chatId;
|
|
831
933
|
}
|
|
934
|
+
async refreshRules() {
|
|
935
|
+
const context = await this.ruleContext();
|
|
936
|
+
if (!context)
|
|
937
|
+
return;
|
|
938
|
+
this.ruleLabel = ruleStatusLine(context, 'tui') ?? '';
|
|
939
|
+
}
|
|
940
|
+
async showRules() {
|
|
941
|
+
await this.ensureChat();
|
|
942
|
+
const context = await this.ruleContext();
|
|
943
|
+
if (!context?.project) {
|
|
944
|
+
this.status = '没有绑定项目';
|
|
945
|
+
this.touch();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const files = context.files ?? [];
|
|
949
|
+
const summary = ruleStatusLine(context, 'tui') ?? '没有规则文件';
|
|
950
|
+
this.lines.push({ kind: 'tree', text: summary });
|
|
951
|
+
for (const file of files)
|
|
952
|
+
this.lines.push({ kind: 'tree', text: file });
|
|
953
|
+
this.ruleLabel = ruleStatusLine(context, 'tui') ?? '';
|
|
954
|
+
this.touch();
|
|
955
|
+
}
|
|
956
|
+
async ruleContext() {
|
|
957
|
+
if (!this.chatId)
|
|
958
|
+
return null;
|
|
959
|
+
const response = await this.client.request('GET', `/api/v2/chats/${encodeURIComponent(this.chatId)}/project-context`, undefined);
|
|
960
|
+
if (response.status !== 200)
|
|
961
|
+
return null;
|
|
962
|
+
return response.data;
|
|
963
|
+
}
|
|
832
964
|
async runTurn(text, images = []) {
|
|
833
965
|
await this.ensureChat();
|
|
834
966
|
if (!this.chatId)
|
|
@@ -866,6 +998,8 @@ export class AgentTui {
|
|
|
866
998
|
this.queue = [];
|
|
867
999
|
if (this.turnAbort === controller)
|
|
868
1000
|
this.turnAbort = null;
|
|
1001
|
+
await this.refreshRules();
|
|
1002
|
+
this.touch();
|
|
869
1003
|
}
|
|
870
1004
|
}
|
|
871
1005
|
observe(event, signal) {
|
|
@@ -887,13 +1021,38 @@ export class AgentTui {
|
|
|
887
1021
|
const payload = event.payload ?? {};
|
|
888
1022
|
const name = typeof payload.name === 'string' ? payload.name : 'tool';
|
|
889
1023
|
const args = toolArgs(payload.arguments ?? payload.input);
|
|
890
|
-
this.
|
|
1024
|
+
const auto = this.takePendingAuto(name);
|
|
1025
|
+
if (name === 'network_egress') {
|
|
1026
|
+
const existing = [...this.lines].reverse().find((line) => line.kind === 'tool' && line.name === name);
|
|
1027
|
+
if (existing) {
|
|
1028
|
+
if (!existing.args && args)
|
|
1029
|
+
existing.args = args;
|
|
1030
|
+
if (auto) {
|
|
1031
|
+
existing.auto = true;
|
|
1032
|
+
existing.status = appendAuto(existing.status);
|
|
1033
|
+
}
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
this.lines.push({
|
|
1038
|
+
kind: 'tool',
|
|
1039
|
+
name,
|
|
1040
|
+
args,
|
|
1041
|
+
status: auto ? cliText('tui.toolAuto') : '…',
|
|
1042
|
+
...(auto ? { auto: true } : {}),
|
|
1043
|
+
});
|
|
891
1044
|
return;
|
|
892
1045
|
}
|
|
893
1046
|
if (event.type === 'tool_result') {
|
|
894
|
-
const tool = [...this.lines].reverse().find((line) => line.kind === 'tool' && line.status
|
|
895
|
-
if (tool)
|
|
896
|
-
|
|
1047
|
+
const tool = [...this.lines].reverse().find((line) => line.kind === 'tool' && openToolStatus(line.status));
|
|
1048
|
+
if (!tool || sessionAllowStatus(tool.status))
|
|
1049
|
+
return;
|
|
1050
|
+
const payload = event.payload ?? {};
|
|
1051
|
+
const failed = payload.success === false || payload.error != null;
|
|
1052
|
+
const duration = typeof payload.durationMs === 'number' ? formatDuration(payload.durationMs) : '';
|
|
1053
|
+
const mark = failed ? '✗' : '✓';
|
|
1054
|
+
const base = duration ? `${mark} ${duration}` : mark;
|
|
1055
|
+
tool.status = tool.auto ? appendAuto(base) : base;
|
|
897
1056
|
return;
|
|
898
1057
|
}
|
|
899
1058
|
if (String(event.type) === 'reasoning' && typeof event.content === 'string') {
|
|
@@ -930,8 +1089,17 @@ export class AgentTui {
|
|
|
930
1089
|
const payload = event.payload;
|
|
931
1090
|
if (!payload.requestId)
|
|
932
1091
|
continue;
|
|
933
|
-
|
|
934
|
-
|
|
1092
|
+
const spec = this.permission === 'auto'
|
|
1093
|
+
? { policy: 'allow-all', answerAsk: this.approvalPolicy.answerAsk }
|
|
1094
|
+
: this.approvalPolicy;
|
|
1095
|
+
const decision = decisionFor(spec, { mode: payload.mode }, 'tui');
|
|
1096
|
+
if (decision !== 'prompt') {
|
|
1097
|
+
if (decision === 'allow_once' && payload.toolName === 'network_egress') {
|
|
1098
|
+
this.ensureEgressLine(payload.arguments);
|
|
1099
|
+
}
|
|
1100
|
+
if (decision === 'allow_once' && payload.toolName)
|
|
1101
|
+
this.markAutoTool(payload.toolName);
|
|
1102
|
+
void this.client.decideApproval(payload.requestId, decision);
|
|
935
1103
|
this.touch();
|
|
936
1104
|
continue;
|
|
937
1105
|
}
|
|
@@ -943,8 +1111,40 @@ export class AgentTui {
|
|
|
943
1111
|
}
|
|
944
1112
|
else if (event.channel === 'ask-user:request') {
|
|
945
1113
|
const payload = event.payload;
|
|
946
|
-
if (payload.requestId)
|
|
947
|
-
|
|
1114
|
+
if (!payload.requestId)
|
|
1115
|
+
continue;
|
|
1116
|
+
const intro = (payload.prompt || payload.intro || '').trim();
|
|
1117
|
+
if (this.approvalPolicy.answerAsk) {
|
|
1118
|
+
const questions = payload.questions?.length ? payload.questions : [{ id: 'answer' }];
|
|
1119
|
+
void this.client.answerAsk(payload.requestId, {
|
|
1120
|
+
answers: autoAnswer(questions.flatMap((question) => question.id ? [{ id: question.id }] : [])),
|
|
1121
|
+
});
|
|
1122
|
+
const asked = questions
|
|
1123
|
+
.map((question) => question.text?.trim() ?? '')
|
|
1124
|
+
.filter((text) => text.length > 0);
|
|
1125
|
+
const answer = cliText('ask.unattended');
|
|
1126
|
+
const args = intro || asked[0] || '';
|
|
1127
|
+
const output = [...asked.map((text) => `问 ${text}`), `答 ${answer}`].join('\n');
|
|
1128
|
+
const line = this.latestAskLine();
|
|
1129
|
+
if (line && !autoAnswerStatus(line.status)) {
|
|
1130
|
+
if (!line.args)
|
|
1131
|
+
line.args = args;
|
|
1132
|
+
line.status = autoAnswerMark();
|
|
1133
|
+
line.output = output;
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
this.lines.push({
|
|
1137
|
+
kind: 'tool',
|
|
1138
|
+
name: 'ask_user',
|
|
1139
|
+
args,
|
|
1140
|
+
status: autoAnswerMark(),
|
|
1141
|
+
output,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
this.touch();
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
this.ask = { requestId: payload.requestId, prompt: intro };
|
|
948
1148
|
}
|
|
949
1149
|
else if (event.channel === 'goal-changed') {
|
|
950
1150
|
const payload = event.payload;
|
|
@@ -983,11 +1183,30 @@ export class AgentTui {
|
|
|
983
1183
|
if (nextTodos)
|
|
984
1184
|
this.todos = nextTodos;
|
|
985
1185
|
const next = toolLine(action);
|
|
986
|
-
|
|
1186
|
+
let index = next.id
|
|
987
1187
|
? this.lines.findIndex((line) => line.kind === 'tool' && line.id === next.id)
|
|
988
1188
|
: -1;
|
|
1189
|
+
if (index < 0 && next.name === 'ask_user')
|
|
1190
|
+
index = this.unboundAskLine();
|
|
1191
|
+
const prior = index >= 0 ? this.lines[index] : undefined;
|
|
1192
|
+
const keepAnswer = autoAnswerStatus(prior?.status);
|
|
1193
|
+
const auto = prior?.auto === true || (!keepAnswer && this.takePendingAuto(next.name ?? ''));
|
|
1194
|
+
if (auto) {
|
|
1195
|
+
next.auto = true;
|
|
1196
|
+
next.status = appendAuto(next.status);
|
|
1197
|
+
}
|
|
989
1198
|
if (index >= 0) {
|
|
990
|
-
next.open =
|
|
1199
|
+
next.open = prior?.open;
|
|
1200
|
+
if (keepAnswer && prior) {
|
|
1201
|
+
next.status = autoAnswerMark();
|
|
1202
|
+
if (prior.output)
|
|
1203
|
+
next.output = prior.output;
|
|
1204
|
+
if (!next.args && prior.args)
|
|
1205
|
+
next.args = prior.args;
|
|
1206
|
+
}
|
|
1207
|
+
else if (prior?.output && !next.output) {
|
|
1208
|
+
next.output = prior.output;
|
|
1209
|
+
}
|
|
991
1210
|
this.lines[index] = next;
|
|
992
1211
|
}
|
|
993
1212
|
else {
|
|
@@ -1152,7 +1371,18 @@ export class AgentTui {
|
|
|
1152
1371
|
async forkChat() {
|
|
1153
1372
|
if (!this.chatId)
|
|
1154
1373
|
return;
|
|
1155
|
-
const created = await this.client.request('POST', `/api/v2/chats/${encodeURIComponent(this.chatId)}/fork`, {});
|
|
1374
|
+
const created = await this.client.request('POST', `/api/v2/chats/${encodeURIComponent(this.chatId)}/fork`, { locale: 'zh' });
|
|
1375
|
+
if (created.status === 409) {
|
|
1376
|
+
this.status = '正在生成,结束后再分叉';
|
|
1377
|
+
this.touch();
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (created.status === 413) {
|
|
1381
|
+
const bytes = numberField(created.data, 'bytes');
|
|
1382
|
+
this.status = `工作区太大,无法分叉(${formatForkBytes(bytes)})`;
|
|
1383
|
+
this.touch();
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1156
1386
|
const chatId = created.status === 200 ? textField(created.data, 'chatId') : '';
|
|
1157
1387
|
if (!chatId) {
|
|
1158
1388
|
this.status = '分叉失败';
|
|
@@ -1274,6 +1504,43 @@ async function defaultSaveAttachments(chatId, files) {
|
|
|
1274
1504
|
...(file.error ? { error: file.error } : {}),
|
|
1275
1505
|
}));
|
|
1276
1506
|
}
|
|
1507
|
+
function egressTarget(value) {
|
|
1508
|
+
if (!value || typeof value !== 'object')
|
|
1509
|
+
return '';
|
|
1510
|
+
const record = value;
|
|
1511
|
+
const host = typeof record.host === 'string' ? record.host : '';
|
|
1512
|
+
if (!host)
|
|
1513
|
+
return '';
|
|
1514
|
+
const port = typeof record.port === 'number' ? record.port : null;
|
|
1515
|
+
if (port === null || port === 80 || port === 443)
|
|
1516
|
+
return host;
|
|
1517
|
+
return `${host}:${port}`;
|
|
1518
|
+
}
|
|
1519
|
+
function autoAnswerMark() {
|
|
1520
|
+
return `✓ ${cliText('tui.toolAutoAnswer')}`;
|
|
1521
|
+
}
|
|
1522
|
+
function autoAnswerStatus(status) {
|
|
1523
|
+
const phrase = cliText('tui.toolAutoAnswer');
|
|
1524
|
+
return status === phrase || status === autoAnswerMark() || (status?.endsWith(` ${phrase}`) ?? false);
|
|
1525
|
+
}
|
|
1526
|
+
function appendAuto(status) {
|
|
1527
|
+
const auto = cliText('tui.toolAuto');
|
|
1528
|
+
const session = cliText('tui.toolSessionAllow');
|
|
1529
|
+
if (autoAnswerStatus(status))
|
|
1530
|
+
return autoAnswerMark();
|
|
1531
|
+
if (!status || status === '…')
|
|
1532
|
+
return auto;
|
|
1533
|
+
if (status === session || status.endsWith(` ${session}`) || status === auto || status.endsWith(` ${auto}`))
|
|
1534
|
+
return status;
|
|
1535
|
+
return `${status} ${auto}`;
|
|
1536
|
+
}
|
|
1537
|
+
function sessionAllowStatus(status) {
|
|
1538
|
+
const session = cliText('tui.toolSessionAllow');
|
|
1539
|
+
return status === session || (status?.endsWith(` ${session}`) ?? false);
|
|
1540
|
+
}
|
|
1541
|
+
function openToolStatus(status) {
|
|
1542
|
+
return status === '…' || status === cliText('tui.toolAuto');
|
|
1543
|
+
}
|
|
1277
1544
|
function toolLine(action) {
|
|
1278
1545
|
const name = typeof action.tool === 'string' && action.tool ? action.tool : 'tool';
|
|
1279
1546
|
const title = typeof action.view?.title === 'string' ? action.view.title : '';
|
|
@@ -1300,6 +1567,13 @@ function textField(value, key) {
|
|
|
1300
1567
|
const field = value[key];
|
|
1301
1568
|
return typeof field === 'string' ? field : '';
|
|
1302
1569
|
}
|
|
1570
|
+
function formatForkBytes(bytes) {
|
|
1571
|
+
if (bytes >= 1024 * 1024)
|
|
1572
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1573
|
+
if (bytes >= 1024)
|
|
1574
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
1575
|
+
return `${bytes} B`;
|
|
1576
|
+
}
|
|
1303
1577
|
function numberField(value, key) {
|
|
1304
1578
|
if (!value || typeof value !== 'object')
|
|
1305
1579
|
return 0;
|
package/dist/tui/transcript.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export interface ToolAction {
|
|
|
9
9
|
success?: boolean;
|
|
10
10
|
error?: unknown;
|
|
11
11
|
durationMs?: number;
|
|
12
|
+
approval?: {
|
|
13
|
+
kind?: string;
|
|
14
|
+
cached?: boolean;
|
|
15
|
+
};
|
|
12
16
|
}
|
|
13
17
|
export declare function formatDuration(ms: number): string;
|
|
14
18
|
export declare function toolStatus(action: ToolAction): string;
|
package/dist/tui/transcript.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyChildEvent, childLines } from './children.js';
|
|
2
|
+
import { cliText } from '../copy.js';
|
|
2
3
|
export function formatDuration(ms) {
|
|
3
4
|
if (!Number.isFinite(ms) || ms < 0)
|
|
4
5
|
return '';
|
|
@@ -12,7 +13,11 @@ export function toolStatus(action) {
|
|
|
12
13
|
return '…';
|
|
13
14
|
const mark = action.success === false || action.error != null ? '✗' : '✓';
|
|
14
15
|
const duration = typeof action.durationMs === 'number' ? formatDuration(action.durationMs) : '';
|
|
15
|
-
|
|
16
|
+
const base = duration ? `${mark} ${duration}` : mark;
|
|
17
|
+
if (action.approval?.kind === 'allow_for_session' && action.approval.cached === true) {
|
|
18
|
+
return `${base} ${cliText('tui.toolSessionAllow')}`;
|
|
19
|
+
}
|
|
20
|
+
return base;
|
|
16
21
|
}
|
|
17
22
|
export function toolOutput(action) {
|
|
18
23
|
const error = textOf(action.error);
|
package/dist/tui/view.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BoxRenderable, ScrollBoxRenderable, TextRenderable, bold, dim as dimText, italic, reverse, strikethrough, t, underline, } from '@opentui/core';
|
|
2
2
|
import { splitAtCursor } from './editor.js';
|
|
3
3
|
import { childLines } from './children.js';
|
|
4
|
-
import { approvalLines, attachmentText, bannerLines, chatText, composerRows, draftHint, footerText, helpLines, pickText, readOnlyText, reasoningBlock, toolBlock, } from './screen.js';
|
|
4
|
+
import { approvalLines, attachmentText, bannerLines, projectStatusLine, chatText, composerRows, draftHint, footerText, helpLines, pickText, readOnlyText, reasoningBlock, toolBlock, } from './screen.js';
|
|
5
5
|
import { pageScrollLines } from './scroll.js';
|
|
6
6
|
import { plainMarkdown } from './transcript.js';
|
|
7
7
|
const canvas = '#16161e';
|
|
@@ -172,8 +172,10 @@ export class OpenTuiView {
|
|
|
172
172
|
this.readOnly.visible = screen.readOnly;
|
|
173
173
|
this.draft.content = draftContent(screen.draft, screen.cursor);
|
|
174
174
|
this.composer.height = composerRows(screen.draft, Math.max(1, this.renderer.width - 2));
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
const project = projectStatusLine(screen);
|
|
176
|
+
const status = [project, screen.status].filter((line) => line && line.length > 0).join('\n');
|
|
177
|
+
this.status.content = status;
|
|
178
|
+
this.status.visible = status.length > 0;
|
|
177
179
|
this.footer.content = footerText();
|
|
178
180
|
const attached = attachmentText(screen.attachments ?? []);
|
|
179
181
|
this.attached.content = attached;
|