@onkernel/cua-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -0
- package/dist/cli.js +1758 -0
- package/dist/harness-models-GT8Ke1vt.js +106 -0
- package/dist/main-Bphx_zOj.js +899 -0
- package/package.json +48 -0
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { i as captureScreenshot, r as resolveCuaModelRef } from "./harness-models-GT8Ke1vt.js";
|
|
2
|
+
import { stderr } from "node:process";
|
|
3
|
+
import { estimateContextTokens, formatSkillInvocation } from "@onkernel/cua-agent";
|
|
4
|
+
import { listCuaModels } from "@onkernel/cua-ai";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { CombinedAutocompleteProvider, Container, Editor, Image, KeybindingsManager, Markdown, ProcessTerminal, Spacer, TUI, TUI_KEYBINDINGS, Text, allocateImageId, detectCapabilities, hyperlink, matchesKey, setCapabilities, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
+
//#region src/tui/debug-log.ts
|
|
10
|
+
const PI_RENDER_DIR = "/tmp/tui";
|
|
11
|
+
const PI_REDRAW_LOG = path.join(os.homedir(), ".pi", "agent", "pi-debug.log");
|
|
12
|
+
function openTuiDebugLog() {
|
|
13
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
|
|
14
|
+
const dir = path.join(os.tmpdir(), `cua-tui-debug-${stamp}-${process.pid}`);
|
|
15
|
+
const piRendersDir = path.join(dir, "pi-renders");
|
|
16
|
+
const terminalWriteLog = path.join(dir, "terminal-output.log");
|
|
17
|
+
const eventsPath = path.join(dir, "events.jsonl");
|
|
18
|
+
mkdirSync(piRendersDir, { recursive: true });
|
|
19
|
+
writeFileSync(path.join(dir, "README.txt"), [
|
|
20
|
+
"cua --debug-tui artifacts",
|
|
21
|
+
"",
|
|
22
|
+
"events.jsonl app-level event timeline",
|
|
23
|
+
"terminal-output.log raw terminal bytes written by pi-tui",
|
|
24
|
+
"pi-debug-redraw.log full redraw reasons from PI_DEBUG_REDRAW",
|
|
25
|
+
"pi-renders/ per-render pi-tui debug snapshots",
|
|
26
|
+
"",
|
|
27
|
+
"These artifacts are meant to be captured during a manual TUI repro."
|
|
28
|
+
].join("\n"));
|
|
29
|
+
const previousEnv = {
|
|
30
|
+
PI_TUI_DEBUG: process.env.PI_TUI_DEBUG,
|
|
31
|
+
PI_DEBUG_REDRAW: process.env.PI_DEBUG_REDRAW,
|
|
32
|
+
PI_TUI_WRITE_LOG: process.env.PI_TUI_WRITE_LOG
|
|
33
|
+
};
|
|
34
|
+
const initialPiRenderFiles = snapshotFiles(PI_RENDER_DIR);
|
|
35
|
+
const redrawLogSize = fileSize(PI_REDRAW_LOG);
|
|
36
|
+
process.env.PI_TUI_DEBUG = "1";
|
|
37
|
+
process.env.PI_DEBUG_REDRAW = "1";
|
|
38
|
+
process.env.PI_TUI_WRITE_LOG = terminalWriteLog;
|
|
39
|
+
stderr.write(`[cua] TUI debug logs: ${dir}\n`);
|
|
40
|
+
const writeEvent = (event, data = {}) => {
|
|
41
|
+
appendFileSync(eventsPath, JSON.stringify({
|
|
42
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
43
|
+
pid: process.pid,
|
|
44
|
+
event,
|
|
45
|
+
...data
|
|
46
|
+
}) + "\n");
|
|
47
|
+
};
|
|
48
|
+
writeEvent("debug_open", { dir });
|
|
49
|
+
let closed = false;
|
|
50
|
+
return {
|
|
51
|
+
dir,
|
|
52
|
+
log(event, data = {}) {
|
|
53
|
+
writeEvent(event, data);
|
|
54
|
+
},
|
|
55
|
+
close(data = {}) {
|
|
56
|
+
if (closed) return;
|
|
57
|
+
closed = true;
|
|
58
|
+
writeEvent("debug_close", data);
|
|
59
|
+
copyNewFiles(PI_RENDER_DIR, initialPiRenderFiles, piRendersDir);
|
|
60
|
+
copyRedrawLogDelta(PI_REDRAW_LOG, redrawLogSize, path.join(dir, "pi-debug-redraw.log"));
|
|
61
|
+
restoreEnv(previousEnv);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function snapshotFiles(dir) {
|
|
66
|
+
if (!existsSync(dir)) return /* @__PURE__ */ new Set();
|
|
67
|
+
return new Set(readdirSync(dir));
|
|
68
|
+
}
|
|
69
|
+
function fileSize(file) {
|
|
70
|
+
if (!existsSync(file)) return 0;
|
|
71
|
+
return statSync(file).size;
|
|
72
|
+
}
|
|
73
|
+
function copyNewFiles(sourceDir, before, targetDir) {
|
|
74
|
+
if (!existsSync(sourceDir)) return;
|
|
75
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
76
|
+
if (before.has(entry)) continue;
|
|
77
|
+
copyFileSync(path.join(sourceDir, entry), path.join(targetDir, entry));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function copyRedrawLogDelta(sourceFile, startSize, targetFile) {
|
|
81
|
+
if (!existsSync(sourceFile)) return;
|
|
82
|
+
const content = readFileSync(sourceFile);
|
|
83
|
+
const start = Math.min(startSize, content.length);
|
|
84
|
+
if (start >= content.length) return;
|
|
85
|
+
writeFileSync(targetFile, content.subarray(start));
|
|
86
|
+
}
|
|
87
|
+
function restoreEnv(previous) {
|
|
88
|
+
restoreVar("PI_TUI_DEBUG", previous.PI_TUI_DEBUG);
|
|
89
|
+
restoreVar("PI_DEBUG_REDRAW", previous.PI_DEBUG_REDRAW);
|
|
90
|
+
restoreVar("PI_TUI_WRITE_LOG", previous.PI_TUI_WRITE_LOG);
|
|
91
|
+
}
|
|
92
|
+
function restoreVar(name, value) {
|
|
93
|
+
if (value === void 0) {
|
|
94
|
+
delete process.env[name];
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
process.env[name] = value;
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/tui/diagnostics.ts
|
|
101
|
+
/**
|
|
102
|
+
* Resolve image protocol with explicit override > env var > pi-tui detection.
|
|
103
|
+
* Mutates pi-tui's cached capabilities so the {@link Image} component uses
|
|
104
|
+
* our resolved value on render.
|
|
105
|
+
*/
|
|
106
|
+
function resolveImageProtocol(flag) {
|
|
107
|
+
const override = normalize(flag) ?? normalize(process.env.CUA_IMAGE_PROTOCOL);
|
|
108
|
+
const detected = detectCapabilities();
|
|
109
|
+
let images;
|
|
110
|
+
if (override === "auto" || override === void 0) images = detected.images;
|
|
111
|
+
else if (override === "none") images = null;
|
|
112
|
+
else images = override;
|
|
113
|
+
const caps = {
|
|
114
|
+
images,
|
|
115
|
+
trueColor: detected.trueColor,
|
|
116
|
+
hyperlinks: detected.hyperlinks
|
|
117
|
+
};
|
|
118
|
+
setCapabilities(caps);
|
|
119
|
+
return caps;
|
|
120
|
+
}
|
|
121
|
+
function normalize(value) {
|
|
122
|
+
if (!value) return void 0;
|
|
123
|
+
const v = value.trim().toLowerCase();
|
|
124
|
+
if (v === "kitty" || v === "iterm2" || v === "none" || v === "auto") return v;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* One-line summary of the resolved terminal capabilities, suitable for
|
|
128
|
+
* the TUI header so users can see at a glance whether inline images will
|
|
129
|
+
* work and how to override.
|
|
130
|
+
*/
|
|
131
|
+
function summarizeCapabilities(applied, source) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
const tag = source === "override" ? " (override)" : "";
|
|
134
|
+
parts.push(`images=${applied.images ?? "none"}${tag}`);
|
|
135
|
+
if (applied.trueColor) parts.push("trueColor");
|
|
136
|
+
if (applied.hyperlinks) parts.push("hyperlinks");
|
|
137
|
+
return parts.join(" · ");
|
|
138
|
+
}
|
|
139
|
+
function applyAndSummarizeImageProtocol(flag) {
|
|
140
|
+
const overridden = !!normalize(flag) || !!normalize(process.env.CUA_IMAGE_PROTOCOL);
|
|
141
|
+
const caps = resolveImageProtocol(flag);
|
|
142
|
+
return {
|
|
143
|
+
caps,
|
|
144
|
+
summary: summarizeCapabilities(caps, overridden ? "override" : "auto"),
|
|
145
|
+
overridden
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/tui/themes.ts
|
|
150
|
+
const RESET = "\x1B[0m";
|
|
151
|
+
const ansi = {
|
|
152
|
+
dim: (text) => `\x1b[2m${text}${RESET}`,
|
|
153
|
+
bold: (text) => `\x1b[1m${text}${RESET}`,
|
|
154
|
+
italic: (text) => `\x1b[3m${text}${RESET}`,
|
|
155
|
+
underline: (text) => `\x1b[4m${text}${RESET}`,
|
|
156
|
+
strikethrough: (text) => `\x1b[9m${text}${RESET}`,
|
|
157
|
+
cyan: (text) => `\x1b[36m${text}${RESET}`,
|
|
158
|
+
green: (text) => `\x1b[32m${text}${RESET}`,
|
|
159
|
+
yellow: (text) => `\x1b[33m${text}${RESET}`,
|
|
160
|
+
red: (text) => `\x1b[31m${text}${RESET}`,
|
|
161
|
+
gray: (text) => `\x1b[90m${text}${RESET}`,
|
|
162
|
+
blue: (text) => `\x1b[34m${text}${RESET}`,
|
|
163
|
+
lightBlue: (text) => `\x1b[38;2;129;162;190m${text}${RESET}`,
|
|
164
|
+
magenta: (text) => `\x1b[35m${text}${RESET}`
|
|
165
|
+
};
|
|
166
|
+
const colors = ansi;
|
|
167
|
+
const editorTheme = {
|
|
168
|
+
borderColor: (text) => ansi.lightBlue(text),
|
|
169
|
+
selectList: {
|
|
170
|
+
selectedPrefix: (text) => ansi.cyan(text),
|
|
171
|
+
selectedText: (text) => ansi.cyan(text),
|
|
172
|
+
description: (text) => ansi.dim(text),
|
|
173
|
+
scrollInfo: (text) => ansi.dim(text),
|
|
174
|
+
noMatch: (text) => ansi.dim(text)
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const imageTheme = { fallbackColor: (text) => ansi.dim(text) };
|
|
178
|
+
const markdownTheme = {
|
|
179
|
+
heading: (text) => ansi.bold(text),
|
|
180
|
+
link: (text) => ansi.cyan(text),
|
|
181
|
+
linkUrl: (text) => ansi.dim(text),
|
|
182
|
+
code: (text) => ansi.magenta(text),
|
|
183
|
+
codeBlock: (text) => text,
|
|
184
|
+
codeBlockBorder: (text) => ansi.dim(text),
|
|
185
|
+
quote: (text) => ansi.dim(text),
|
|
186
|
+
quoteBorder: (text) => ansi.dim(text),
|
|
187
|
+
hr: (text) => ansi.dim(text),
|
|
188
|
+
listBullet: (text) => ansi.cyan(text),
|
|
189
|
+
bold: (text) => ansi.bold(text),
|
|
190
|
+
italic: (text) => ansi.italic(text),
|
|
191
|
+
strikethrough: (text) => ansi.strikethrough(text),
|
|
192
|
+
underline: (text) => ansi.underline(text)
|
|
193
|
+
};
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/tui/message-list.ts
|
|
196
|
+
/**
|
|
197
|
+
* Append-only chat log of user prompts, assistant text, tool-call summaries,
|
|
198
|
+
* and inline error notes. Assistant blocks render through pi-tui's
|
|
199
|
+
* {@link Markdown}; everything else uses plain styled {@link Text}.
|
|
200
|
+
*/
|
|
201
|
+
var MessageList = class extends Container {
|
|
202
|
+
addUser(text) {
|
|
203
|
+
this.appendBlock([colors.bold("you ") + colors.dim("›") + " " + text]);
|
|
204
|
+
}
|
|
205
|
+
addAssistantStart() {
|
|
206
|
+
const buffer = new AssistantBuffer();
|
|
207
|
+
this.addChild(buffer);
|
|
208
|
+
this.invalidate();
|
|
209
|
+
return buffer;
|
|
210
|
+
}
|
|
211
|
+
addToolCall(name, args) {
|
|
212
|
+
const summary = formatToolCall(name, args);
|
|
213
|
+
this.appendBlock([colors.cyan("· ") + colors.dim(name) + " " + summary]);
|
|
214
|
+
}
|
|
215
|
+
addToolResult(name, ok, summary) {
|
|
216
|
+
const icon = ok ? colors.green("✓") : colors.red("✗");
|
|
217
|
+
this.appendBlock([` ${icon} ${colors.dim(name)} ${summary}`]);
|
|
218
|
+
}
|
|
219
|
+
addNotice(text) {
|
|
220
|
+
this.appendBlock([colors.yellow("· ") + colors.dim(text)]);
|
|
221
|
+
}
|
|
222
|
+
addError(text) {
|
|
223
|
+
this.appendBlock([colors.red("error ") + text]);
|
|
224
|
+
}
|
|
225
|
+
appendBlock(lines) {
|
|
226
|
+
for (const line of lines) this.addChild(new Text(line, 0, 0));
|
|
227
|
+
this.invalidate();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
/** Live-updating buffer for the in-flight assistant message. */
|
|
231
|
+
var AssistantBuffer = class extends Container {
|
|
232
|
+
text = "";
|
|
233
|
+
body;
|
|
234
|
+
constructor() {
|
|
235
|
+
super();
|
|
236
|
+
this.addChild(new Text(colors.green("assistant"), 0, 0));
|
|
237
|
+
this.body = new Markdown("", 0, 0, markdownTheme);
|
|
238
|
+
this.addChild(this.body);
|
|
239
|
+
}
|
|
240
|
+
append(delta) {
|
|
241
|
+
this.text += delta;
|
|
242
|
+
this.body.setText(this.text);
|
|
243
|
+
this.invalidate();
|
|
244
|
+
}
|
|
245
|
+
end() {
|
|
246
|
+
if (!this.text.trim()) this.children = [];
|
|
247
|
+
this.invalidate();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
function formatToolCall(name, args) {
|
|
251
|
+
if (!args || typeof args !== "object") return "";
|
|
252
|
+
const obj = args;
|
|
253
|
+
switch (name) {
|
|
254
|
+
case "computer_batch": {
|
|
255
|
+
const actions = Array.isArray(obj.actions) ? obj.actions : [];
|
|
256
|
+
if (actions.length === 0) return "(empty)";
|
|
257
|
+
const parts = actions.slice(0, 4).map(describeAction);
|
|
258
|
+
const more = actions.length > 4 ? colors.dim(` +${actions.length - 4} more`) : "";
|
|
259
|
+
return parts.join(colors.dim(" → ")) + more;
|
|
260
|
+
}
|
|
261
|
+
case "computer_use_extra": {
|
|
262
|
+
const action = typeof obj.action === "string" ? obj.action : "?";
|
|
263
|
+
if (action === "goto" && typeof obj.url === "string") return `goto(${obj.url})`;
|
|
264
|
+
return action;
|
|
265
|
+
}
|
|
266
|
+
case "bash": return colors.dim(typeof obj.command === "string" ? truncate$1(obj.command, 80) : "");
|
|
267
|
+
case "read":
|
|
268
|
+
case "write":
|
|
269
|
+
case "edit": return colors.dim(typeof obj.path === "string" ? obj.path : "");
|
|
270
|
+
default: return describeAction(obj);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function truncate$1(text, max) {
|
|
274
|
+
if (text.length <= max) return text;
|
|
275
|
+
return text.slice(0, max - 1) + "…";
|
|
276
|
+
}
|
|
277
|
+
function describeAction(action) {
|
|
278
|
+
const t = typeof action.type === "string" ? action.type : "";
|
|
279
|
+
const num = (v) => typeof v === "number" ? Math.trunc(v) : 0;
|
|
280
|
+
switch (t) {
|
|
281
|
+
case "click": return `click(${num(action.x)},${num(action.y)})`;
|
|
282
|
+
case "double_click": return `dblclick(${num(action.x)},${num(action.y)})`;
|
|
283
|
+
case "triple_click": return `triple(${num(action.x)},${num(action.y)})`;
|
|
284
|
+
case "type": return `type(${truncate$1(JSON.stringify(action.text ?? ""), 24)})`;
|
|
285
|
+
case "keypress": return `key(${action.keys?.join("+") ?? ""})`;
|
|
286
|
+
case "scroll": return `scroll(${num(action.x)},${num(action.y)})`;
|
|
287
|
+
case "move": return `move(${num(action.x)},${num(action.y)})`;
|
|
288
|
+
case "drag": return `drag(...)`;
|
|
289
|
+
case "wait": return `wait(${typeof action.ms === "number" ? action.ms : 1e3}ms)`;
|
|
290
|
+
case "goto": return `goto(${typeof action.url === "string" ? action.url : ""})`;
|
|
291
|
+
case "back": return "back";
|
|
292
|
+
case "forward": return "forward";
|
|
293
|
+
case "url": return "url";
|
|
294
|
+
case "screenshot": return "screenshot";
|
|
295
|
+
default: return t || colors.dim(truncate$1(JSON.stringify(action), 80));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/tui/screenshot-widget.ts
|
|
300
|
+
const MAX_WIDTH_CELLS = 60;
|
|
301
|
+
/**
|
|
302
|
+
* Sticky screenshot widget that re-renders the latest tool screenshot
|
|
303
|
+
* inline using pi-tui's terminal-image (Kitty / iTerm2). Falls back to
|
|
304
|
+
* a compact text card on terminals without inline image support.
|
|
305
|
+
*/
|
|
306
|
+
var ScreenshotWidget = class extends Container {
|
|
307
|
+
imageId = allocateImageId();
|
|
308
|
+
clear() {
|
|
309
|
+
this.children = [];
|
|
310
|
+
this.invalidate();
|
|
311
|
+
}
|
|
312
|
+
update(pngBase64, mimeType = "image/png") {
|
|
313
|
+
const image = new Image(pngBase64, mimeType, imageTheme, {
|
|
314
|
+
maxWidthCells: MAX_WIDTH_CELLS,
|
|
315
|
+
imageId: this.imageId
|
|
316
|
+
});
|
|
317
|
+
this.children = [image];
|
|
318
|
+
this.invalidate();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/tui/slash-commands.ts
|
|
323
|
+
/**
|
|
324
|
+
* Build an autocomplete provider for the TUI editor with the slash commands
|
|
325
|
+
* the interactive app supports: `/model`, `/thinking`, `/compact`, plus a
|
|
326
|
+
* `/skill:<name>` entry per loaded skill.
|
|
327
|
+
*
|
|
328
|
+
* Model and thinking values are exposed as `getArgumentCompletions` so
|
|
329
|
+
* users can tab through CUA refs and reasoning levels.
|
|
330
|
+
*/
|
|
331
|
+
function buildAutocompleteProvider(cwd, skills) {
|
|
332
|
+
const commands = [];
|
|
333
|
+
commands.push({
|
|
334
|
+
name: "model",
|
|
335
|
+
description: "Switch the active CUA model",
|
|
336
|
+
argumentHint: "<provider:model>",
|
|
337
|
+
getArgumentCompletions: (prefix) => modelCompletions(prefix)
|
|
338
|
+
});
|
|
339
|
+
commands.push({
|
|
340
|
+
name: "thinking",
|
|
341
|
+
description: "Set the reasoning level for future turns",
|
|
342
|
+
argumentHint: "<off|minimal|low|medium|high|xhigh>",
|
|
343
|
+
getArgumentCompletions: (prefix) => thinkingCompletions(prefix)
|
|
344
|
+
});
|
|
345
|
+
commands.push({
|
|
346
|
+
name: "compact",
|
|
347
|
+
description: "Summarize older turns to free context budget"
|
|
348
|
+
});
|
|
349
|
+
for (const skill of skills) commands.push({
|
|
350
|
+
name: `skill:${skill.name}`,
|
|
351
|
+
description: skill.description
|
|
352
|
+
});
|
|
353
|
+
return new CombinedAutocompleteProvider(commands, cwd);
|
|
354
|
+
}
|
|
355
|
+
function modelCompletions(prefix) {
|
|
356
|
+
const all = listCuaModels();
|
|
357
|
+
const trimmed = prefix.trim().toLowerCase();
|
|
358
|
+
return (trimmed ? all.filter((m) => m.ref.toLowerCase().includes(trimmed) || m.model.toLowerCase().includes(trimmed)) : all).map((m) => ({
|
|
359
|
+
value: m.ref,
|
|
360
|
+
label: m.ref,
|
|
361
|
+
description: m.name
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
const THINKING_LEVELS = [
|
|
365
|
+
{
|
|
366
|
+
value: "off",
|
|
367
|
+
description: "Disable reasoning"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
value: "minimal",
|
|
371
|
+
description: "Minimal reasoning"
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
value: "low",
|
|
375
|
+
description: "Low reasoning (default)"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
value: "medium",
|
|
379
|
+
description: "Medium reasoning"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
value: "high",
|
|
383
|
+
description: "High reasoning"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
value: "xhigh",
|
|
387
|
+
description: "Maximum reasoning (selected models only)"
|
|
388
|
+
}
|
|
389
|
+
];
|
|
390
|
+
function thinkingCompletions(prefix) {
|
|
391
|
+
const trimmed = prefix.trim().toLowerCase();
|
|
392
|
+
return (trimmed ? THINKING_LEVELS.filter((t) => t.value.startsWith(trimmed)) : THINKING_LEVELS).map((t) => ({
|
|
393
|
+
value: t.value,
|
|
394
|
+
label: t.value,
|
|
395
|
+
description: t.description
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Recognize the supported slash-command forms. Returns undefined when the
|
|
400
|
+
* text is a regular user prompt.
|
|
401
|
+
*/
|
|
402
|
+
function parseSlashCommand(text) {
|
|
403
|
+
const trimmed = text.trim();
|
|
404
|
+
if (!trimmed.startsWith("/")) return void 0;
|
|
405
|
+
const skillMatch = trimmed.match(/^\/skill:([A-Za-z0-9_\-.]+)\s*(.*)$/);
|
|
406
|
+
if (skillMatch) {
|
|
407
|
+
const [, name, rest] = skillMatch;
|
|
408
|
+
return {
|
|
409
|
+
command: "skill",
|
|
410
|
+
name: name ?? "",
|
|
411
|
+
remainder: (rest ?? "").trim()
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const builtinMatch = trimmed.match(/^\/(model|thinking|compact)\s*(.*)$/);
|
|
415
|
+
if (builtinMatch) {
|
|
416
|
+
const [, name, rest] = builtinMatch;
|
|
417
|
+
return {
|
|
418
|
+
command: name,
|
|
419
|
+
argument: (rest ?? "").trim()
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/tui/status-line.ts
|
|
425
|
+
var StatusLine = class extends Text {
|
|
426
|
+
state;
|
|
427
|
+
constructor(initial) {
|
|
428
|
+
super("", 0, 0);
|
|
429
|
+
this.state = initial;
|
|
430
|
+
this.refresh();
|
|
431
|
+
}
|
|
432
|
+
update(patch) {
|
|
433
|
+
this.state = {
|
|
434
|
+
...this.state,
|
|
435
|
+
...patch
|
|
436
|
+
};
|
|
437
|
+
this.refresh();
|
|
438
|
+
}
|
|
439
|
+
refresh() {
|
|
440
|
+
const sep = colors.dim(" · ");
|
|
441
|
+
const parts = [colors.bold("cua")];
|
|
442
|
+
if (this.state.liveUrl) parts.push(colors.dim("browser ") + hyperlink(this.state.liveUrl, this.state.liveUrl));
|
|
443
|
+
else if (this.state.browserSession) parts.push(colors.dim("browser ") + this.state.browserSession.slice(0, 6) + "…");
|
|
444
|
+
if (this.state.currentUrl) parts.push(colors.dim("url ") + truncate(this.state.currentUrl, 50));
|
|
445
|
+
if (this.state.tokens !== void 0) parts.push(colors.dim("tokens ") + this.state.tokens.toLocaleString());
|
|
446
|
+
if (this.state.cost !== void 0) parts.push(colors.dim("$") + this.state.cost.toFixed(3));
|
|
447
|
+
if (this.state.working) parts.push(colors.yellow(`⏳ ${this.state.working}`));
|
|
448
|
+
this.setText(parts.join(sep));
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
function truncate(text, max) {
|
|
452
|
+
if (text.length <= max) return text;
|
|
453
|
+
return text.slice(0, max - 1) + "…";
|
|
454
|
+
}
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/tui/telemetry-footer.ts
|
|
457
|
+
var TelemetryFooter = class {
|
|
458
|
+
state;
|
|
459
|
+
constructor(initial) {
|
|
460
|
+
this.state = initial;
|
|
461
|
+
}
|
|
462
|
+
update(patch) {
|
|
463
|
+
this.state = {
|
|
464
|
+
...this.state,
|
|
465
|
+
...patch
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
invalidate() {}
|
|
469
|
+
render(width) {
|
|
470
|
+
const left = this.renderContextUsage();
|
|
471
|
+
const right = this.renderModelInfo();
|
|
472
|
+
if (!left && !right) return [" ".repeat(width)];
|
|
473
|
+
if (!left) return [padToWidth(truncateToWidth(right, width), width)];
|
|
474
|
+
if (!right) return [padToWidth(truncateToWidth(left, width), width)];
|
|
475
|
+
const rightWidth = visibleWidth(right);
|
|
476
|
+
if (rightWidth >= width) return [padToWidth(truncateToWidth(right, width), width)];
|
|
477
|
+
const gap = 1;
|
|
478
|
+
const leftText = truncateToWidth(left, Math.max(1, width - rightWidth - gap));
|
|
479
|
+
return [padToWidth(leftText + " ".repeat(Math.max(gap, width - visibleWidth(leftText) - rightWidth)) + right, width)];
|
|
480
|
+
}
|
|
481
|
+
renderContextUsage() {
|
|
482
|
+
if (!this.state.contextWindow || this.state.contextWindow <= 0) return "";
|
|
483
|
+
const used = Math.max(0, this.state.contextTokens ?? 0);
|
|
484
|
+
const percent = this.state.contextWindow > 0 ? (used / this.state.contextWindow * 100).toFixed(1) : "?";
|
|
485
|
+
return colors.dim(`${percent}%/${formatTokens(this.state.contextWindow)}`);
|
|
486
|
+
}
|
|
487
|
+
renderModelInfo() {
|
|
488
|
+
const modelLabel = this.state.provider && this.state.model ? `${this.state.provider}/${this.state.model}` : this.state.model ?? "";
|
|
489
|
+
if (!modelLabel) return "";
|
|
490
|
+
const thinking = this.state.thinkingLevel && this.state.thinkingLevel.length > 0 ? this.state.thinkingLevel === "off" ? "thinking off" : this.state.thinkingLevel : "";
|
|
491
|
+
if (!thinking) return colors.dim(modelLabel);
|
|
492
|
+
return colors.dim(modelLabel) + colors.dim(" • ") + colors.dim(thinking);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
function formatTokens(tokens) {
|
|
496
|
+
if (tokens >= 1e6) return `${trimFraction(tokens / 1e6)}M`;
|
|
497
|
+
if (tokens >= 1e3) return `${trimFraction(tokens / 1e3)}K`;
|
|
498
|
+
return Math.round(tokens).toString();
|
|
499
|
+
}
|
|
500
|
+
function trimFraction(value) {
|
|
501
|
+
const rounded = value >= 10 ? value.toFixed(0) : value.toFixed(1);
|
|
502
|
+
return rounded.endsWith(".0") ? rounded.slice(0, -2) : rounded;
|
|
503
|
+
}
|
|
504
|
+
function padToWidth(text, width) {
|
|
505
|
+
const pad = Math.max(0, width - visibleWidth(text));
|
|
506
|
+
return text + " ".repeat(pad);
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/tui/main.ts
|
|
510
|
+
/**
|
|
511
|
+
* Run the interactive cua TUI: pi-tui differential renderer with header,
|
|
512
|
+
* message list, sticky screenshot widget, editor (autocomplete-backed slash
|
|
513
|
+
* commands), status line, and telemetry footer. Drives a {@link CuaAgentHarness}
|
|
514
|
+
* directly via `harness.subscribe()`.
|
|
515
|
+
*/
|
|
516
|
+
async function runInteractive(opts) {
|
|
517
|
+
const { summary: capsSummary, overridden } = applyAndSummarizeImageProtocol(opts.imageProtocol);
|
|
518
|
+
const debug = opts.debugTui ? openTuiDebugLog() : void 0;
|
|
519
|
+
const initialModel = opts.harness.getModel();
|
|
520
|
+
const initialThinking = opts.harness.getThinkingLevel();
|
|
521
|
+
const initialContextWindow = initialModel.contextWindow ?? void 0;
|
|
522
|
+
debug?.log("interactive_init", {
|
|
523
|
+
model: opts.modelRef,
|
|
524
|
+
browserSession: opts.browserHandle.browser.session_id,
|
|
525
|
+
liveUrl: opts.browserHandle.browser.browser_live_view_url,
|
|
526
|
+
capsSummary,
|
|
527
|
+
imageProtocol: opts.imageProtocol ?? "auto",
|
|
528
|
+
overridden
|
|
529
|
+
});
|
|
530
|
+
const terminal = new ProcessTerminal();
|
|
531
|
+
const tui = new TUI(terminal);
|
|
532
|
+
const requestRender = (reason, force = false, data = {}) => {
|
|
533
|
+
debug?.log("request_render", {
|
|
534
|
+
reason,
|
|
535
|
+
force,
|
|
536
|
+
columns: terminal.columns,
|
|
537
|
+
rows: terminal.rows,
|
|
538
|
+
fullRedraws: tui.fullRedraws,
|
|
539
|
+
...data
|
|
540
|
+
});
|
|
541
|
+
tui.requestRender(force);
|
|
542
|
+
};
|
|
543
|
+
new KeybindingsManager(TUI_KEYBINDINGS);
|
|
544
|
+
const editor = new Editor(tui, editorTheme);
|
|
545
|
+
editor.setAutocompleteProvider(buildAutocompleteProvider(opts.cwd, opts.skills ?? []));
|
|
546
|
+
const messages = new MessageList();
|
|
547
|
+
const screenshot = new ScreenshotWidget();
|
|
548
|
+
const liveUrl = opts.browserHandle.browser.browser_live_view_url;
|
|
549
|
+
const status = new StatusLine({
|
|
550
|
+
model: modelLabel(initialModel),
|
|
551
|
+
browserSession: opts.browserHandle.browser.session_id,
|
|
552
|
+
liveUrl
|
|
553
|
+
});
|
|
554
|
+
const footer = new TelemetryFooter({
|
|
555
|
+
provider: opts.provider,
|
|
556
|
+
model: modelLabel(initialModel),
|
|
557
|
+
thinkingLevel: initialThinking,
|
|
558
|
+
contextWindow: initialContextWindow,
|
|
559
|
+
contextTokens: 0
|
|
560
|
+
});
|
|
561
|
+
const header = new Container();
|
|
562
|
+
header.addChild(new Text(colors.bold("cua") + colors.dim(" — kernel-cloud-browser computer-use agent"), 0, 0));
|
|
563
|
+
const capsHint = overridden ? colors.dim(capsSummary) : colors.dim(capsSummary + " · set CUA_IMAGE_PROTOCOL=kitty|iterm2 to force inline images");
|
|
564
|
+
header.addChild(new Text(capsHint, 0, 0));
|
|
565
|
+
if (liveUrl) header.addChild(new Text(colors.dim("live ") + hyperlink(liveUrl, liveUrl), 0, 0));
|
|
566
|
+
header.addChild(new Text("", 0, 0));
|
|
567
|
+
const skillSection = buildSkillSection(opts.skills ?? []);
|
|
568
|
+
tui.addChild(header);
|
|
569
|
+
if (skillSection) {
|
|
570
|
+
tui.addChild(skillSection);
|
|
571
|
+
tui.addChild(new Spacer(1));
|
|
572
|
+
}
|
|
573
|
+
tui.addChild(messages);
|
|
574
|
+
tui.addChild(new Spacer(1));
|
|
575
|
+
tui.addChild(screenshot);
|
|
576
|
+
tui.addChild(new Spacer(1));
|
|
577
|
+
tui.addChild(editor);
|
|
578
|
+
tui.addChild(status);
|
|
579
|
+
tui.addChild(footer);
|
|
580
|
+
tui.setFocus(editor);
|
|
581
|
+
tui.onDebug = () => {
|
|
582
|
+
debug?.log("pi_tui_debug_key", {
|
|
583
|
+
columns: terminal.columns,
|
|
584
|
+
rows: terminal.rows,
|
|
585
|
+
fullRedraws: tui.fullRedraws
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
if (opts.resumed) {
|
|
589
|
+
const transcript = opts.transcriptPath ? ` ${opts.transcriptPath}` : "";
|
|
590
|
+
messages.addNotice(`resumed${transcript} · fresh browser`);
|
|
591
|
+
}
|
|
592
|
+
let assistantBuffer;
|
|
593
|
+
let inflight = 0;
|
|
594
|
+
let firstPromptSent = false;
|
|
595
|
+
let lastDisplayedError;
|
|
596
|
+
const displayAgentError = (error, reason) => {
|
|
597
|
+
if (typeof error !== "string" || error.trim().length === 0) return;
|
|
598
|
+
if (error === lastDisplayedError) return;
|
|
599
|
+
lastDisplayedError = error;
|
|
600
|
+
messages.addError(error);
|
|
601
|
+
status.update({ working: void 0 });
|
|
602
|
+
debug?.log("agent_error", {
|
|
603
|
+
reason,
|
|
604
|
+
message: error
|
|
605
|
+
});
|
|
606
|
+
requestRender("agent_error", false, { reason });
|
|
607
|
+
};
|
|
608
|
+
const unsubscribe = opts.harness.subscribe((event) => {
|
|
609
|
+
switch (event.type) {
|
|
610
|
+
case "agent_start":
|
|
611
|
+
inflight += 1;
|
|
612
|
+
status.update({ working: "thinking…" });
|
|
613
|
+
debug?.log("agent_start", { inflight });
|
|
614
|
+
requestRender("agent_start", false, { inflight });
|
|
615
|
+
return;
|
|
616
|
+
case "agent_end":
|
|
617
|
+
inflight -= 1;
|
|
618
|
+
if (inflight <= 0) status.update({ working: void 0 });
|
|
619
|
+
displayAgentError(lastErrorMessage(event.messages), "agent_end");
|
|
620
|
+
debug?.log("agent_end", { inflight });
|
|
621
|
+
requestRender("agent_end", false, { inflight });
|
|
622
|
+
return;
|
|
623
|
+
case "message_start":
|
|
624
|
+
if (event.message.role === "assistant") {
|
|
625
|
+
assistantBuffer = messages.addAssistantStart();
|
|
626
|
+
debug?.log("assistant_message_start");
|
|
627
|
+
requestRender("assistant_message_start");
|
|
628
|
+
}
|
|
629
|
+
return;
|
|
630
|
+
case "message_update":
|
|
631
|
+
if (event.assistantMessageEvent.type === "text_delta") {
|
|
632
|
+
assistantBuffer?.append(event.assistantMessageEvent.delta);
|
|
633
|
+
requestRender("assistant_text_delta", false, { deltaLength: event.assistantMessageEvent.delta.length });
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
case "message_end":
|
|
637
|
+
if (event.message.role === "assistant") {
|
|
638
|
+
if (event.message.usage) footer.update({ contextTokens: event.message.usage.input });
|
|
639
|
+
assistantBuffer?.end();
|
|
640
|
+
assistantBuffer = void 0;
|
|
641
|
+
displayAgentError(event.message.errorMessage, "assistant_message_end");
|
|
642
|
+
debug?.log("assistant_message_end");
|
|
643
|
+
requestRender("assistant_message_end");
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
case "tool_execution_start":
|
|
647
|
+
messages.addToolCall(event.toolName, event.args);
|
|
648
|
+
status.update({ working: event.toolName });
|
|
649
|
+
debug?.log("tool_execution_start", { toolName: event.toolName });
|
|
650
|
+
requestRender("tool_execution_start", false, { toolName: event.toolName });
|
|
651
|
+
return;
|
|
652
|
+
case "tool_execution_end": {
|
|
653
|
+
const result = event.result;
|
|
654
|
+
const isError = !!event.isError;
|
|
655
|
+
let summary = isError ? colors.red("error") : colors.green("ok");
|
|
656
|
+
if (!isError && result?.content) {
|
|
657
|
+
const imgs = result.content.filter((c) => c?.type === "image");
|
|
658
|
+
if (imgs.length > 0) summary += colors.dim(` · ${imgs.length} screenshot${imgs.length > 1 ? "s" : ""}`);
|
|
659
|
+
const lastImg = imgs[imgs.length - 1];
|
|
660
|
+
if (lastImg?.data) screenshot.update(lastImg.data, lastImg.mimeType ?? "image/png");
|
|
661
|
+
}
|
|
662
|
+
if (isError && result?.details?.error) summary = colors.red(result.details.error);
|
|
663
|
+
messages.addToolResult(event.toolName, !isError, summary);
|
|
664
|
+
debug?.log("tool_execution_end", {
|
|
665
|
+
toolName: event.toolName,
|
|
666
|
+
isError,
|
|
667
|
+
hasImage: !!result?.content?.some((c) => c?.type === "image")
|
|
668
|
+
});
|
|
669
|
+
requestRender("tool_execution_end", false, {
|
|
670
|
+
toolName: event.toolName,
|
|
671
|
+
isError
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
case "model_update":
|
|
676
|
+
footer.update({
|
|
677
|
+
provider: event.model.provider,
|
|
678
|
+
model: modelLabel(event.model),
|
|
679
|
+
contextWindow: event.model.contextWindow
|
|
680
|
+
});
|
|
681
|
+
status.update({ model: modelLabel(event.model) });
|
|
682
|
+
requestRender("model_update");
|
|
683
|
+
return;
|
|
684
|
+
case "thinking_level_update":
|
|
685
|
+
footer.update({ thinkingLevel: event.level });
|
|
686
|
+
requestRender("thinking_level_update");
|
|
687
|
+
return;
|
|
688
|
+
case "session_compact":
|
|
689
|
+
messages.addNotice(`compacted ${event.compactionEntry.tokensBefore} tokens`);
|
|
690
|
+
refreshContextTokens(opts.session).then((tokens) => {
|
|
691
|
+
footer.update({ contextTokens: tokens });
|
|
692
|
+
requestRender("session_compact");
|
|
693
|
+
});
|
|
694
|
+
return;
|
|
695
|
+
default: return;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
const pendingPrompt = opts.initialPrompt?.trim() || "";
|
|
699
|
+
let exitRequested = false;
|
|
700
|
+
const runPrompt = async (text) => {
|
|
701
|
+
debug?.log("run_prompt_start", { length: text.length });
|
|
702
|
+
try {
|
|
703
|
+
const parsed = parseSlashCommand(text);
|
|
704
|
+
if (parsed?.command === "model") {
|
|
705
|
+
await applyModelCommand(opts, footer, status, messages, parsed.argument);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (parsed?.command === "thinking") {
|
|
709
|
+
await applyThinkingCommand(opts, footer, messages, parsed.argument);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (parsed?.command === "compact") {
|
|
713
|
+
await applyCompactCommand(opts, messages);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (parsed?.command === "skill") {
|
|
717
|
+
const skill = (opts.skills ?? []).find((s) => s.name === parsed.name);
|
|
718
|
+
if (!skill) {
|
|
719
|
+
messages.addError(`unknown skill "${parsed.name}"`);
|
|
720
|
+
requestRender("skill_unknown");
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
messages.addNotice(`invoking /skill:${skill.name}`);
|
|
724
|
+
requestRender("skill_invocation");
|
|
725
|
+
const skillRemainder = parsed.remainder || void 0;
|
|
726
|
+
const skillImages = await maybeInitialScreenshot(opts, firstPromptSent);
|
|
727
|
+
firstPromptSent = true;
|
|
728
|
+
if (skillImages) await opts.harness.prompt(formatSkillInvocation(skill, skillRemainder), { images: skillImages });
|
|
729
|
+
else await opts.harness.skill(skill.name, skillRemainder);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const images = await maybeInitialScreenshot(opts, firstPromptSent);
|
|
733
|
+
firstPromptSent = true;
|
|
734
|
+
await opts.harness.prompt(text, images ? { images } : void 0);
|
|
735
|
+
} catch (err) {
|
|
736
|
+
messages.addError(err.message);
|
|
737
|
+
debug?.log("run_prompt_error", { message: err.message });
|
|
738
|
+
requestRender("run_prompt_error", false, { message: err.message });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
debug?.log("run_prompt_end");
|
|
742
|
+
};
|
|
743
|
+
editor.onSubmit = (text) => {
|
|
744
|
+
const trimmed = text.trim();
|
|
745
|
+
if (!trimmed) return;
|
|
746
|
+
editor.setText("");
|
|
747
|
+
editor.addToHistory(trimmed);
|
|
748
|
+
messages.addUser(trimmed);
|
|
749
|
+
debug?.log("editor_submit", { length: trimmed.length });
|
|
750
|
+
runPrompt(trimmed);
|
|
751
|
+
};
|
|
752
|
+
const removeListener = tui.addInputListener((data) => {
|
|
753
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
754
|
+
if (inflight > 0) {
|
|
755
|
+
opts.harness.abort();
|
|
756
|
+
messages.addNotice("aborted");
|
|
757
|
+
debug?.log("input_abort_stream", { key: "ctrl+c" });
|
|
758
|
+
requestRender("input_abort_stream", false, { key: "ctrl+c" });
|
|
759
|
+
return { consume: true };
|
|
760
|
+
}
|
|
761
|
+
exitRequested = true;
|
|
762
|
+
debug?.log("input_exit_request", { key: "ctrl+c" });
|
|
763
|
+
requestRender("input_exit_request", false, { key: "ctrl+c" });
|
|
764
|
+
return { consume: true };
|
|
765
|
+
}
|
|
766
|
+
if (matchesKey(data, "ctrl+d")) {
|
|
767
|
+
exitRequested = true;
|
|
768
|
+
debug?.log("input_exit_request", { key: "ctrl+d" });
|
|
769
|
+
return { consume: true };
|
|
770
|
+
}
|
|
771
|
+
if (matchesKey(data, "escape") && inflight > 0) {
|
|
772
|
+
opts.harness.abort();
|
|
773
|
+
messages.addNotice("turn aborted");
|
|
774
|
+
debug?.log("input_abort_stream", { key: "escape" });
|
|
775
|
+
requestRender("input_abort_stream", false, { key: "escape" });
|
|
776
|
+
return { consume: true };
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
tui.start();
|
|
780
|
+
debug?.log("tui_started", {
|
|
781
|
+
columns: terminal.columns,
|
|
782
|
+
rows: terminal.rows,
|
|
783
|
+
fullRedraws: tui.fullRedraws
|
|
784
|
+
});
|
|
785
|
+
try {
|
|
786
|
+
if (pendingPrompt) {
|
|
787
|
+
messages.addUser(pendingPrompt);
|
|
788
|
+
runPrompt(pendingPrompt);
|
|
789
|
+
}
|
|
790
|
+
await waitForExit(() => exitRequested, () => inflight > 0);
|
|
791
|
+
return 0;
|
|
792
|
+
} finally {
|
|
793
|
+
removeListener();
|
|
794
|
+
unsubscribe();
|
|
795
|
+
tui.stop();
|
|
796
|
+
debug?.close({
|
|
797
|
+
fullRedraws: tui.fullRedraws,
|
|
798
|
+
columns: terminal.columns,
|
|
799
|
+
rows: terminal.rows
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
async function waitForExit(shouldExit, isBusy) {
|
|
804
|
+
while (true) {
|
|
805
|
+
if (shouldExit() && !isBusy()) return;
|
|
806
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function modelLabel(model) {
|
|
810
|
+
if (!model) return "";
|
|
811
|
+
return model.id;
|
|
812
|
+
}
|
|
813
|
+
function lastErrorMessage(messages) {
|
|
814
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
815
|
+
const m = messages[i];
|
|
816
|
+
if (m && m.role === "assistant" && typeof m.errorMessage === "string") return m.errorMessage;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async function maybeInitialScreenshot(opts, firstPromptSent) {
|
|
820
|
+
if (firstPromptSent) return void 0;
|
|
821
|
+
if (opts.skipInitialScreenshot) return void 0;
|
|
822
|
+
if (await sessionHasPriorTurn(opts.session)) return void 0;
|
|
823
|
+
const png = await captureScreenshot(opts.browserHandle.client, opts.browserHandle.browser.session_id);
|
|
824
|
+
if (!png) return void 0;
|
|
825
|
+
return [{
|
|
826
|
+
type: "image",
|
|
827
|
+
data: png.toString("base64"),
|
|
828
|
+
mimeType: "image/png"
|
|
829
|
+
}];
|
|
830
|
+
}
|
|
831
|
+
async function sessionHasPriorTurn(session) {
|
|
832
|
+
const entries = await session.getBranch();
|
|
833
|
+
for (const entry of entries) if (entry.type === "message" && (entry.message.role === "user" || entry.message.role === "assistant")) return true;
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
async function applyModelCommand(opts, footer, status, messages, argument) {
|
|
837
|
+
const ref = argument.trim();
|
|
838
|
+
if (!ref) {
|
|
839
|
+
messages.addError("usage: /model <provider:model>");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
const resolved = resolveCuaModelRef(ref);
|
|
844
|
+
await opts.harness.setModel(resolved);
|
|
845
|
+
const model = opts.harness.getModel();
|
|
846
|
+
footer.update({
|
|
847
|
+
provider: model.provider,
|
|
848
|
+
model: modelLabel(model),
|
|
849
|
+
contextWindow: model.contextWindow
|
|
850
|
+
});
|
|
851
|
+
status.update({ model: modelLabel(model) });
|
|
852
|
+
messages.addNotice(`model → ${resolved}`);
|
|
853
|
+
} catch (err) {
|
|
854
|
+
messages.addError(err.message);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async function applyThinkingCommand(opts, footer, messages, argument) {
|
|
858
|
+
const value = argument.trim().toLowerCase();
|
|
859
|
+
if (!isThinkingLevel(value)) {
|
|
860
|
+
messages.addError("usage: /thinking <off|minimal|low|medium|high|xhigh>");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
await opts.harness.setThinkingLevel(value);
|
|
865
|
+
footer.update({ thinkingLevel: value });
|
|
866
|
+
messages.addNotice(`thinking → ${value}`);
|
|
867
|
+
} catch (err) {
|
|
868
|
+
messages.addError(err.message);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function isThinkingLevel(value) {
|
|
872
|
+
return [
|
|
873
|
+
"off",
|
|
874
|
+
"minimal",
|
|
875
|
+
"low",
|
|
876
|
+
"medium",
|
|
877
|
+
"high",
|
|
878
|
+
"xhigh"
|
|
879
|
+
].includes(value);
|
|
880
|
+
}
|
|
881
|
+
async function applyCompactCommand(opts, messages) {
|
|
882
|
+
messages.addNotice("compacting…");
|
|
883
|
+
try {
|
|
884
|
+
await opts.harness.compact();
|
|
885
|
+
} catch (err) {
|
|
886
|
+
messages.addError(err.message);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function refreshContextTokens(session) {
|
|
890
|
+
return estimateContextTokens((await session.buildContext()).messages).tokens;
|
|
891
|
+
}
|
|
892
|
+
function buildSkillSection(skills) {
|
|
893
|
+
if (skills.length === 0) return void 0;
|
|
894
|
+
const container = new Container();
|
|
895
|
+
container.addChild(new Text(colors.blue("[Skills]") + "\n" + skills.map((s) => s.name).join(", "), 0, 0));
|
|
896
|
+
return container;
|
|
897
|
+
}
|
|
898
|
+
//#endregion
|
|
899
|
+
export { runInteractive };
|