@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 +29 -17
- package/dist/cli.js +89 -32
- package/dist/{main-Bphx_zOj.js → main-BJ1jOxe3.js} +89 -57
- package/package.json +1 -1
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
|
|
10
|
+
## Install
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
#
|
|
14
|
-
npm install
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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`
|
|
141
|
-
|
|
142
|
-
|
|
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 (
|
|
366
|
-
|
|
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
|
|
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
|
-
*
|
|
649
|
-
*
|
|
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
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
696
|
+
contextFiles,
|
|
668
697
|
diagnostics: []
|
|
669
698
|
};
|
|
670
|
-
const
|
|
699
|
+
const loaded = await loadSkills(opts.env, skillDirs);
|
|
671
700
|
return {
|
|
672
|
-
skills:
|
|
673
|
-
|
|
674
|
-
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-
|
|
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
|
|
1514
|
-
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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.
|
|
209
|
+
this.appendBlock([colors.accent("· ") + colors.dim(name) + " " + summary]);
|
|
214
210
|
}
|
|
215
211
|
addToolResult(name, ok, summary) {
|
|
216
|
-
const icon = ok ? colors.
|
|
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.
|
|
216
|
+
this.appendBlock([colors.warning("· ") + colors.dim(text)]);
|
|
221
217
|
}
|
|
222
218
|
addError(text) {
|
|
223
|
-
this.appendBlock([colors.
|
|
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.
|
|
237
|
-
this.body = new Markdown("", 0, 0,
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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 };
|