@steerable/agent-cli 0.6.59 → 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
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface SessionTodo {
|
|
2
|
+
content: string;
|
|
3
|
+
status: 'pending' | 'in_progress' | 'completed';
|
|
4
|
+
}
|
|
5
|
+
/** The latest `todo_write` list. Each call replaces the whole list. */
|
|
6
|
+
export declare function todosFromAction(tool: string | undefined, args: unknown, result: unknown): SessionTodo[] | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** The latest `todo_write` list. Each call replaces the whole list. */
|
|
2
|
+
export function todosFromAction(tool, args, result) {
|
|
3
|
+
if (tool !== 'todo_write')
|
|
4
|
+
return null;
|
|
5
|
+
return readTodos(result) ?? readTodos(args);
|
|
6
|
+
}
|
|
7
|
+
function readTodos(value) {
|
|
8
|
+
const direct = todoItems(value);
|
|
9
|
+
if (direct)
|
|
10
|
+
return direct;
|
|
11
|
+
const record = asRecord(value);
|
|
12
|
+
if (!record)
|
|
13
|
+
return null;
|
|
14
|
+
const nested = asRecord(record.data);
|
|
15
|
+
const envelope = asRecord(nested?.value);
|
|
16
|
+
return todoItems(record.todos) ?? todoItems(nested?.todos) ?? todoItems(envelope?.todos);
|
|
17
|
+
}
|
|
18
|
+
function todoItems(value) {
|
|
19
|
+
if (!Array.isArray(value))
|
|
20
|
+
return null;
|
|
21
|
+
const todos = [];
|
|
22
|
+
for (const item of value) {
|
|
23
|
+
const row = asRecord(item);
|
|
24
|
+
if (!row || typeof row.content !== 'string' || row.content.length === 0)
|
|
25
|
+
continue;
|
|
26
|
+
const status = row.status === 'in_progress' || row.status === 'completed' ? row.status : 'pending';
|
|
27
|
+
todos.push({ content: row.content, status });
|
|
28
|
+
}
|
|
29
|
+
return todos.length > 0 ? todos : null;
|
|
30
|
+
}
|
|
31
|
+
function asRecord(value) {
|
|
32
|
+
if (typeof value === 'string') {
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('['))
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
return asRecord(JSON.parse(trimmed));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
void error;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
45
|
+
return null;
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ToolAction {
|
|
2
|
+
id?: string;
|
|
3
|
+
tool?: string;
|
|
4
|
+
arguments?: unknown;
|
|
5
|
+
view?: {
|
|
6
|
+
title?: string;
|
|
7
|
+
};
|
|
8
|
+
result?: unknown;
|
|
9
|
+
success?: boolean;
|
|
10
|
+
error?: unknown;
|
|
11
|
+
durationMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function formatDuration(ms: number): string;
|
|
14
|
+
export declare function toolStatus(action: ToolAction): string;
|
|
15
|
+
export declare function toolOutput(action: ToolAction): string;
|
|
16
|
+
export declare function plainMarkdown(source: string): string;
|
|
17
|
+
export interface StoredMessage {
|
|
18
|
+
role?: string;
|
|
19
|
+
content?: string;
|
|
20
|
+
createdAt?: string | number;
|
|
21
|
+
messageMetadata?: string | null;
|
|
22
|
+
}
|
|
23
|
+
export type HistoryRow = {
|
|
24
|
+
kind: 'user' | 'assistant' | 'tree' | 'reasoning';
|
|
25
|
+
text: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'tool';
|
|
28
|
+
action: ToolAction;
|
|
29
|
+
};
|
|
30
|
+
/** The list endpoint is newest-first. Dated lists are ordered by time; undated lists stay as given. */
|
|
31
|
+
export declare function historyRows(messages: readonly StoredMessage[]): HistoryRow[];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { applyChildEvent, childLines } from './children.js';
|
|
2
|
+
export function formatDuration(ms) {
|
|
3
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
4
|
+
return '';
|
|
5
|
+
if (ms < 10_000)
|
|
6
|
+
return `${(Math.round(ms / 100) / 10).toFixed(1)}s`;
|
|
7
|
+
return `${Math.round(ms / 1000)}s`;
|
|
8
|
+
}
|
|
9
|
+
export function toolStatus(action) {
|
|
10
|
+
const settled = typeof action.durationMs === 'number' || typeof action.success === 'boolean' || action.error != null;
|
|
11
|
+
if (!settled)
|
|
12
|
+
return '…';
|
|
13
|
+
const mark = action.success === false || action.error != null ? '✗' : '✓';
|
|
14
|
+
const duration = typeof action.durationMs === 'number' ? formatDuration(action.durationMs) : '';
|
|
15
|
+
return duration ? `${mark} ${duration}` : mark;
|
|
16
|
+
}
|
|
17
|
+
export function toolOutput(action) {
|
|
18
|
+
const error = textOf(action.error);
|
|
19
|
+
if (error)
|
|
20
|
+
return clip(error);
|
|
21
|
+
return clip(textOf(action.result));
|
|
22
|
+
}
|
|
23
|
+
export function plainMarkdown(source) {
|
|
24
|
+
const out = [];
|
|
25
|
+
let fence = false;
|
|
26
|
+
for (const line of source.replace(/\r\n/g, '\n').split('\n')) {
|
|
27
|
+
if (line.trim().startsWith('```')) {
|
|
28
|
+
fence = !fence;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (fence) {
|
|
32
|
+
out.push(line);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
out.push(line
|
|
36
|
+
.replace(/^#{1,6}\s+/, '')
|
|
37
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '$1')
|
|
38
|
+
.replace(/~~([^~\n]+)~~/g, '$1')
|
|
39
|
+
.replace(/`([^`\n]+)`/g, '$1')
|
|
40
|
+
.replace(/\[([^\]\n]+)\]\([^)\n]+\)/g, '$1'));
|
|
41
|
+
}
|
|
42
|
+
return out.join('\n');
|
|
43
|
+
}
|
|
44
|
+
function textOf(value) {
|
|
45
|
+
if (typeof value === 'string')
|
|
46
|
+
return value;
|
|
47
|
+
if (!value || typeof value !== 'object')
|
|
48
|
+
return '';
|
|
49
|
+
const record = value;
|
|
50
|
+
for (const key of ['stdout', 'output', 'content', 'text', 'message', 'error']) {
|
|
51
|
+
if (typeof record[key] === 'string' && record[key])
|
|
52
|
+
return record[key];
|
|
53
|
+
}
|
|
54
|
+
const data = record.data;
|
|
55
|
+
if (typeof data === 'string')
|
|
56
|
+
return data;
|
|
57
|
+
if (data && typeof data === 'object')
|
|
58
|
+
return textOf(data);
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
function clip(text) {
|
|
62
|
+
const trimmed = text.replace(/\s+$/g, '');
|
|
63
|
+
return trimmed.length > 4000 ? `${trimmed.slice(0, 4000)}…` : trimmed;
|
|
64
|
+
}
|
|
65
|
+
/** The list endpoint is newest-first. Dated lists are ordered by time; undated lists stay as given. */
|
|
66
|
+
export function historyRows(messages) {
|
|
67
|
+
const rows = [];
|
|
68
|
+
for (const message of inTimeOrder(messages)) {
|
|
69
|
+
if (message.role === 'user') {
|
|
70
|
+
rows.push({ kind: 'user', text: message.content ?? '' });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const meta = parseMeta(message.messageMetadata);
|
|
74
|
+
const timeline = Array.isArray(meta.timeline) ? meta.timeline : null;
|
|
75
|
+
if (timeline) {
|
|
76
|
+
let sawText = false;
|
|
77
|
+
for (const block of timeline) {
|
|
78
|
+
if (!block || typeof block !== 'object')
|
|
79
|
+
continue;
|
|
80
|
+
const record = block;
|
|
81
|
+
if (record.type === 'text' && typeof record.content === 'string' && record.content.length > 0) {
|
|
82
|
+
sawText = true;
|
|
83
|
+
rows.push({ kind: 'assistant', text: record.content });
|
|
84
|
+
}
|
|
85
|
+
else if (record.type === 'reasoning' && typeof record.content === 'string' && record.content.length > 0) {
|
|
86
|
+
rows.push({ kind: 'reasoning', text: record.content });
|
|
87
|
+
}
|
|
88
|
+
else if (record.type === 'tools' && Array.isArray(record.actions)) {
|
|
89
|
+
for (const action of record.actions) {
|
|
90
|
+
if (action && typeof action === 'object')
|
|
91
|
+
rows.push({ kind: 'tool', action: action });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!sawText && message.content)
|
|
96
|
+
rows.push({ kind: 'assistant', text: message.content });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
if (message.content)
|
|
100
|
+
rows.push({ kind: 'assistant', text: message.content });
|
|
101
|
+
const actions = Array.isArray(meta.executedActions) ? meta.executedActions : [];
|
|
102
|
+
for (const action of actions) {
|
|
103
|
+
if (action && typeof action === 'object')
|
|
104
|
+
rows.push({ kind: 'tool', action: action });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
appendChildren(rows, meta.orchestrationChildEvents);
|
|
108
|
+
}
|
|
109
|
+
return rows;
|
|
110
|
+
}
|
|
111
|
+
function inTimeOrder(messages) {
|
|
112
|
+
if (messages.length < 2)
|
|
113
|
+
return [...messages];
|
|
114
|
+
const times = messages.map((message) => message.createdAt == null ? null : String(message.createdAt));
|
|
115
|
+
if (times.some((time) => time == null))
|
|
116
|
+
return [...messages];
|
|
117
|
+
const first = times[0] ?? '';
|
|
118
|
+
const last = times[times.length - 1] ?? '';
|
|
119
|
+
if (first > last)
|
|
120
|
+
return [...messages].reverse();
|
|
121
|
+
return [...messages];
|
|
122
|
+
}
|
|
123
|
+
function appendChildren(rows, events) {
|
|
124
|
+
if (!Array.isArray(events))
|
|
125
|
+
return;
|
|
126
|
+
let children = [];
|
|
127
|
+
for (const event of events) {
|
|
128
|
+
if (event && typeof event === 'object')
|
|
129
|
+
children = applyChildEvent(children, event);
|
|
130
|
+
}
|
|
131
|
+
for (const line of childLines(children))
|
|
132
|
+
rows.push({ kind: 'tree', text: line });
|
|
133
|
+
}
|
|
134
|
+
function parseMeta(raw) {
|
|
135
|
+
if (!raw)
|
|
136
|
+
return {};
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(raw);
|
|
139
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
void error;
|
|
144
|
+
}
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** One status line from `GET /api/v2/usage/summary`. */
|
|
2
|
+
export function formatUsage(data) {
|
|
3
|
+
const totals = data?.totals ?? {};
|
|
4
|
+
const turns = numberOf(totals.turns);
|
|
5
|
+
const tokens = numberOf(totals.totalTokens);
|
|
6
|
+
const cost = typeof totals.costUsd === 'number' && Number.isFinite(totals.costUsd)
|
|
7
|
+
? ` · $${totals.costUsd.toFixed(2)}`
|
|
8
|
+
: '';
|
|
9
|
+
return `用量 ${turns} 回合 · ${tokens} token${cost}`;
|
|
10
|
+
}
|
|
11
|
+
function numberOf(value) {
|
|
12
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
13
|
+
}
|
package/dist/tui/view.d.ts
CHANGED
|
@@ -8,13 +8,24 @@ export declare class OpenTuiView {
|
|
|
8
8
|
private readonly body;
|
|
9
9
|
private readonly readOnly;
|
|
10
10
|
private readonly overlay;
|
|
11
|
+
private readonly picks;
|
|
12
|
+
private readonly attached;
|
|
13
|
+
private readonly queued;
|
|
14
|
+
private readonly search;
|
|
15
|
+
private readonly banner;
|
|
16
|
+
private readonly composer;
|
|
11
17
|
private readonly draft;
|
|
12
18
|
private readonly status;
|
|
13
19
|
private readonly footer;
|
|
14
20
|
private bodyKey;
|
|
15
21
|
private overlayKey;
|
|
22
|
+
private picksKey;
|
|
23
|
+
private readonly syntax;
|
|
16
24
|
constructor(renderer: CliRenderer);
|
|
25
|
+
scrollPage(direction: -1 | 1): void;
|
|
26
|
+
scrollEdge(edge: 'top' | 'bottom'): void;
|
|
17
27
|
apply(screen: TuiScreen): void;
|
|
18
28
|
private paintOverlay;
|
|
29
|
+
private paintPicks;
|
|
19
30
|
private replaceBody;
|
|
20
31
|
}
|
package/dist/tui/view.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { BoxRenderable, ScrollBoxRenderable, TextRenderable, } from '@opentui/core';
|
|
2
|
-
import {
|
|
1
|
+
import { BoxRenderable, MarkdownRenderable, ScrollBoxRenderable, SyntaxStyle, TextRenderable, dim as dimText, reverse, t, } from '@opentui/core';
|
|
2
|
+
import { splitAtCursor } from './editor.js';
|
|
3
|
+
import { childLines } from './children.js';
|
|
4
|
+
import { approvalLines, attachmentText, bannerLines, chatText, composerRows, draftHint, footerText, helpLines, pickText, readOnlyText, reasoningBlock, toolBlock, } from './screen.js';
|
|
5
|
+
import { pageScrollLines } from './scroll.js';
|
|
6
|
+
import { plainMarkdown } from './transcript.js';
|
|
3
7
|
const canvas = '#16161e';
|
|
4
8
|
const bar = '#24283b';
|
|
5
9
|
const ink = '#c0caf5';
|
|
@@ -18,13 +22,28 @@ export class OpenTuiView {
|
|
|
18
22
|
body;
|
|
19
23
|
readOnly;
|
|
20
24
|
overlay;
|
|
25
|
+
picks;
|
|
26
|
+
attached;
|
|
27
|
+
queued;
|
|
28
|
+
search;
|
|
29
|
+
banner;
|
|
30
|
+
composer;
|
|
21
31
|
draft;
|
|
22
32
|
status;
|
|
23
33
|
footer;
|
|
24
34
|
bodyKey = '';
|
|
25
35
|
overlayKey = '';
|
|
36
|
+
picksKey = '';
|
|
37
|
+
syntax;
|
|
26
38
|
constructor(renderer) {
|
|
27
39
|
this.renderer = renderer;
|
|
40
|
+
try {
|
|
41
|
+
this.syntax = SyntaxStyle.create();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
void error;
|
|
45
|
+
this.syntax = null;
|
|
46
|
+
}
|
|
28
47
|
const shell = new BoxRenderable(renderer, {
|
|
29
48
|
width: '100%',
|
|
30
49
|
height: '100%',
|
|
@@ -66,15 +85,38 @@ export class OpenTuiView {
|
|
|
66
85
|
paddingRight: 1,
|
|
67
86
|
visible: false,
|
|
68
87
|
});
|
|
69
|
-
|
|
88
|
+
this.picks = new BoxRenderable(renderer, {
|
|
89
|
+
width: '100%',
|
|
90
|
+
border: true,
|
|
91
|
+
borderStyle: 'rounded',
|
|
92
|
+
borderColor: user,
|
|
93
|
+
backgroundColor: '#1a1b2e',
|
|
94
|
+
paddingLeft: 1,
|
|
95
|
+
paddingRight: 1,
|
|
96
|
+
visible: false,
|
|
97
|
+
});
|
|
98
|
+
this.attached = text(renderer, warn);
|
|
99
|
+
this.queued = text(renderer, warn);
|
|
100
|
+
this.queued.visible = false;
|
|
101
|
+
this.search = text(renderer, warn);
|
|
102
|
+
this.search.visible = false;
|
|
103
|
+
this.banner = text(renderer, warn);
|
|
104
|
+
this.banner.visible = false;
|
|
105
|
+
this.composer = new BoxRenderable(renderer, {
|
|
70
106
|
width: '100%',
|
|
71
107
|
height: 1,
|
|
108
|
+
flexShrink: 0,
|
|
72
109
|
backgroundColor: bar,
|
|
73
110
|
paddingLeft: 1,
|
|
74
111
|
paddingRight: 1,
|
|
75
112
|
});
|
|
76
|
-
this.draft =
|
|
77
|
-
|
|
113
|
+
this.draft = new TextRenderable(renderer, {
|
|
114
|
+
content: '',
|
|
115
|
+
fg: prompt,
|
|
116
|
+
width: '100%',
|
|
117
|
+
wrapMode: 'word',
|
|
118
|
+
});
|
|
119
|
+
this.composer.add(this.draft);
|
|
78
120
|
this.status = text(renderer, warn);
|
|
79
121
|
this.footer = text(renderer, dim);
|
|
80
122
|
const footerBar = new BoxRenderable(renderer, {
|
|
@@ -89,20 +131,56 @@ export class OpenTuiView {
|
|
|
89
131
|
shell.add(this.body);
|
|
90
132
|
shell.add(this.readOnly);
|
|
91
133
|
shell.add(this.overlay);
|
|
92
|
-
shell.add(
|
|
134
|
+
shell.add(this.picks);
|
|
135
|
+
shell.add(this.attached);
|
|
136
|
+
shell.add(this.banner);
|
|
137
|
+
shell.add(this.queued);
|
|
138
|
+
shell.add(this.search);
|
|
139
|
+
shell.add(this.composer);
|
|
93
140
|
shell.add(this.status);
|
|
94
141
|
shell.add(footerBar);
|
|
95
142
|
renderer.root.add(shell);
|
|
96
143
|
}
|
|
144
|
+
scrollPage(direction) {
|
|
145
|
+
const height = this.body.viewport.height;
|
|
146
|
+
this.body.scrollBy(pageScrollLines(typeof height === 'number' ? height : 0, direction));
|
|
147
|
+
}
|
|
148
|
+
scrollEdge(edge) {
|
|
149
|
+
if (edge === 'top')
|
|
150
|
+
this.body.scrollTo(0);
|
|
151
|
+
else
|
|
152
|
+
this.body.scrollTo(this.body.scrollHeight);
|
|
153
|
+
}
|
|
97
154
|
apply(screen) {
|
|
98
155
|
this.heading.content = `${screen.product} ${screen.title}`;
|
|
99
156
|
this.model.content = screen.modelName;
|
|
100
157
|
this.readOnly.visible = screen.readOnly;
|
|
101
|
-
this.draft.content =
|
|
158
|
+
this.draft.content = draftContent(screen.draft, screen.cursor);
|
|
159
|
+
this.composer.height = composerRows(screen.draft, Math.max(1, this.renderer.width - 2));
|
|
102
160
|
this.status.content = screen.status;
|
|
103
161
|
this.status.visible = screen.status.length > 0;
|
|
104
162
|
this.footer.content = footerText();
|
|
163
|
+
const attached = attachmentText(screen.attachments ?? []);
|
|
164
|
+
this.attached.content = attached;
|
|
165
|
+
this.attached.visible = attached.length > 0;
|
|
166
|
+
const queued = screen.queued ?? [];
|
|
167
|
+
this.queued.content = queued.map((item) => `排队 ${item}`).join('\n');
|
|
168
|
+
this.queued.visible = queued.length > 0;
|
|
169
|
+
this.queued.height = queued.length;
|
|
170
|
+
const search = screen.search ?? '';
|
|
171
|
+
this.search.content = search;
|
|
172
|
+
this.search.visible = search.length > 0;
|
|
173
|
+
this.search.height = search.length > 0 ? 1 : 0;
|
|
174
|
+
const banner = bannerLines(screen);
|
|
175
|
+
this.banner.content = banner.join('\n');
|
|
176
|
+
this.banner.visible = banner.length > 0;
|
|
177
|
+
this.banner.height = banner.length;
|
|
178
|
+
if (screen.scroll)
|
|
179
|
+
this.scrollEdge(screen.scroll);
|
|
180
|
+
if (typeof screen.searchFocus === 'number')
|
|
181
|
+
this.body.scrollChildIntoView(`line-${screen.searchFocus}`);
|
|
105
182
|
this.paintOverlay(screen);
|
|
183
|
+
this.paintPicks(screen);
|
|
106
184
|
const key = bodyKey(screen);
|
|
107
185
|
if (key === this.bodyKey)
|
|
108
186
|
return;
|
|
@@ -135,6 +213,28 @@ export class OpenTuiView {
|
|
|
135
213
|
wrapMode: 'none',
|
|
136
214
|
}));
|
|
137
215
|
}
|
|
216
|
+
paintPicks(screen) {
|
|
217
|
+
const lines = (screen.picks ?? []).map(pickText);
|
|
218
|
+
const key = lines.join('\n');
|
|
219
|
+
if (key === this.picksKey)
|
|
220
|
+
return;
|
|
221
|
+
this.picksKey = key;
|
|
222
|
+
for (const child of this.picks.getChildren()) {
|
|
223
|
+
this.picks.remove(child);
|
|
224
|
+
child.destroyRecursively();
|
|
225
|
+
}
|
|
226
|
+
this.picks.visible = lines.length > 0;
|
|
227
|
+
this.picks.height = lines.length === 0 ? 0 : lines.length + 2;
|
|
228
|
+
if (lines.length === 0)
|
|
229
|
+
return;
|
|
230
|
+
this.picks.add(new TextRenderable(this.renderer, {
|
|
231
|
+
content: lines.join('\n'),
|
|
232
|
+
fg: ink,
|
|
233
|
+
width: '100%',
|
|
234
|
+
height: lines.length,
|
|
235
|
+
wrapMode: 'none',
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
138
238
|
replaceBody(screen) {
|
|
139
239
|
for (const child of this.body.getChildren()) {
|
|
140
240
|
this.body.remove(child);
|
|
@@ -160,20 +260,48 @@ export class OpenTuiView {
|
|
|
160
260
|
}
|
|
161
261
|
return;
|
|
162
262
|
}
|
|
163
|
-
|
|
164
|
-
this.body.add(transcriptRow(renderer, line));
|
|
263
|
+
screen.lines.forEach((line, index) => {
|
|
264
|
+
this.body.add(transcriptRow(renderer, line, this.syntax, `line-${index}`));
|
|
265
|
+
});
|
|
266
|
+
for (const line of childLines(screen.children ?? []))
|
|
267
|
+
this.body.add(text(renderer, tool, line));
|
|
165
268
|
}
|
|
166
269
|
}
|
|
270
|
+
function draftContent(draft, cursor) {
|
|
271
|
+
const { before, head, after } = splitAtCursor(draft, cursor);
|
|
272
|
+
const mark = reverse('▍');
|
|
273
|
+
if (draft.length === 0)
|
|
274
|
+
return t `> ${mark} ${dimText(draftHint())}`;
|
|
275
|
+
if (head.length === 0)
|
|
276
|
+
return t `> ${before}${mark}`;
|
|
277
|
+
return t `> ${before}${mark}${head}${after}`;
|
|
278
|
+
}
|
|
167
279
|
function bodyKey(screen) {
|
|
168
280
|
return JSON.stringify({
|
|
169
281
|
help: screen.help,
|
|
170
282
|
chats: screen.chats,
|
|
171
283
|
lines: screen.lines,
|
|
284
|
+
children: screen.children ?? [],
|
|
172
285
|
});
|
|
173
286
|
}
|
|
174
|
-
function transcriptRow(renderer, line) {
|
|
287
|
+
function transcriptRow(renderer, line, syntax, id) {
|
|
288
|
+
if (line.kind === 'reasoning') {
|
|
289
|
+
const card = new BoxRenderable(renderer, {
|
|
290
|
+
id,
|
|
291
|
+
width: '100%',
|
|
292
|
+
border: true,
|
|
293
|
+
borderStyle: 'rounded',
|
|
294
|
+
borderColor: warn,
|
|
295
|
+
paddingLeft: 1,
|
|
296
|
+
paddingRight: 1,
|
|
297
|
+
});
|
|
298
|
+
for (const row of reasoningBlock(line))
|
|
299
|
+
card.add(text(renderer, row.startsWith(' ') ? dim : warn, row));
|
|
300
|
+
return card;
|
|
301
|
+
}
|
|
175
302
|
if (line.kind === 'tool') {
|
|
176
303
|
const card = new BoxRenderable(renderer, {
|
|
304
|
+
id,
|
|
177
305
|
width: '100%',
|
|
178
306
|
border: true,
|
|
179
307
|
borderStyle: 'rounded',
|
|
@@ -181,14 +309,27 @@ function transcriptRow(renderer, line) {
|
|
|
181
309
|
paddingLeft: 1,
|
|
182
310
|
paddingRight: 1,
|
|
183
311
|
});
|
|
184
|
-
|
|
312
|
+
for (const row of toolBlock(line))
|
|
313
|
+
card.add(text(renderer, row.startsWith(' ') ? dim : tool, row));
|
|
185
314
|
return card;
|
|
186
315
|
}
|
|
187
316
|
if (line.kind === 'user') {
|
|
188
|
-
return text(renderer, user, `user ${line.text ?? ''}
|
|
317
|
+
return text(renderer, user, `user ${line.text ?? ''}`, id);
|
|
318
|
+
}
|
|
319
|
+
if (line.kind === 'assistant' && syntax) {
|
|
320
|
+
return new MarkdownRenderable(renderer, {
|
|
321
|
+
id,
|
|
322
|
+
content: line.text && line.text.length > 0 ? line.text : ' ',
|
|
323
|
+
syntaxStyle: syntax,
|
|
324
|
+
fg: ink,
|
|
325
|
+
conceal: true,
|
|
326
|
+
streaming: line.streaming === true,
|
|
327
|
+
width: '100%',
|
|
328
|
+
});
|
|
189
329
|
}
|
|
190
|
-
|
|
330
|
+
const plain = plainMarkdown(line.text ?? '');
|
|
331
|
+
return text(renderer, ink, plain.length > 0 ? plain : ' ', id);
|
|
191
332
|
}
|
|
192
|
-
function text(renderer, fg, content = '') {
|
|
193
|
-
return new TextRenderable(renderer, { content, fg, wrapMode: 'word' });
|
|
333
|
+
function text(renderer, fg, content = '', id) {
|
|
334
|
+
return new TextRenderable(renderer, { ...(id ? { id } : {}), content, fg, wrapMode: 'word' });
|
|
194
335
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.61",
|
|
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/",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@earendil-works/pi-tui": "0.87.1",
|
|
40
40
|
"@opentui/core": "0.5.12",
|
|
41
|
-
"@steerable/agent-client": "0.6.
|
|
42
|
-
"@steerable/agent-protocol": "0.6.
|
|
43
|
-
"@steerable/agent-shell": "0.6.
|
|
41
|
+
"@steerable/agent-client": "0.6.61",
|
|
42
|
+
"@steerable/agent-protocol": "0.6.61",
|
|
43
|
+
"@steerable/agent-shell": "0.6.61"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^22.10.5",
|