@steerable/agent-cli 0.6.57 → 0.6.59
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/run.js +51 -0
- package/dist/tui/screen.d.ts +9 -0
- package/dist/tui/screen.js +30 -14
- package/dist/tui/session.d.ts +2 -0
- package/dist/tui/session.js +4 -1
- package/dist/tui/view.d.ts +20 -0
- package/dist/tui/view.js +194 -0
- package/package.json +6 -5
package/dist/tui/run.js
CHANGED
|
@@ -3,6 +3,50 @@ import { installAgentKeybindings } from './keys.js';
|
|
|
3
3
|
import { AgentTui } from './session.js';
|
|
4
4
|
export async function runTui(options) {
|
|
5
5
|
installAgentKeybindings();
|
|
6
|
+
if (options.terminal)
|
|
7
|
+
return runPlainTui(options);
|
|
8
|
+
try {
|
|
9
|
+
return await runOpenTui(options);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (!openTuiUnavailable(error))
|
|
13
|
+
throw error;
|
|
14
|
+
return runPlainTui(options);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function runOpenTui(options) {
|
|
18
|
+
const { createCliRenderer } = await import('@opentui/core');
|
|
19
|
+
const { OpenTuiView } = await import('./view.js');
|
|
20
|
+
const renderer = await createCliRenderer({
|
|
21
|
+
exitOnCtrlC: false,
|
|
22
|
+
useMouse: false,
|
|
23
|
+
consoleMode: 'disabled',
|
|
24
|
+
openConsoleOnError: false,
|
|
25
|
+
screenMode: 'alternate-screen',
|
|
26
|
+
backgroundColor: '#16161e',
|
|
27
|
+
targetFps: 30,
|
|
28
|
+
});
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const session = new AgentTui(options.client, {
|
|
31
|
+
product: options.product,
|
|
32
|
+
...(options.dataDir ? { dataDir: options.dataDir } : {}),
|
|
33
|
+
onChange() {
|
|
34
|
+
view.apply(session.snapshot());
|
|
35
|
+
},
|
|
36
|
+
onExit() {
|
|
37
|
+
renderer.destroy();
|
|
38
|
+
resolve(0);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const view = new OpenTuiView(renderer);
|
|
42
|
+
renderer.prependInputHandler((sequence) => {
|
|
43
|
+
session.handleInput(sequence);
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
void session.open().then(() => view.apply(session.snapshot()));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function runPlainTui(options) {
|
|
6
50
|
const terminal = options.terminal ?? new ProcessTerminal();
|
|
7
51
|
const tui = new TuiAltScreen(terminal);
|
|
8
52
|
return new Promise((resolve) => {
|
|
@@ -23,3 +67,10 @@ export async function runTui(options) {
|
|
|
23
67
|
tui.start();
|
|
24
68
|
});
|
|
25
69
|
}
|
|
70
|
+
function openTuiUnavailable(error) {
|
|
71
|
+
const cause = error instanceof Error && error.cause instanceof Error ? error.cause.message : '';
|
|
72
|
+
const message = `${error instanceof Error ? error.message : String(error)} ${cause}`;
|
|
73
|
+
return message.includes('OpenTUI native FFI')
|
|
74
|
+
|| message.includes('--experimental-ffi')
|
|
75
|
+
|| message.includes('node:ffi');
|
|
76
|
+
}
|
package/dist/tui/screen.d.ts
CHANGED
|
@@ -30,4 +30,13 @@ export interface TuiScreen {
|
|
|
30
30
|
status: string;
|
|
31
31
|
}
|
|
32
32
|
export declare function renderScreen(screen: TuiScreen, width: number): string[];
|
|
33
|
+
export declare function toolText(line: TranscriptLine): string;
|
|
34
|
+
export declare function chatText(chat: ChatRow): string;
|
|
35
|
+
export declare function helpLines(): string[];
|
|
36
|
+
export declare function approvalLines(approval: {
|
|
37
|
+
toolName: string;
|
|
38
|
+
summary: string;
|
|
39
|
+
}): string[];
|
|
40
|
+
export declare function footerText(): string;
|
|
41
|
+
export declare const readOnlyText = "\u53EA\u8BFB \u00B7 \u53E6\u4E00\u4E2A\u8FDB\u7A0B\u6B63\u5728\u8FD0\u884C";
|
|
33
42
|
export declare function visibleText(lines: string[]): string;
|
package/dist/tui/screen.js
CHANGED
|
@@ -6,26 +6,45 @@ export function renderScreen(screen, width) {
|
|
|
6
6
|
box.addChild(new PlainLine(line));
|
|
7
7
|
return box.render(width);
|
|
8
8
|
}
|
|
9
|
+
export function toolText(line) {
|
|
10
|
+
return `▸ ${line.name} ${line.args} ${line.status ?? ''}`.trimEnd();
|
|
11
|
+
}
|
|
12
|
+
export function chatText(chat) {
|
|
13
|
+
const mark = chat.selected ? '*' : ' ';
|
|
14
|
+
const busy = chat.busy ? ' 只读' : '';
|
|
15
|
+
return `${mark} ${chat.id} ${chat.title}${busy}`;
|
|
16
|
+
}
|
|
17
|
+
export function helpLines() {
|
|
18
|
+
return ['/new 新会话', '/model 查看或切换模型', '/clear 清屏', '/help 帮助'];
|
|
19
|
+
}
|
|
20
|
+
export function approvalLines(approval) {
|
|
21
|
+
return [
|
|
22
|
+
`审批 ${approval.toolName} ${approval.summary}`.trimEnd(),
|
|
23
|
+
`${keyLabel('agent.approval.allowOnce')} 本次允许 ${keyLabel('agent.approval.allowSession')} 本会话 ${keyLabel('agent.approval.allowAlways')} 总是`,
|
|
24
|
+
`${keyLabel('agent.approval.denyOnce')} 拒绝 ${keyLabel('agent.approval.denySession')} 本会话拒绝 ${keyLabel('agent.approval.denyAlways')} 总是拒绝 ${keyLabel('agent.approval.abort')} 中止`,
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
export function footerText() {
|
|
28
|
+
return `${keyLabel('agent.interrupt')} 中断 · ${keyLabel('agent.chats')} 会话 · /help`;
|
|
29
|
+
}
|
|
30
|
+
export const readOnlyText = '只读 · 另一个进程正在运行';
|
|
9
31
|
export function visibleText(lines) {
|
|
10
32
|
return lines.map((line) => line.trimEnd()).join('\n');
|
|
11
33
|
}
|
|
12
34
|
function screenLines(screen) {
|
|
13
35
|
const lines = [`${screen.product} · ${screen.title} · ${screen.modelName}`];
|
|
14
36
|
if (screen.help) {
|
|
15
|
-
lines.push(
|
|
37
|
+
lines.push(...helpLines());
|
|
16
38
|
}
|
|
17
39
|
else if (screen.chats) {
|
|
18
40
|
lines.push('会话');
|
|
19
|
-
for (const chat of screen.chats)
|
|
20
|
-
|
|
21
|
-
const busy = chat.busy ? ' 只读' : '';
|
|
22
|
-
lines.push(`${mark} ${chat.id} ${chat.title}${busy}`);
|
|
23
|
-
}
|
|
41
|
+
for (const chat of screen.chats)
|
|
42
|
+
lines.push(chatText(chat));
|
|
24
43
|
}
|
|
25
44
|
else {
|
|
26
45
|
for (const line of screen.lines) {
|
|
27
46
|
if (line.kind === 'tool')
|
|
28
|
-
lines.push(
|
|
47
|
+
lines.push(toolText(line));
|
|
29
48
|
else if (line.kind === 'user')
|
|
30
49
|
lines.push(`user ${line.text ?? ''}`);
|
|
31
50
|
else
|
|
@@ -33,18 +52,15 @@ function screenLines(screen) {
|
|
|
33
52
|
}
|
|
34
53
|
}
|
|
35
54
|
if (screen.readOnly)
|
|
36
|
-
lines.push(
|
|
37
|
-
if (screen.approval)
|
|
38
|
-
lines.push(
|
|
39
|
-
lines.push(`${keyLabel('agent.approval.allowOnce')} 本次允许 ${keyLabel('agent.approval.allowSession')} 本会话 ${keyLabel('agent.approval.allowAlways')} 总是`);
|
|
40
|
-
lines.push(`${keyLabel('agent.approval.denyOnce')} 拒绝 ${keyLabel('agent.approval.denySession')} 本会话拒绝 ${keyLabel('agent.approval.denyAlways')} 总是拒绝 ${keyLabel('agent.approval.abort')} 中止`);
|
|
41
|
-
}
|
|
55
|
+
lines.push(readOnlyText);
|
|
56
|
+
if (screen.approval)
|
|
57
|
+
lines.push(...approvalLines(screen.approval));
|
|
42
58
|
if (screen.ask)
|
|
43
59
|
lines.push(`追问 ${screen.ask.prompt}`);
|
|
44
60
|
lines.push(`> ${screen.draft}`);
|
|
45
61
|
if (screen.status)
|
|
46
62
|
lines.push(screen.status);
|
|
47
|
-
lines.push(
|
|
63
|
+
lines.push(footerText());
|
|
48
64
|
return lines;
|
|
49
65
|
}
|
|
50
66
|
class PlainLine {
|
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 TuiScreen } from './screen.js';
|
|
3
4
|
export interface AgentTuiOptions {
|
|
4
5
|
product: string;
|
|
5
6
|
dataDir?: string;
|
|
@@ -31,6 +32,7 @@ export declare class AgentTui implements Component {
|
|
|
31
32
|
settled(): Promise<void>;
|
|
32
33
|
invalidate(): void;
|
|
33
34
|
render(width: number): string[];
|
|
35
|
+
snapshot(): TuiScreen;
|
|
34
36
|
handleInput(data: string): void;
|
|
35
37
|
private screen;
|
|
36
38
|
private showChats;
|
package/dist/tui/session.js
CHANGED
|
@@ -45,7 +45,10 @@ export class AgentTui {
|
|
|
45
45
|
}
|
|
46
46
|
invalidate() { }
|
|
47
47
|
render(width) {
|
|
48
|
-
return renderScreen(this.
|
|
48
|
+
return renderScreen(this.snapshot(), width);
|
|
49
|
+
}
|
|
50
|
+
snapshot() {
|
|
51
|
+
return this.screen();
|
|
49
52
|
}
|
|
50
53
|
handleInput(data) {
|
|
51
54
|
if (data.length > 1 && !data.includes('\x1b')) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type CliRenderer } from '@opentui/core';
|
|
2
|
+
import { type TuiScreen } from './screen.js';
|
|
3
|
+
/** Live terminal picture. The session still owns input and the text snapshot. */
|
|
4
|
+
export declare class OpenTuiView {
|
|
5
|
+
private readonly renderer;
|
|
6
|
+
private readonly heading;
|
|
7
|
+
private readonly model;
|
|
8
|
+
private readonly body;
|
|
9
|
+
private readonly readOnly;
|
|
10
|
+
private readonly overlay;
|
|
11
|
+
private readonly draft;
|
|
12
|
+
private readonly status;
|
|
13
|
+
private readonly footer;
|
|
14
|
+
private bodyKey;
|
|
15
|
+
private overlayKey;
|
|
16
|
+
constructor(renderer: CliRenderer);
|
|
17
|
+
apply(screen: TuiScreen): void;
|
|
18
|
+
private paintOverlay;
|
|
19
|
+
private replaceBody;
|
|
20
|
+
}
|
package/dist/tui/view.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { BoxRenderable, ScrollBoxRenderable, TextRenderable, } from '@opentui/core';
|
|
2
|
+
import { approvalLines, chatText, footerText, helpLines, readOnlyText, toolText, } from './screen.js';
|
|
3
|
+
const canvas = '#16161e';
|
|
4
|
+
const bar = '#24283b';
|
|
5
|
+
const ink = '#c0caf5';
|
|
6
|
+
const dim = '#565f89';
|
|
7
|
+
const user = '#7aa2f7';
|
|
8
|
+
const tool = '#9ece6a';
|
|
9
|
+
const warn = '#e0af68';
|
|
10
|
+
const danger = '#f7768e';
|
|
11
|
+
const prompt = '#bb9af7';
|
|
12
|
+
const selected = '#33467c';
|
|
13
|
+
/** Live terminal picture. The session still owns input and the text snapshot. */
|
|
14
|
+
export class OpenTuiView {
|
|
15
|
+
renderer;
|
|
16
|
+
heading;
|
|
17
|
+
model;
|
|
18
|
+
body;
|
|
19
|
+
readOnly;
|
|
20
|
+
overlay;
|
|
21
|
+
draft;
|
|
22
|
+
status;
|
|
23
|
+
footer;
|
|
24
|
+
bodyKey = '';
|
|
25
|
+
overlayKey = '';
|
|
26
|
+
constructor(renderer) {
|
|
27
|
+
this.renderer = renderer;
|
|
28
|
+
const shell = new BoxRenderable(renderer, {
|
|
29
|
+
width: '100%',
|
|
30
|
+
height: '100%',
|
|
31
|
+
flexDirection: 'column',
|
|
32
|
+
backgroundColor: canvas,
|
|
33
|
+
});
|
|
34
|
+
const header = new BoxRenderable(renderer, {
|
|
35
|
+
width: '100%',
|
|
36
|
+
height: 1,
|
|
37
|
+
flexDirection: 'row',
|
|
38
|
+
justifyContent: 'space-between',
|
|
39
|
+
backgroundColor: bar,
|
|
40
|
+
paddingLeft: 1,
|
|
41
|
+
paddingRight: 1,
|
|
42
|
+
});
|
|
43
|
+
this.heading = text(renderer, ink);
|
|
44
|
+
this.model = text(renderer, user);
|
|
45
|
+
header.add(this.heading);
|
|
46
|
+
header.add(this.model);
|
|
47
|
+
this.body = new ScrollBoxRenderable(renderer, {
|
|
48
|
+
flexGrow: 1,
|
|
49
|
+
flexShrink: 1,
|
|
50
|
+
width: '100%',
|
|
51
|
+
stickyScroll: true,
|
|
52
|
+
stickyStart: 'bottom',
|
|
53
|
+
viewportCulling: false,
|
|
54
|
+
rootOptions: { backgroundColor: canvas },
|
|
55
|
+
contentOptions: { backgroundColor: canvas, paddingLeft: 1, paddingRight: 1, gap: 1 },
|
|
56
|
+
});
|
|
57
|
+
this.readOnly = text(renderer, danger);
|
|
58
|
+
this.readOnly.content = readOnlyText;
|
|
59
|
+
this.overlay = new BoxRenderable(renderer, {
|
|
60
|
+
width: '100%',
|
|
61
|
+
border: true,
|
|
62
|
+
borderStyle: 'rounded',
|
|
63
|
+
borderColor: warn,
|
|
64
|
+
backgroundColor: '#2d2a1f',
|
|
65
|
+
paddingLeft: 1,
|
|
66
|
+
paddingRight: 1,
|
|
67
|
+
visible: false,
|
|
68
|
+
});
|
|
69
|
+
const input = new BoxRenderable(renderer, {
|
|
70
|
+
width: '100%',
|
|
71
|
+
height: 1,
|
|
72
|
+
backgroundColor: bar,
|
|
73
|
+
paddingLeft: 1,
|
|
74
|
+
paddingRight: 1,
|
|
75
|
+
});
|
|
76
|
+
this.draft = text(renderer, prompt);
|
|
77
|
+
input.add(this.draft);
|
|
78
|
+
this.status = text(renderer, warn);
|
|
79
|
+
this.footer = text(renderer, dim);
|
|
80
|
+
const footerBar = new BoxRenderable(renderer, {
|
|
81
|
+
width: '100%',
|
|
82
|
+
height: 1,
|
|
83
|
+
backgroundColor: canvas,
|
|
84
|
+
paddingLeft: 1,
|
|
85
|
+
paddingRight: 1,
|
|
86
|
+
});
|
|
87
|
+
footerBar.add(this.footer);
|
|
88
|
+
shell.add(header);
|
|
89
|
+
shell.add(this.body);
|
|
90
|
+
shell.add(this.readOnly);
|
|
91
|
+
shell.add(this.overlay);
|
|
92
|
+
shell.add(input);
|
|
93
|
+
shell.add(this.status);
|
|
94
|
+
shell.add(footerBar);
|
|
95
|
+
renderer.root.add(shell);
|
|
96
|
+
}
|
|
97
|
+
apply(screen) {
|
|
98
|
+
this.heading.content = `${screen.product} ${screen.title}`;
|
|
99
|
+
this.model.content = screen.modelName;
|
|
100
|
+
this.readOnly.visible = screen.readOnly;
|
|
101
|
+
this.draft.content = `> ${screen.draft}`;
|
|
102
|
+
this.status.content = screen.status;
|
|
103
|
+
this.status.visible = screen.status.length > 0;
|
|
104
|
+
this.footer.content = footerText();
|
|
105
|
+
this.paintOverlay(screen);
|
|
106
|
+
const key = bodyKey(screen);
|
|
107
|
+
if (key === this.bodyKey)
|
|
108
|
+
return;
|
|
109
|
+
this.bodyKey = key;
|
|
110
|
+
this.replaceBody(screen);
|
|
111
|
+
}
|
|
112
|
+
paintOverlay(screen) {
|
|
113
|
+
const lines = screen.approval
|
|
114
|
+
? approvalLines(screen.approval)
|
|
115
|
+
: screen.ask
|
|
116
|
+
? [`追问 ${screen.ask.prompt}`]
|
|
117
|
+
: [];
|
|
118
|
+
const key = lines.join('\n');
|
|
119
|
+
if (key === this.overlayKey)
|
|
120
|
+
return;
|
|
121
|
+
this.overlayKey = key;
|
|
122
|
+
for (const child of this.overlay.getChildren()) {
|
|
123
|
+
this.overlay.remove(child);
|
|
124
|
+
child.destroyRecursively();
|
|
125
|
+
}
|
|
126
|
+
this.overlay.visible = lines.length > 0;
|
|
127
|
+
this.overlay.height = lines.length === 0 ? 0 : lines.length + 2;
|
|
128
|
+
if (lines.length === 0)
|
|
129
|
+
return;
|
|
130
|
+
this.overlay.add(new TextRenderable(this.renderer, {
|
|
131
|
+
content: lines.join('\n'),
|
|
132
|
+
fg: ink,
|
|
133
|
+
width: '100%',
|
|
134
|
+
height: lines.length,
|
|
135
|
+
wrapMode: 'none',
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
replaceBody(screen) {
|
|
139
|
+
for (const child of this.body.getChildren()) {
|
|
140
|
+
this.body.remove(child);
|
|
141
|
+
child.destroyRecursively();
|
|
142
|
+
}
|
|
143
|
+
const renderer = this.renderer;
|
|
144
|
+
if (screen.help) {
|
|
145
|
+
for (const line of helpLines())
|
|
146
|
+
this.body.add(text(renderer, dim, line));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (screen.chats) {
|
|
150
|
+
this.body.add(text(renderer, dim, '会话'));
|
|
151
|
+
for (const chat of screen.chats) {
|
|
152
|
+
const row = new BoxRenderable(renderer, {
|
|
153
|
+
width: '100%',
|
|
154
|
+
height: 1,
|
|
155
|
+
backgroundColor: chat.selected ? selected : canvas,
|
|
156
|
+
paddingLeft: 1,
|
|
157
|
+
});
|
|
158
|
+
row.add(text(renderer, chat.selected ? ink : dim, chatText(chat)));
|
|
159
|
+
this.body.add(row);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (const line of screen.lines)
|
|
164
|
+
this.body.add(transcriptRow(renderer, line));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function bodyKey(screen) {
|
|
168
|
+
return JSON.stringify({
|
|
169
|
+
help: screen.help,
|
|
170
|
+
chats: screen.chats,
|
|
171
|
+
lines: screen.lines,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function transcriptRow(renderer, line) {
|
|
175
|
+
if (line.kind === 'tool') {
|
|
176
|
+
const card = new BoxRenderable(renderer, {
|
|
177
|
+
width: '100%',
|
|
178
|
+
border: true,
|
|
179
|
+
borderStyle: 'rounded',
|
|
180
|
+
borderColor: tool,
|
|
181
|
+
paddingLeft: 1,
|
|
182
|
+
paddingRight: 1,
|
|
183
|
+
});
|
|
184
|
+
card.add(text(renderer, tool, toolText(line)));
|
|
185
|
+
return card;
|
|
186
|
+
}
|
|
187
|
+
if (line.kind === 'user') {
|
|
188
|
+
return text(renderer, user, `user ${line.text ?? ''}`);
|
|
189
|
+
}
|
|
190
|
+
return text(renderer, ink, line.text && line.text.length > 0 ? line.text : ' ');
|
|
191
|
+
}
|
|
192
|
+
function text(renderer, fg, content = '') {
|
|
193
|
+
return new TextRenderable(renderer, { content, fg, wrapMode: 'word' });
|
|
194
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.59",
|
|
4
4
|
"description": "Product-neutral command line for a Steerable host. Products supply the bin name and brand.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -33,13 +33,14 @@
|
|
|
33
33
|
"provenance": true
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
36
|
+
"node": ">=26"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@earendil-works/pi-tui": "0.87.1",
|
|
40
|
-
"@
|
|
41
|
-
"@steerable/agent-
|
|
42
|
-
"@steerable/agent-
|
|
40
|
+
"@opentui/core": "0.5.12",
|
|
41
|
+
"@steerable/agent-client": "0.6.59",
|
|
42
|
+
"@steerable/agent-protocol": "0.6.59",
|
|
43
|
+
"@steerable/agent-shell": "0.6.59"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@types/node": "^22.10.5",
|