@onkernel/cua-cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,22 +7,19 @@ interactive front-end and to
7
7
  [`pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)'s
8
8
  coding tools for workspace access.
9
9
 
10
- ## Install (from the monorepo)
10
+ ## Install
11
11
 
12
12
  ```bash
13
- # from the repo root:
14
- npm install
15
- # run directly from source via tsx (no global install required):
16
- npx tsx packages/cli/src/cli.ts --help
17
-
18
- # optional: pin a shell function in your rc so `cua` works from any cwd
19
- # while preserving the caller's directory (so `--out`, transcript
20
- # bucketing, and `.agents/skills` discovery use the directory you
21
- # invoked from):
22
- # CUA_REPO=/absolute/path/to/cua
23
- # cua() { "$CUA_REPO/node_modules/.bin/tsx" "$CUA_REPO/packages/cli/src/cli.ts" "$@"; }
13
+ # global install (puts `cua` on your PATH):
14
+ npm install -g @onkernel/cua-cli
15
+ cua --help
16
+
17
+ # or run a one-off without installing:
18
+ npx @onkernel/cua-cli --help
24
19
  ```
25
20
 
21
+ Requires Node >= 22.19.0.
22
+
26
23
  ## Usage
27
24
 
28
25
  ```bash
@@ -135,14 +132,19 @@ For named sessions, the exact transcript path is in
135
132
  [Session transcripts section in the top-level README](../../README.md#session-transcripts)
136
133
  for the JSONL schema and `jq` analysis examples.
137
134
 
138
- ## Skills
135
+ ## Skills and context
139
136
 
140
- `cua` follows the cross-agent
141
- [`~/.agents/skills/`](https://agentskills.io) standard. Discovery
142
- defaults:
137
+ `cua` resolves skills and context files through pi's resource loader
138
+ (the same loader pi's own TUI uses), so the discovery set matches pi.
139
+ Skills load from:
143
140
 
144
- - `~/.agents/skills/` (user-global)
141
+ - `~/.agents/skills/` (user-global, the cross-agent
142
+ [`~/.agents/skills/`](https://agentskills.io) standard)
145
143
  - `<cwd>/.agents/skills/` (project-local)
144
+ - the pi agent dir (`~/.pi/agent/`)
145
+ - pi-installed packages (`pi install …` records the package in pi's
146
+ settings and clones it under the agent dir; its bundled skills load
147
+ here too)
146
148
 
147
149
  Plus any explicit `--skill <path>` flags. Disable with `--no-skills`
148
150
  (`-ns`).
@@ -152,6 +154,16 @@ the system prompt; the model uses the `read` tool to load a skill's
152
154
  full body when its description matches the task. Use `/skill:<name>`
153
155
  in a prompt to force-load a skill body inline.
154
156
 
157
+ Context files (`AGENTS.md` / `CLAUDE.md`) discovered by the resource
158
+ loader are appended to the system prompt and listed in the TUI's
159
+ `[Context]` section. `--no-skills` disables skill discovery only;
160
+ context files still load, since they describe the project rather than
161
+ add agent capabilities.
162
+
163
+ pi *extensions* are not executed by `cua`: extensions bind into pi's
164
+ `AgentSession`, and `cua` drives the lower-level `AgentHarness`
165
+ directly. Installed-package skills and context still load.
166
+
155
167
  ## Image protocol
156
168
 
157
169
  Force the inline-screenshot protocol with `--image-protocol` or
package/dist/cli.js CHANGED
@@ -5,10 +5,9 @@ import { parseArgs } from "node:util";
5
5
  import { CuaAgentHarness, InMemorySessionRepo, JsonlSessionRepo, NodeExecutionEnv, formatSkillsForSystemPrompt, loadSkills } from "@onkernel/cua-agent";
6
6
  import { getCuaEnvApiKey, getCuaModel, parseCuaModelRef, requireCuaEnvApiKey, resolveCuaRuntimeSpec } from "@onkernel/cua-ai";
7
7
  import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
8
- import { createCodingTools } from "@earendil-works/pi-coding-agent";
8
+ import { DefaultResourceLoader, SettingsManager, createCodingTools, getAgentDir } from "@earendil-works/pi-coding-agent";
9
9
  import { homedir } from "node:os";
10
- import { isAbsolute, join, resolve } from "node:path";
11
- import { existsSync } from "node:fs";
10
+ import { dirname, isAbsolute, join, resolve } from "node:path";
12
11
  //#region src/action/prompts.ts
13
12
  function buildPrompt(req) {
14
13
  switch (req.action) {
@@ -337,6 +336,7 @@ function emitCompact(res) {
337
336
  */
338
337
  function buildCuaHarness(opts) {
339
338
  const skills = opts.skills ?? [];
339
+ const contextFiles = opts.contextFiles ?? [];
340
340
  const extraTools = opts.extraTools ?? createCodingTools(opts.cwd);
341
341
  const model = opts.modelBaseUrl ? {
342
342
  ...getCuaModel(opts.model),
@@ -352,7 +352,7 @@ function buildCuaHarness(opts) {
352
352
  resources: { skills },
353
353
  thinkingLevel: opts.thinkingLevel,
354
354
  systemPrompt: ({ model: activeModel, resources }) => {
355
- return composeSystemPrompt(resolveCuaRuntimeSpec(activeModel).defaultSystemPrompt, resources.skills ?? []);
355
+ return composeSystemPrompt(resolveCuaRuntimeSpec(activeModel).defaultSystemPrompt, resources.skills ?? [], contextFiles);
356
356
  },
357
357
  getApiKeyAndHeaders: opts.getApiKeyAndHeaders ?? (async (resolvedModel) => {
358
358
  const apiKey = getCuaEnvApiKey(resolvedModel.provider);
@@ -360,10 +360,18 @@ function buildCuaHarness(opts) {
360
360
  })
361
361
  });
362
362
  }
363
- function composeSystemPrompt(base, skills) {
363
+ function composeSystemPrompt(base, skills, contextFiles) {
364
+ const sections = [base.trim()];
364
365
  const skillBlock = formatSkillsForSystemPrompt(skills).trim();
365
- if (!skillBlock) return base;
366
- return `${base.trim()}\n\n${skillBlock}\n`;
366
+ if (skillBlock) sections.push(skillBlock);
367
+ const contextBlock = formatContextFiles(contextFiles);
368
+ if (contextBlock) sections.push(contextBlock);
369
+ return `${sections.join("\n\n")}\n`;
370
+ }
371
+ function formatContextFiles(contextFiles) {
372
+ const blocks = contextFiles.filter((file) => file.content.trim().length > 0).map((file) => `## ${file.path}\n\n${file.content.trim()}`);
373
+ if (blocks.length === 0) return "";
374
+ return `# Context\n\n${blocks.join("\n\n")}`;
367
375
  }
368
376
  //#endregion
369
377
  //#region src/harness-named-sessions.ts
@@ -643,35 +651,79 @@ async function appendBrowserEntry(session, data) {
643
651
  //#endregion
644
652
  //#region src/harness-skills.ts
645
653
  /**
646
- * Discover skills following the cross-agent `~/.agents/skills/` standard.
654
+ * Discover skills and context files via pi's `DefaultResourceLoader`, the same
655
+ * loader pi's own TUI uses. This resolves skills from installed pi packages
656
+ * (`pi install …` writes them under the agent dir and records them in
657
+ * settings.json) in addition to `~/.agents/skills/`, `<cwd>/.agents/skills/`,
658
+ * `~/.pi/agent/skills/`, and explicit `--skill` paths.
659
+ *
660
+ * Startup never blocks on an interactive prompt: project settings start
661
+ * untrusted (no trust prompt), and `PI_OFFLINE` keeps a configured-but-not-
662
+ * installed package from triggering a network install — it is skipped instead.
647
663
  *
648
- * Discovery order: explicit `--skill` paths, then `~/.agents/skills/`,
649
- * then `<cwd>/.agents/skills/`. Missing paths are skipped silently.
664
+ * pi extensions are not loaded (`noExtensions`): cua's harness drives the
665
+ * lower-level `AgentHarness` directly and cannot bind pi `AgentSession`
666
+ * extensions.
650
667
  */
651
668
  async function discoverCuaSkills(opts) {
652
- if (opts.disabled) return {
653
- skills: [],
654
- sources: [],
655
- diagnostics: []
656
- };
657
669
  const extras = (opts.extraPaths ?? []).filter((p) => p && p.trim().length > 0);
658
- const userAgentsDir = join(homedir(), ".agents", "skills");
659
- const projectAgentsDir = join(opts.cwd, ".agents", "skills");
660
- const sources = [
661
- ...extras,
662
- userAgentsDir,
663
- projectAgentsDir
664
- ].filter((p) => existsSync(p));
665
- if (sources.length === 0) return {
670
+ const agentDir = opts.agentDir ?? getAgentDir();
671
+ const settingsManager = SettingsManager.create(opts.cwd, agentDir, { projectTrusted: false });
672
+ const projectSkillDir = join(opts.cwd, ".agents", "skills");
673
+ const additionalSkillPaths = [...extras, projectSkillDir];
674
+ const loader = new DefaultResourceLoader({
675
+ cwd: opts.cwd,
676
+ agentDir,
677
+ settingsManager,
678
+ additionalSkillPaths,
679
+ noSkills: opts.disabled === true,
680
+ noExtensions: true,
681
+ noPromptTemplates: true,
682
+ noThemes: true
683
+ });
684
+ const restoreOffline = forceOfflinePackageResolution();
685
+ try {
686
+ await loader.reload();
687
+ } finally {
688
+ restoreOffline();
689
+ }
690
+ const piSkills = loader.getSkills().skills;
691
+ const contextFiles = loader.getAgentsFiles().agentsFiles;
692
+ const discoveredPaths = new Set(piSkills.map((s) => s.filePath));
693
+ const skillDirs = [...new Set(piSkills.map((s) => dirname(s.filePath)))];
694
+ if (skillDirs.length === 0) return {
666
695
  skills: [],
667
- sources: [],
696
+ contextFiles,
668
697
  diagnostics: []
669
698
  };
670
- const result = await loadSkills(opts.env, sources);
699
+ const loaded = await loadSkills(opts.env, skillDirs);
671
700
  return {
672
- skills: result.skills,
673
- sources,
674
- diagnostics: result.diagnostics
701
+ skills: dedupeByFilePath(loaded.skills.filter((s) => discoveredPaths.has(s.filePath))),
702
+ contextFiles,
703
+ diagnostics: loaded.diagnostics
704
+ };
705
+ }
706
+ function dedupeByFilePath(skills) {
707
+ const seen = /* @__PURE__ */ new Set();
708
+ const result = [];
709
+ for (const skill of skills) {
710
+ if (seen.has(skill.filePath)) continue;
711
+ seen.add(skill.filePath);
712
+ result.push(skill);
713
+ }
714
+ return result;
715
+ }
716
+ /**
717
+ * `DefaultResourceLoader.reload()` resolves packages without an `onMissing`
718
+ * callback, which would auto-install a configured-but-missing package over the
719
+ * network. `PI_OFFLINE` makes that resolution skip missing packages instead, so
720
+ * startup can never hang on an install. Restores any prior value afterward.
721
+ */
722
+ function forceOfflinePackageResolution() {
723
+ if (process.env.PI_OFFLINE !== void 0) return () => {};
724
+ process.env.PI_OFFLINE = "1";
725
+ return () => {
726
+ delete process.env.PI_OFFLINE;
675
727
  };
676
728
  }
677
729
  /**
@@ -1162,7 +1214,7 @@ async function pickSession(sessions) {
1162
1214
  async function setupHarnessRuntime(flags, opts = {}) {
1163
1215
  const auth = resolveAuth(flags);
1164
1216
  const cwd = process.cwd();
1165
- const { skills } = await discoverCuaSkills({
1217
+ const { skills, contextFiles } = await discoverCuaSkills({
1166
1218
  cwd,
1167
1219
  env: new NodeExecutionEnv({ cwd }),
1168
1220
  extraPaths: flags.skillPaths,
@@ -1197,6 +1249,7 @@ async function setupHarnessRuntime(flags, opts = {}) {
1197
1249
  session,
1198
1250
  model: auth.modelRef,
1199
1251
  skills,
1252
+ contextFiles,
1200
1253
  thinkingLevel,
1201
1254
  modelBaseUrl: baseUrlOverride
1202
1255
  });
@@ -1205,6 +1258,7 @@ async function setupHarnessRuntime(flags, opts = {}) {
1205
1258
  resolved,
1206
1259
  session,
1207
1260
  skills,
1261
+ contextFiles,
1208
1262
  harness,
1209
1263
  provider,
1210
1264
  modelRef: auth.modelRef
@@ -1261,7 +1315,7 @@ async function runPrintCommand(prompt, flags) {
1261
1315
  /** Run the interactive TUI through the new harness wiring. */
1262
1316
  async function runInteractiveCommand(initialPrompt, flags) {
1263
1317
  const runtime = await setupHarnessRuntime(flags);
1264
- const { runInteractive } = await import("./main-Bphx_zOj.js");
1318
+ const { runInteractive } = await import("./main-BJ1jOxe3.js");
1265
1319
  try {
1266
1320
  return await runInteractive({
1267
1321
  cwd: process.cwd(),
@@ -1269,6 +1323,7 @@ async function runInteractiveCommand(initialPrompt, flags) {
1269
1323
  browserHandle: runtime.handle,
1270
1324
  session: runtime.session,
1271
1325
  skills: runtime.skills,
1326
+ contextFiles: runtime.contextFiles,
1272
1327
  modelRef: runtime.modelRef,
1273
1328
  provider: runtime.provider,
1274
1329
  initialPrompt: initialPrompt || void 0,
@@ -1510,8 +1565,10 @@ Options:
1510
1565
  --session <ref> Resume a specific session: path | partial id | latest
1511
1566
  --session-dir <dir> Override the sessions directory
1512
1567
  --no-session Don't persist this session to disk
1513
- --skill <path> Load a skill file or directory (repeatable).
1514
- Defaults: ~/.agents/skills/, <cwd>/.agents/skills/
1568
+ --skill <path> Load an extra skill file or directory (repeatable).
1569
+ Skills also load from ~/.agents/skills/,
1570
+ <cwd>/.agents/skills/, the pi agent dir
1571
+ (~/.pi/agent/), and pi-installed packages.
1515
1572
  -ns, --no-skills Disable skill discovery entirely
1516
1573
  --debug-tui Enable TUI render diagnostics for manual repros
1517
1574
  -v, --verbose Verbose progress output to stderr
@@ -2,10 +2,11 @@ import { i as captureScreenshot, r as resolveCuaModelRef } from "./harness-model
2
2
  import { stderr } from "node:process";
3
3
  import { estimateContextTokens, formatSkillInvocation } from "@onkernel/cua-agent";
4
4
  import { listCuaModels } from "@onkernel/cua-ai";
5
- import os from "node:os";
5
+ import { Theme, getMarkdownTheme, getSelectListTheme, initTheme } from "@earendil-works/pi-coding-agent";
6
+ import os, { homedir } from "node:os";
6
7
  import path from "node:path";
7
- import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
8
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
+ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
9
10
  //#region src/tui/debug-log.ts
10
11
  const PI_RENDER_DIR = "/tmp/tui";
11
12
  const PI_REDRAW_LOG = path.join(os.homedir(), ".pi", "agent", "pi-debug.log");
@@ -147,50 +148,45 @@ function applyAndSummarizeImageProtocol(flag) {
147
148
  }
148
149
  //#endregion
149
150
  //#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)
151
+ /**
152
+ * cua's TUI styling rides on pi's theme system so it matches pi's own TUI.
153
+ * `initTheme()` must run once at TUI startup (see `tui/main.ts`) before any of
154
+ * these helpers are used.
155
+ *
156
+ * pi exports the `Theme` class and the markdown/select-list theme getters, but
157
+ * not the live theme instance behind `theme.fg(...)`. That instance is published
158
+ * on a `Symbol.for` global key (pi's cross-realm contract for its own `theme`
159
+ * proxy), so we read it back here to colorize text with the active palette.
160
+ */
161
+ const THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme");
162
+ function activeTheme() {
163
+ const instance = globalThis[THEME_KEY];
164
+ if (!(instance instanceof Theme)) throw new Error("pi theme not initialized; call initTheme() before rendering the TUI");
165
+ return instance;
166
+ }
167
+ /**
168
+ * The small palette cua's components reach for, mapped onto pi theme colors so
169
+ * existing call sites keep working while picking up pi's palette.
170
+ */
171
+ const colors = {
172
+ dim: (text) => activeTheme().fg("dim", text),
173
+ bold: (text) => activeTheme().bold(text),
174
+ accent: (text) => activeTheme().fg("accent", text),
175
+ muted: (text) => activeTheme().fg("muted", text),
176
+ heading: (text) => activeTheme().fg("mdHeading", text),
177
+ success: (text) => activeTheme().fg("success", text),
178
+ error: (text) => activeTheme().fg("error", text),
179
+ warning: (text) => activeTheme().fg("warning", text)
193
180
  };
181
+ /** pi has no exported editor theme; compose one from its select-list theme. */
182
+ function getEditorTheme() {
183
+ return {
184
+ borderColor: (text) => activeTheme().fg("borderAccent", text),
185
+ selectList: getSelectListTheme()
186
+ };
187
+ }
188
+ /** pi has no image theme; only the text fallback color is cua-specific. */
189
+ const imageTheme = { fallbackColor: (text) => activeTheme().fg("dim", text) };
194
190
  //#endregion
195
191
  //#region src/tui/message-list.ts
196
192
  /**
@@ -210,17 +206,17 @@ var MessageList = class extends Container {
210
206
  }
211
207
  addToolCall(name, args) {
212
208
  const summary = formatToolCall(name, args);
213
- this.appendBlock([colors.cyan("· ") + colors.dim(name) + " " + summary]);
209
+ this.appendBlock([colors.accent("· ") + colors.dim(name) + " " + summary]);
214
210
  }
215
211
  addToolResult(name, ok, summary) {
216
- const icon = ok ? colors.green("✓") : colors.red("✗");
212
+ const icon = ok ? colors.success("✓") : colors.error("✗");
217
213
  this.appendBlock([` ${icon} ${colors.dim(name)} ${summary}`]);
218
214
  }
219
215
  addNotice(text) {
220
- this.appendBlock([colors.yellow("· ") + colors.dim(text)]);
216
+ this.appendBlock([colors.warning("· ") + colors.dim(text)]);
221
217
  }
222
218
  addError(text) {
223
- this.appendBlock([colors.red("error ") + text]);
219
+ this.appendBlock([colors.error("error ") + text]);
224
220
  }
225
221
  appendBlock(lines) {
226
222
  for (const line of lines) this.addChild(new Text(line, 0, 0));
@@ -233,8 +229,8 @@ var AssistantBuffer = class extends Container {
233
229
  body;
234
230
  constructor() {
235
231
  super();
236
- this.addChild(new Text(colors.green("assistant"), 0, 0));
237
- this.body = new Markdown("", 0, 0, markdownTheme);
232
+ this.addChild(new Text(colors.success("assistant"), 0, 0));
233
+ this.body = new Markdown("", 0, 0, getMarkdownTheme());
238
234
  this.addChild(this.body);
239
235
  }
240
236
  append(delta) {
@@ -444,7 +440,7 @@ var StatusLine = class extends Text {
444
440
  if (this.state.currentUrl) parts.push(colors.dim("url ") + truncate(this.state.currentUrl, 50));
445
441
  if (this.state.tokens !== void 0) parts.push(colors.dim("tokens ") + this.state.tokens.toLocaleString());
446
442
  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}`));
443
+ if (this.state.working) parts.push(colors.warning(`⏳ ${this.state.working}`));
448
444
  this.setText(parts.join(sep));
449
445
  }
450
446
  };
@@ -506,6 +502,11 @@ function padToWidth(text, width) {
506
502
  return text + " ".repeat(pad);
507
503
  }
508
504
  //#endregion
505
+ //#region src/tui/version.ts
506
+ function cuaVersion() {
507
+ return "0.1.2";
508
+ }
509
+ //#endregion
509
510
  //#region src/tui/main.ts
510
511
  /**
511
512
  * Run the interactive cua TUI: pi-tui differential renderer with header,
@@ -514,6 +515,7 @@ function padToWidth(text, width) {
514
515
  * directly via `harness.subscribe()`.
515
516
  */
516
517
  async function runInteractive(opts) {
518
+ initTheme();
517
519
  const { summary: capsSummary, overridden } = applyAndSummarizeImageProtocol(opts.imageProtocol);
518
520
  const debug = opts.debugTui ? openTuiDebugLog() : void 0;
519
521
  const initialModel = opts.harness.getModel();
@@ -541,7 +543,7 @@ async function runInteractive(opts) {
541
543
  tui.requestRender(force);
542
544
  };
543
545
  new KeybindingsManager(TUI_KEYBINDINGS);
544
- const editor = new Editor(tui, editorTheme);
546
+ const editor = new Editor(tui, getEditorTheme());
545
547
  editor.setAutocompleteProvider(buildAutocompleteProvider(opts.cwd, opts.skills ?? []));
546
548
  const messages = new MessageList();
547
549
  const screenshot = new ScreenshotWidget();
@@ -559,13 +561,20 @@ async function runInteractive(opts) {
559
561
  contextTokens: 0
560
562
  });
561
563
  const header = new Container();
562
- header.addChild(new Text(colors.bold("cua") + colors.dim(" — kernel-cloud-browser computer-use agent"), 0, 0));
564
+ const logo = colors.bold(colors.accent("cua")) + colors.dim(` v${cuaVersion()}`);
565
+ header.addChild(new Text(logo, 0, 0));
566
+ header.addChild(new Text(keyHintRow(), 0, 0));
563
567
  const capsHint = overridden ? colors.dim(capsSummary) : colors.dim(capsSummary + " · set CUA_IMAGE_PROTOCOL=kitty|iterm2 to force inline images");
564
568
  header.addChild(new Text(capsHint, 0, 0));
565
569
  if (liveUrl) header.addChild(new Text(colors.dim("live ") + hyperlink(liveUrl, liveUrl), 0, 0));
566
570
  header.addChild(new Text("", 0, 0));
571
+ const contextSection = buildContextSection(opts.contextFiles ?? []);
567
572
  const skillSection = buildSkillSection(opts.skills ?? []);
568
573
  tui.addChild(header);
574
+ if (contextSection) {
575
+ tui.addChild(contextSection);
576
+ tui.addChild(new Spacer(1));
577
+ }
569
578
  if (skillSection) {
570
579
  tui.addChild(skillSection);
571
580
  tui.addChild(new Spacer(1));
@@ -652,14 +661,14 @@ async function runInteractive(opts) {
652
661
  case "tool_execution_end": {
653
662
  const result = event.result;
654
663
  const isError = !!event.isError;
655
- let summary = isError ? colors.red("error") : colors.green("ok");
664
+ let summary = isError ? colors.error("error") : colors.success("ok");
656
665
  if (!isError && result?.content) {
657
666
  const imgs = result.content.filter((c) => c?.type === "image");
658
667
  if (imgs.length > 0) summary += colors.dim(` · ${imgs.length} screenshot${imgs.length > 1 ? "s" : ""}`);
659
668
  const lastImg = imgs[imgs.length - 1];
660
669
  if (lastImg?.data) screenshot.update(lastImg.data, lastImg.mimeType ?? "image/png");
661
670
  }
662
- if (isError && result?.details?.error) summary = colors.red(result.details.error);
671
+ if (isError && result?.details?.error) summary = colors.error(result.details.error);
663
672
  messages.addToolResult(event.toolName, !isError, summary);
664
673
  debug?.log("tool_execution_end", {
665
674
  toolName: event.toolName,
@@ -889,11 +898,34 @@ async function applyCompactCommand(opts, messages) {
889
898
  async function refreshContextTokens(session) {
890
899
  return estimateContextTokens((await session.buildContext()).messages).tokens;
891
900
  }
901
+ function keyHintRow() {
902
+ const hint = (keys, label) => colors.bold(keys) + colors.dim(` ${label}`);
903
+ return [
904
+ hint("esc/ctrl+c", "to interrupt"),
905
+ hint("ctrl+c/ctrl+d", "to exit"),
906
+ hint("/", "for commands")
907
+ ].join(colors.muted(" · "));
908
+ }
909
+ function sectionLabel(name) {
910
+ return colors.heading(`[${name}]`);
911
+ }
912
+ function buildContextSection(contextFiles) {
913
+ if (contextFiles.length === 0) return void 0;
914
+ const paths = contextFiles.map((file) => displayPath(file.path)).join(", ");
915
+ const container = new Container();
916
+ container.addChild(new Text(sectionLabel("Context") + "\n" + colors.dim(` ${paths}`), 0, 0));
917
+ return container;
918
+ }
892
919
  function buildSkillSection(skills) {
893
920
  if (skills.length === 0) return void 0;
921
+ const names = skills.map((s) => s.name).sort((a, b) => a.localeCompare(b)).join(", ");
894
922
  const container = new Container();
895
- container.addChild(new Text(colors.blue("[Skills]") + "\n" + skills.map((s) => s.name).join(", "), 0, 0));
923
+ container.addChild(new Text(sectionLabel("Skills") + "\n" + colors.dim(` ${names}`), 0, 0));
896
924
  return container;
897
925
  }
926
+ function displayPath(path) {
927
+ const home = homedir();
928
+ return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
929
+ }
898
930
  //#endregion
899
931
  export { runInteractive };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onkernel/cua-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Kernel-cloud-browser computer-use TUI built on @onkernel/cua-agent and pi-tui",
5
5
  "license": "MIT",
6
6
  "type": "module",