@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.
@@ -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 };