@cruxy/cli 0.11.0 → 0.13.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/dist/approval/prompt.js +17 -15
- package/dist/cli/commands/checkpoint.js +6 -4
- package/dist/cli/commands/config.js +10 -7
- package/dist/cli/commands/index.js +16 -15
- package/dist/cli/commands/init.js +5 -3
- package/dist/cli/commands/login.js +5 -3
- package/dist/cli/commands/pr.js +8 -7
- package/dist/cli/commands/rollback.js +7 -6
- package/dist/cli/commands/run.js +26 -7
- package/dist/cli/commands/skills.js +12 -10
- package/dist/cli/program.js +7 -6
- package/dist/cli/repl.js +11 -9
- package/dist/cli/session-factory.d.ts +2 -1
- package/dist/cli/session-factory.js +10 -3
- package/dist/components/frame.js +3 -1
- package/dist/components/fuzzy.d.ts +4 -4
- package/dist/components/fuzzy.js +14 -13
- package/dist/components/select.js +8 -7
- package/dist/config/schema.d.ts +123 -0
- package/dist/config/schema.js +40 -0
- package/dist/errors/constructors.d.ts +21 -0
- package/dist/errors/constructors.js +58 -0
- package/dist/errors/format.js +8 -8
- package/dist/errors/types.d.ts +5 -0
- package/dist/errors/types.js +11 -0
- package/dist/onboarding/flow.js +6 -6
- package/dist/onboarding/steps.js +11 -11
- package/dist/plan/approve.js +6 -6
- package/dist/plan/render.js +26 -18
- package/dist/render/capabilities.js +4 -0
- package/dist/render/diff.d.ts +6 -7
- package/dist/render/diff.js +33 -22
- package/dist/render/highlight.d.ts +3 -3
- package/dist/render/highlight.js +15 -15
- package/dist/render/index.d.ts +1 -1
- package/dist/render/plain-renderer.d.ts +2 -1
- package/dist/render/plain-renderer.js +7 -6
- package/dist/render/state.d.ts +7 -2
- package/dist/render/state.js +16 -10
- package/dist/render/tty-renderer.d.ts +2 -1
- package/dist/render/tty-renderer.js +20 -17
- package/dist/render/types.d.ts +7 -0
- package/dist/sandbox/detect.d.ts +22 -0
- package/dist/sandbox/detect.js +67 -0
- package/dist/sandbox/docker-runtime.d.ts +32 -0
- package/dist/sandbox/docker-runtime.js +263 -0
- package/dist/sandbox/index.d.ts +7 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/policy.d.ts +17 -0
- package/dist/sandbox/policy.js +90 -0
- package/dist/sandbox/service.d.ts +57 -0
- package/dist/sandbox/service.js +64 -0
- package/dist/sandbox/types.d.ts +114 -0
- package/dist/sandbox/types.js +17 -0
- package/dist/subagent/orchestrator.d.ts +7 -0
- package/dist/subagent/orchestrator.js +22 -6
- package/dist/testing/run-tests-tool.d.ts +5 -1
- package/dist/testing/run-tests-tool.js +8 -1
- package/dist/testing/sandbox-runner.d.ts +16 -0
- package/dist/testing/sandbox-runner.js +47 -0
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/resolve.d.ts +32 -0
- package/dist/theme/resolve.js +73 -0
- package/dist/theme/tokens.d.ts +104 -0
- package/dist/theme/tokens.js +52 -0
- package/dist/tools/shell/run-command.js +35 -1
- package/dist/tools/types.d.ts +10 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +7 -4
- package/package.json +1 -1
package/dist/onboarding/steps.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import { themeForColor } from "../theme/index.js";
|
|
4
4
|
import { CREATE_KEY_URL } from "../constants.js";
|
|
5
5
|
import { loadProjectInstructions } from "../config/index.js";
|
|
6
6
|
/**
|
|
@@ -9,7 +9,7 @@ import { loadProjectInstructions } from "../config/index.js";
|
|
|
9
9
|
* or logged. Rendering is gated on `io.color` so output respects NO_COLOR / pipes.
|
|
10
10
|
*/
|
|
11
11
|
const MAX_KEY_ATTEMPTS = 3;
|
|
12
|
-
const c = (io) =>
|
|
12
|
+
const c = (io) => themeForColor(io.color);
|
|
13
13
|
/**
|
|
14
14
|
* Acquire and persist a provider key: print the create-key URL, read it masked,
|
|
15
15
|
* validate it live, and **only then** write it to the credentials store. Loops on
|
|
@@ -17,23 +17,23 @@ const c = (io) => pc.createColors(io.color);
|
|
|
17
17
|
*/
|
|
18
18
|
export async function acquireKeyStep(io, deps, provider) {
|
|
19
19
|
const col = c(io);
|
|
20
|
-
io.write(`\nYou'll need a Cruxy API key. Create one at ${col.
|
|
20
|
+
io.write(`\nYou'll need a Cruxy API key. Create one at ${col.accent(CREATE_KEY_URL)}\n`);
|
|
21
21
|
for (let attempt = 1; attempt <= MAX_KEY_ATTEMPTS; attempt++) {
|
|
22
|
-
io.write(col.
|
|
22
|
+
io.write(col.strong("Paste your API key: "));
|
|
23
23
|
const key = (await io.readSecret()).trim();
|
|
24
24
|
if (key === "") {
|
|
25
25
|
// Empty / Ctrl-C / EOF — treat as an abort of the mandatory step.
|
|
26
26
|
return { status: "aborted" };
|
|
27
27
|
}
|
|
28
|
-
io.write(col.
|
|
28
|
+
io.write(col.muted("validating…\n"));
|
|
29
29
|
const outcome = await deps.validateKey(provider, key);
|
|
30
30
|
if (outcome === "valid") {
|
|
31
31
|
deps.writeCredential(provider, key);
|
|
32
|
-
io.write(`${col.
|
|
32
|
+
io.write(`${col.success(col.glyph.success)} key validated and saved to ~/.cruxy\n`);
|
|
33
33
|
return { status: "ok", apiKey: key };
|
|
34
34
|
}
|
|
35
35
|
if (outcome === "unreachable") {
|
|
36
|
-
io.write(`${col.
|
|
36
|
+
io.write(`${col.danger(col.glyph.failure)} couldn't reach the gateway to validate the key.\n`);
|
|
37
37
|
return {
|
|
38
38
|
status: "failed",
|
|
39
39
|
message: "network unreachable — try again with `cruxy login`",
|
|
@@ -41,7 +41,7 @@ export async function acquireKeyStep(io, deps, provider) {
|
|
|
41
41
|
}
|
|
42
42
|
// invalid
|
|
43
43
|
const left = MAX_KEY_ATTEMPTS - attempt;
|
|
44
|
-
io.write(`${col.
|
|
44
|
+
io.write(`${col.danger(col.glyph.failure)} that key was rejected${left > 0 ? ` (${left} ${left === 1 ? "try" : "tries"} left)` : ""}.\n`);
|
|
45
45
|
}
|
|
46
46
|
return { status: "failed", message: "key rejected after 3 attempts" };
|
|
47
47
|
}
|
|
@@ -71,14 +71,14 @@ export async function scaffoldStep(io, cwd) {
|
|
|
71
71
|
if (loadProjectInstructions(cwd) !== null) {
|
|
72
72
|
return { status: "skipped" };
|
|
73
73
|
}
|
|
74
|
-
io.write(`\nScaffold a ${col.
|
|
74
|
+
io.write(`\nScaffold a ${col.strong("CRUXY.md")} to guide cruxy in this project? ${col.muted("[y/N]")} `);
|
|
75
75
|
const key = (await io.readKey()).toLowerCase();
|
|
76
76
|
io.write("\n");
|
|
77
77
|
if (key !== "y")
|
|
78
78
|
return { status: "skipped" };
|
|
79
79
|
const file = join(cwd, "CRUXY.md");
|
|
80
80
|
writeFileSync(file, CRUXY_MD_TEMPLATE, "utf8");
|
|
81
|
-
io.write(`${col.
|
|
81
|
+
io.write(`${col.success(col.glyph.success)} wrote ${col.strong("CRUXY.md")}\n`);
|
|
82
82
|
return { status: "ok" };
|
|
83
83
|
}
|
|
84
84
|
const FIRST_WIN_PROMPT = "Give me a concise 3-sentence summary of what this repository does, based on its README and structure.";
|
|
@@ -90,7 +90,7 @@ export async function firstWinStep(io, deps) {
|
|
|
90
90
|
const col = c(io);
|
|
91
91
|
if (!deps.runTask)
|
|
92
92
|
return { status: "skipped" };
|
|
93
|
-
io.write(`\nRun a quick demo now — let cruxy summarize this repo? ${col.
|
|
93
|
+
io.write(`\nRun a quick demo now — let cruxy summarize this repo? ${col.muted("[Y/n]")} `);
|
|
94
94
|
const key = (await io.readKey()).toLowerCase();
|
|
95
95
|
io.write("\n");
|
|
96
96
|
if (key === "n")
|
package/dist/plan/approve.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { themeForColor } from "../theme/index.js";
|
|
2
2
|
import { renderPlan } from "./render.js";
|
|
3
3
|
/**
|
|
4
4
|
* The plan-approval prompt (C.31). Renders the plan and reads a single-key,
|
|
@@ -9,11 +9,11 @@ import { renderPlan } from "./render.js";
|
|
|
9
9
|
* Default-deny: EOF / Ctrl-C / any unrecognized key → `abort`.
|
|
10
10
|
*/
|
|
11
11
|
export async function promptPlanDecision(plan, io) {
|
|
12
|
-
const
|
|
12
|
+
const t = themeForColor(io.color);
|
|
13
13
|
io.write(renderPlan(plan, io.color));
|
|
14
14
|
io.write("\n\n");
|
|
15
|
-
io.write(` ${
|
|
16
|
-
io.write(` ${
|
|
15
|
+
io.write(` ${t.muted("Approving consents to the shape of the work — every action still asks before it runs.")}\n`);
|
|
16
|
+
io.write(` ${t.muted(`[a] approve ${t.glyph.sep}`)} [g] approve ${t.strong("+ allow the read/mutate steps")} this run ${t.muted("(destructive still confirms)")} ${t.muted(`${t.glyph.sep} [e] reject & revise`)} `);
|
|
17
17
|
const key = (await io.readKey()).toLowerCase();
|
|
18
18
|
io.write("\n");
|
|
19
19
|
switch (key) {
|
|
@@ -38,8 +38,8 @@ export async function promptPlanDecision(plan, io) {
|
|
|
38
38
|
* the run. Default-deny → abort (the safe choice: stop on failure).
|
|
39
39
|
*/
|
|
40
40
|
export async function promptContinueAfterFailure(io) {
|
|
41
|
-
const
|
|
42
|
-
io.write(` ${
|
|
41
|
+
const t = themeForColor(io.color);
|
|
42
|
+
io.write(` ${t.danger("step failed.")} ${t.muted(`[c] continue with the remaining steps ${t.glyph.sep} [any other key] abort`)} `);
|
|
43
43
|
const key = (await io.readKey()).toLowerCase();
|
|
44
44
|
io.write("\n");
|
|
45
45
|
return key === "c";
|
package/dist/plan/render.js
CHANGED
|
@@ -1,47 +1,55 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { themeForColor } from "../theme/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Plan rendering (C.31): data → string so it's testable and color is gated on a
|
|
4
|
+
* boolean (NO_COLOR / non-TTY aware, passed in by the caller). Used both for the
|
|
5
|
+
* approval prompt and for live status during execution. Glyphs and colors are
|
|
6
|
+
* sourced from the theme (U.1); status is carried by the glyph (○/◐/✓/✗) and
|
|
7
|
+
* risk by the `[read]/[mutate]/[destructive]` label, so meaning survives
|
|
8
|
+
* NO_COLOR.
|
|
9
|
+
*/
|
|
2
10
|
/** Status glyph, colored when enabled. */
|
|
3
|
-
function statusMark(status,
|
|
11
|
+
function statusMark(status, t) {
|
|
4
12
|
switch (status) {
|
|
5
13
|
case "pending":
|
|
6
|
-
return
|
|
14
|
+
return t.muted(t.glyph.pending);
|
|
7
15
|
case "running":
|
|
8
|
-
return
|
|
16
|
+
return t.accent(t.glyph.running);
|
|
9
17
|
case "done":
|
|
10
|
-
return
|
|
18
|
+
return t.success(t.glyph.success);
|
|
11
19
|
case "failed":
|
|
12
|
-
return
|
|
20
|
+
return t.danger(t.glyph.failure);
|
|
13
21
|
}
|
|
14
22
|
}
|
|
15
23
|
/** A short tag for the step's risk estimate. */
|
|
16
|
-
function kindTag(kind,
|
|
24
|
+
function kindTag(kind, t) {
|
|
17
25
|
switch (kind) {
|
|
18
26
|
case "read":
|
|
19
|
-
return
|
|
27
|
+
return t.muted("[read]");
|
|
20
28
|
case "mutate":
|
|
21
|
-
return
|
|
29
|
+
return t.warning("[mutate]");
|
|
22
30
|
case "destructive":
|
|
23
|
-
return
|
|
31
|
+
return t.danger(t.strong("[destructive]"));
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
|
-
function renderStep(step,
|
|
27
|
-
const head = ` ${statusMark(step.status,
|
|
34
|
+
function renderStep(step, t) {
|
|
35
|
+
const head = ` ${statusMark(step.status, t)} ${t.strong(step.id + ".")} ${step.title} ${kindTag(step.kind, t)}`;
|
|
28
36
|
const why = step.rationale.trim()
|
|
29
|
-
? `\n ${
|
|
37
|
+
? `\n ${t.muted(step.rationale.trim())}`
|
|
30
38
|
: "";
|
|
31
39
|
return head + why;
|
|
32
40
|
}
|
|
33
41
|
/** Render the whole plan as a block (header + one line per step + rationale). */
|
|
34
42
|
export function renderPlan(plan, color) {
|
|
35
|
-
const
|
|
43
|
+
const t = themeForColor(color);
|
|
36
44
|
const lines = [
|
|
37
|
-
|
|
45
|
+
t.heading(`Plan — ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"}:`),
|
|
38
46
|
];
|
|
39
47
|
for (const step of plan.steps)
|
|
40
|
-
lines.push(renderStep(step,
|
|
48
|
+
lines.push(renderStep(step, t));
|
|
41
49
|
return lines.join("\n");
|
|
42
50
|
}
|
|
43
51
|
/** A one-line status update for a single step (used during execution). */
|
|
44
52
|
export function renderStepStatus(step, color) {
|
|
45
|
-
const
|
|
46
|
-
return `${statusMark(step.status,
|
|
53
|
+
const t = themeForColor(color);
|
|
54
|
+
return `${statusMark(step.status, t)} ${t.strong(step.id + ".")} ${step.title}`;
|
|
47
55
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { shouldUseColor } from "../errors/index.js";
|
|
2
|
+
import { detectUnicode } from "../theme/index.js";
|
|
2
3
|
/**
|
|
3
4
|
* Probe the environment once and reduce it to {@link RenderCapabilities}. Pure
|
|
4
5
|
* given its inputs (stream + env are injectable), so every row of the
|
|
@@ -20,6 +21,9 @@ export function detectCapabilities(stream = process.stdout, env = process.env) {
|
|
|
20
21
|
// Same set-and-non-empty convention as NO_COLOR: any value disables.
|
|
21
22
|
spinner: cursor &&
|
|
22
23
|
!(env.CRUXY_NO_SPINNER !== undefined && env.CRUXY_NO_SPINNER !== ""),
|
|
24
|
+
// Unicode glyph safety (U.1) — independent of color. dumb / CRUXY_ASCII →
|
|
25
|
+
// ASCII glyphs; everything else (incl. pipes) keeps unicode.
|
|
26
|
+
unicode: detectUnicode(env),
|
|
23
27
|
width: typeof stream.columns === "number" && stream.columns > 0
|
|
24
28
|
? stream.columns
|
|
25
29
|
: 80,
|
package/dist/render/diff.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import pc from "picocolors";
|
|
2
1
|
import type { ActionPreview } from "../tools/types.js";
|
|
2
|
+
import type { Theme } from "../theme/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* The one diff/action-preview renderer (U.2). The approval prompt, PR preview,
|
|
5
5
|
* and the streaming render path all draw diffs through here — there is no
|
|
6
|
-
* second implementation to drift. Pure data → string; color is
|
|
7
|
-
*
|
|
8
|
-
* lines from the exact same code path.
|
|
6
|
+
* second implementation to drift. Pure data → string; color is sourced from
|
|
7
|
+
* the resolved {@link Theme} (U.1), so NO_COLOR/non-TTY callers get symbol-only
|
|
8
|
+
* `+`/`-` lines from the exact same code path. Diff add/remove intentionally
|
|
9
|
+
* reuse the `success`/`danger` hues (green/red), as before.
|
|
9
10
|
*/
|
|
10
|
-
/** The picocolors instance type (colorless or not), from createColors. */
|
|
11
|
-
export type Colors = ReturnType<typeof pc.createColors>;
|
|
12
11
|
/** Cap on rendered preview lines before collapsing the rest. */
|
|
13
12
|
export declare const PREVIEW_MAX_LINES = 40;
|
|
14
13
|
/**
|
|
@@ -16,4 +15,4 @@ export declare const PREVIEW_MAX_LINES = 40;
|
|
|
16
15
|
* patches, a create/overwrite listing for writes, the publish plan for PRs.
|
|
17
16
|
* Long previews collapse past {@link PREVIEW_MAX_LINES}.
|
|
18
17
|
*/
|
|
19
|
-
export declare function renderActionPreview(preview: ActionPreview | undefined, c:
|
|
18
|
+
export declare function renderActionPreview(preview: ActionPreview | undefined, c: Theme): string;
|
package/dist/render/diff.js
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one diff/action-preview renderer (U.2). The approval prompt, PR preview,
|
|
3
|
+
* and the streaming render path all draw diffs through here — there is no
|
|
4
|
+
* second implementation to drift. Pure data → string; color is sourced from
|
|
5
|
+
* the resolved {@link Theme} (U.1), so NO_COLOR/non-TTY callers get symbol-only
|
|
6
|
+
* `+`/`-` lines from the exact same code path. Diff add/remove intentionally
|
|
7
|
+
* reuse the `success`/`danger` hues (green/red), as before.
|
|
8
|
+
*/
|
|
1
9
|
/** Cap on rendered preview lines before collapsing the rest. */
|
|
2
10
|
export const PREVIEW_MAX_LINES = 40;
|
|
3
11
|
function diffLines(oldStr, newStr, c) {
|
|
4
|
-
const removed = oldStr.split("\n").map((l) => c.
|
|
5
|
-
const added = newStr.split("\n").map((l) => c.
|
|
12
|
+
const removed = oldStr.split("\n").map((l) => c.danger(`- ${l}`));
|
|
13
|
+
const added = newStr.split("\n").map((l) => c.success(`+ ${l}`));
|
|
6
14
|
return [...removed, ...added];
|
|
7
15
|
}
|
|
8
16
|
function renderPatchFiles(files, c) {
|
|
9
17
|
const out = [];
|
|
10
18
|
for (const file of files) {
|
|
11
19
|
if (file.op === "delete") {
|
|
12
|
-
out.push(c.
|
|
20
|
+
out.push(c.danger(`delete ${file.path}`));
|
|
13
21
|
}
|
|
14
22
|
else if (file.op === "create") {
|
|
15
|
-
out.push(c.
|
|
16
|
-
out.push(...file.lines.map((l) => c.
|
|
23
|
+
out.push(c.success(`create ${file.path}`));
|
|
24
|
+
out.push(...file.lines.map((l) => c.success(`+ ${l}`)));
|
|
17
25
|
if (file.omittedLines > 0)
|
|
18
|
-
out.push(c.
|
|
26
|
+
out.push(c.muted(` ...${file.omittedLines} more lines`));
|
|
19
27
|
}
|
|
20
28
|
else {
|
|
21
|
-
out.push(c.
|
|
29
|
+
out.push(c.warning(`update ${file.path}`));
|
|
22
30
|
for (const hunk of file.hunks)
|
|
23
31
|
out.push(...diffLines(hunk.oldStr, hunk.newStr, c));
|
|
24
32
|
}
|
|
@@ -28,16 +36,16 @@ function renderPatchFiles(files, c) {
|
|
|
28
36
|
/** Render a `vcs` pull-request publish plan: branch, commit, and PR body. */
|
|
29
37
|
function renderPrPreview(preview, c) {
|
|
30
38
|
const out = [];
|
|
31
|
-
out.push(`${c.
|
|
39
|
+
out.push(`${c.strong("branch")} ${c.success(preview.branch)} ${c.glyph.arrow} ${preview.base}`);
|
|
32
40
|
out.push("");
|
|
33
|
-
out.push(c.
|
|
41
|
+
out.push(c.strong("commit"));
|
|
34
42
|
out.push(` ${preview.commitSubject}`);
|
|
35
43
|
for (const line of bodyLines(preview.commitBody))
|
|
36
|
-
out.push(c.
|
|
44
|
+
out.push(c.muted(` ${line}`));
|
|
37
45
|
out.push("");
|
|
38
|
-
out.push(`${c.
|
|
46
|
+
out.push(`${c.strong("pull request")} ${preview.prTitle}`);
|
|
39
47
|
for (const line of bodyLines(preview.prBody))
|
|
40
|
-
out.push(c.
|
|
48
|
+
out.push(c.muted(` ${line}`));
|
|
41
49
|
return out;
|
|
42
50
|
}
|
|
43
51
|
/**
|
|
@@ -48,17 +56,17 @@ function renderPrPreview(preview, c) {
|
|
|
48
56
|
*/
|
|
49
57
|
function renderRollbackPreview(preview, c) {
|
|
50
58
|
const out = [];
|
|
51
|
-
out.push(`${c.
|
|
59
|
+
out.push(`${c.strong("restore checkpoint")} ${c.accent(preview.checkpointId)} ${c.muted(`(${preview.createdAt})`)}`);
|
|
52
60
|
if (preview.runSummary)
|
|
53
|
-
out.push(c.
|
|
54
|
-
out.push(c.
|
|
61
|
+
out.push(c.muted(`run: ${preview.runSummary}`));
|
|
62
|
+
out.push(c.muted("working-tree files only — commits, pushes, and PRs made during the run are not undone"));
|
|
55
63
|
if (preview.externalPaths.length > 0) {
|
|
56
|
-
out.push(c.
|
|
64
|
+
out.push(c.danger(c.strong("changed outside this run — rollback will overwrite these too:")));
|
|
57
65
|
for (const p of preview.externalPaths)
|
|
58
|
-
out.push(c.
|
|
66
|
+
out.push(c.danger(`! ${p}`));
|
|
59
67
|
}
|
|
60
68
|
if (preview.attributionUnknown) {
|
|
61
|
-
out.push(c.
|
|
69
|
+
out.push(c.warning("this run executed shell commands; some changes below may not have been made by the run"));
|
|
62
70
|
}
|
|
63
71
|
out.push("");
|
|
64
72
|
out.push(...renderPatchFiles(preview.files, c));
|
|
@@ -92,16 +100,19 @@ export function renderActionPreview(preview, c) {
|
|
|
92
100
|
}
|
|
93
101
|
else {
|
|
94
102
|
const header = preview.exists
|
|
95
|
-
? c.
|
|
96
|
-
: c.
|
|
103
|
+
? c.warning("OVERWRITE existing")
|
|
104
|
+
: c.success("create");
|
|
97
105
|
const body = preview.lines.map((l) => ` ${l}`);
|
|
98
106
|
if (preview.omittedLines > 0)
|
|
99
|
-
body.push(c.
|
|
107
|
+
body.push(c.muted(` ...${preview.omittedLines} more lines`));
|
|
100
108
|
lines = [header, ...body];
|
|
101
109
|
}
|
|
102
110
|
if (lines.length > PREVIEW_MAX_LINES) {
|
|
103
111
|
const hidden = lines.length - PREVIEW_MAX_LINES;
|
|
104
|
-
lines = [
|
|
112
|
+
lines = [
|
|
113
|
+
...lines.slice(0, PREVIEW_MAX_LINES),
|
|
114
|
+
c.muted(`...${hidden} more`),
|
|
115
|
+
];
|
|
105
116
|
}
|
|
106
117
|
return lines.map((l) => ` ${l}`).join("\n");
|
|
107
118
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Theme } from "../theme/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Best-effort, bounded syntax highlighting for fenced code blocks in streamed
|
|
4
4
|
* markdown (U.2). Priorities, in order: never crash the stream, never hold the
|
|
@@ -38,10 +38,10 @@ export interface StreamHighlighter {
|
|
|
38
38
|
* Create the per-segment streaming highlighter. `highlightLine` is injectable
|
|
39
39
|
* for tests (e.g. to prove a throwing tokenizer degrades to plain text).
|
|
40
40
|
*/
|
|
41
|
-
export declare function createStreamHighlighter(
|
|
41
|
+
export declare function createStreamHighlighter(theme: Theme, highlightLine?: LineHighlighter): StreamHighlighter;
|
|
42
42
|
/**
|
|
43
43
|
* Build the default per-line tokenizer over `c`. A plain left-to-right scan:
|
|
44
44
|
* comments dim, strings green, keywords magenta, numbers yellow, everything
|
|
45
45
|
* else untouched. Unknown language → identity.
|
|
46
46
|
*/
|
|
47
|
-
export declare function defaultLineHighlighter(
|
|
47
|
+
export declare function defaultLineHighlighter(theme: Theme): LineHighlighter;
|
package/dist/render/highlight.js
CHANGED
|
@@ -7,7 +7,7 @@ const FENCE_PLAUSIBLE = /^(?:`{1,2}|`{3,}[\w+#.-]*\s*)$/;
|
|
|
7
7
|
* Create the per-segment streaming highlighter. `highlightLine` is injectable
|
|
8
8
|
* for tests (e.g. to prove a throwing tokenizer degrades to plain text).
|
|
9
9
|
*/
|
|
10
|
-
export function createStreamHighlighter(
|
|
10
|
+
export function createStreamHighlighter(theme, highlightLine = defaultLineHighlighter(theme)) {
|
|
11
11
|
let mode = "prose";
|
|
12
12
|
let atLineStart = true;
|
|
13
13
|
let lineBuf = "";
|
|
@@ -45,7 +45,7 @@ export function createStreamHighlighter(c, highlightLine = defaultLineHighlighte
|
|
|
45
45
|
lang = m[2] ? m[2].toLowerCase() : null;
|
|
46
46
|
carry = FRESH_CARRY;
|
|
47
47
|
mode = "code";
|
|
48
|
-
out +=
|
|
48
|
+
out += theme.muted(lineBuf) + "\n";
|
|
49
49
|
}
|
|
50
50
|
else {
|
|
51
51
|
mode = "prose";
|
|
@@ -70,7 +70,7 @@ export function createStreamHighlighter(c, highlightLine = defaultLineHighlighte
|
|
|
70
70
|
if (ch === "\n") {
|
|
71
71
|
if (/^`{3,}\s*$/.test(lineBuf)) {
|
|
72
72
|
mode = "prose";
|
|
73
|
-
out +=
|
|
73
|
+
out += theme.muted(lineBuf) + "\n";
|
|
74
74
|
}
|
|
75
75
|
else {
|
|
76
76
|
out += styleCodeLine(lineBuf) + "\n";
|
|
@@ -165,7 +165,7 @@ const WORD = /[A-Za-z0-9_$]/;
|
|
|
165
165
|
* comments dim, strings green, keywords magenta, numbers yellow, everything
|
|
166
166
|
* else untouched. Unknown language → identity.
|
|
167
167
|
*/
|
|
168
|
-
export function defaultLineHighlighter(
|
|
168
|
+
export function defaultLineHighlighter(theme) {
|
|
169
169
|
return (line, lang, carry) => {
|
|
170
170
|
const def = lang ? LANGS[lang] : undefined;
|
|
171
171
|
if (!def)
|
|
@@ -179,37 +179,37 @@ export function defaultLineHighlighter(c) {
|
|
|
179
179
|
if (next.blockComment && def.blockComment) {
|
|
180
180
|
const close = line.indexOf(def.blockComment[1]);
|
|
181
181
|
if (close === -1)
|
|
182
|
-
return { text:
|
|
182
|
+
return { text: theme.syntax.comment(line), carry: next };
|
|
183
183
|
const end = close + def.blockComment[1].length;
|
|
184
|
-
out +=
|
|
184
|
+
out += theme.syntax.comment(line.slice(0, end));
|
|
185
185
|
i = end;
|
|
186
186
|
next.blockComment = false;
|
|
187
187
|
}
|
|
188
188
|
else if (next.stringDelim) {
|
|
189
189
|
const close = findStringEnd(line, 0, next.stringDelim);
|
|
190
190
|
if (close === -1)
|
|
191
|
-
return { text:
|
|
192
|
-
out +=
|
|
191
|
+
return { text: theme.syntax.string(line), carry: next };
|
|
192
|
+
out += theme.syntax.string(line.slice(0, close));
|
|
193
193
|
i = close;
|
|
194
194
|
next.stringDelim = null;
|
|
195
195
|
}
|
|
196
196
|
while (i < line.length) {
|
|
197
197
|
const rest = line.slice(i);
|
|
198
198
|
if (def.lineComment && rest.startsWith(def.lineComment)) {
|
|
199
|
-
out +=
|
|
199
|
+
out += theme.syntax.comment(rest);
|
|
200
200
|
i = line.length;
|
|
201
201
|
break;
|
|
202
202
|
}
|
|
203
203
|
if (def.blockComment && rest.startsWith(def.blockComment[0])) {
|
|
204
204
|
const close = line.indexOf(def.blockComment[1], i + def.blockComment[0].length);
|
|
205
205
|
if (close === -1) {
|
|
206
|
-
out +=
|
|
206
|
+
out += theme.syntax.comment(rest);
|
|
207
207
|
next = { ...next, blockComment: true };
|
|
208
208
|
i = line.length;
|
|
209
209
|
break;
|
|
210
210
|
}
|
|
211
211
|
const end = close + def.blockComment[1].length;
|
|
212
|
-
out +=
|
|
212
|
+
out += theme.syntax.comment(line.slice(i, end));
|
|
213
213
|
i = end;
|
|
214
214
|
continue;
|
|
215
215
|
}
|
|
@@ -217,13 +217,13 @@ export function defaultLineHighlighter(c) {
|
|
|
217
217
|
if (quote) {
|
|
218
218
|
const close = findStringEnd(line, i + quote.length, quote);
|
|
219
219
|
if (close === -1) {
|
|
220
|
-
out +=
|
|
220
|
+
out += theme.syntax.string(rest);
|
|
221
221
|
if (def.multiline.includes(quote))
|
|
222
222
|
next = { ...next, stringDelim: quote };
|
|
223
223
|
i = line.length;
|
|
224
224
|
break;
|
|
225
225
|
}
|
|
226
|
-
out +=
|
|
226
|
+
out += theme.syntax.string(line.slice(i, close));
|
|
227
227
|
i = close;
|
|
228
228
|
continue;
|
|
229
229
|
}
|
|
@@ -234,9 +234,9 @@ export function defaultLineHighlighter(c) {
|
|
|
234
234
|
j++;
|
|
235
235
|
const word = line.slice(i, j);
|
|
236
236
|
if (def.keywords.has(word))
|
|
237
|
-
out +=
|
|
237
|
+
out += theme.syntax.keyword(word);
|
|
238
238
|
else if (/^\d/.test(word))
|
|
239
|
-
out +=
|
|
239
|
+
out += theme.syntax.number(word);
|
|
240
240
|
else
|
|
241
241
|
out += word;
|
|
242
242
|
i = j;
|
package/dist/render/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { RenderStream, StreamRenderer } from "./types.js";
|
|
|
2
2
|
export type { ProgressState, RenderCapabilities, RenderPhase, RenderStream, StreamRenderer, TokenUsage, ToolLifecycleEvent, } from "./types.js";
|
|
3
3
|
export { detectCapabilities } from "./capabilities.js";
|
|
4
4
|
export { composeStatusLine, describePhase, ELAPSED_AFTER_MS, formatElapsed, formatTokens, phaseIdentity, } from "./state.js";
|
|
5
|
-
export { renderActionPreview, PREVIEW_MAX_LINES
|
|
5
|
+
export { renderActionPreview, PREVIEW_MAX_LINES } from "./diff.js";
|
|
6
6
|
export { createStreamHighlighter, defaultLineHighlighter, type HighlightCarry, type LineHighlighter, type StreamHighlighter, } from "./highlight.js";
|
|
7
7
|
export { PlainRenderer } from "./plain-renderer.js";
|
|
8
8
|
export { TtyRenderer } from "./tty-renderer.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionPreview } from "../tools/types.js";
|
|
2
|
+
import { type Theme } from "../theme/index.js";
|
|
2
3
|
import type { RenderCapabilities, RenderStream, StreamRenderer, ToolLifecycleEvent } from "./types.js";
|
|
3
4
|
/**
|
|
4
5
|
* The append-only renderer for pipes, CI, and cursor-less terminals. Emits no
|
|
@@ -15,7 +16,7 @@ export declare class PlainRenderer implements StreamRenderer {
|
|
|
15
16
|
readonly caps: RenderCapabilities;
|
|
16
17
|
private readonly out;
|
|
17
18
|
private readonly err;
|
|
18
|
-
|
|
19
|
+
readonly theme: Theme;
|
|
19
20
|
/** Per-turn leading-newline trim; also tells endSegment whether to newline. */
|
|
20
21
|
private print;
|
|
21
22
|
private wroteInSegment;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { resolveTheme } from "../theme/index.js";
|
|
2
2
|
import { createStreamPrinter } from "../cli/stream-print.js";
|
|
3
3
|
import { renderActionPreview } from "./diff.js";
|
|
4
4
|
import { ELAPSED_AFTER_MS, formatElapsed } from "./state.js";
|
|
@@ -17,7 +17,7 @@ export class PlainRenderer {
|
|
|
17
17
|
caps;
|
|
18
18
|
out;
|
|
19
19
|
err;
|
|
20
|
-
|
|
20
|
+
theme;
|
|
21
21
|
/** Per-turn leading-newline trim; also tells endSegment whether to newline. */
|
|
22
22
|
print;
|
|
23
23
|
wroteInSegment = false;
|
|
@@ -27,7 +27,7 @@ export class PlainRenderer {
|
|
|
27
27
|
this.caps = caps;
|
|
28
28
|
this.out = out;
|
|
29
29
|
this.err = err;
|
|
30
|
-
this.
|
|
30
|
+
this.theme = resolveTheme(caps);
|
|
31
31
|
this.print = this.newPrinter();
|
|
32
32
|
}
|
|
33
33
|
newPrinter() {
|
|
@@ -49,10 +49,10 @@ export class PlainRenderer {
|
|
|
49
49
|
this.wroteInSegment = false;
|
|
50
50
|
}
|
|
51
51
|
note(text) {
|
|
52
|
-
this.err.write(this.
|
|
52
|
+
this.err.write(this.theme.muted(text) + "\n");
|
|
53
53
|
}
|
|
54
54
|
preview(preview) {
|
|
55
|
-
const block = renderActionPreview(preview, this.
|
|
55
|
+
const block = renderActionPreview(preview, this.theme);
|
|
56
56
|
if (block)
|
|
57
57
|
this.out.write(block + "\n");
|
|
58
58
|
}
|
|
@@ -77,7 +77,8 @@ export class PlainRenderer {
|
|
|
77
77
|
this.toolStart = null;
|
|
78
78
|
const elapsed = started === null ? 0 : Date.now() - started.at;
|
|
79
79
|
const suffix = elapsed >= ELAPSED_AFTER_MS ? ` (${formatElapsed(elapsed)})` : "";
|
|
80
|
-
|
|
80
|
+
const mark = event.ok ? this.theme.glyph.success : this.theme.glyph.failure;
|
|
81
|
+
this.note(`${mark} ${event.label}${suffix}`);
|
|
81
82
|
}
|
|
82
83
|
promptResolved() {
|
|
83
84
|
// No live region to restore.
|
package/dist/render/state.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { type ThemeGlyphs } from "../theme/index.js";
|
|
1
2
|
import type { ProgressState, RenderPhase } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* The U.4 state→text mapping: pure data → string, like plan/render.ts and
|
|
4
5
|
* diff.ts, so both renderers (and tests) share one composition with no
|
|
5
6
|
* terminal in sight. Color is deliberately absent — the live line is drawn
|
|
6
7
|
* dim as a whole by the TTY renderer; state text is content, not chrome.
|
|
8
|
+
*
|
|
9
|
+
* Glyphs (ellipsis, token arrows, the ` · ` joiner) come from the theme (U.1);
|
|
10
|
+
* they default to the unicode table so colorless/test callers are unchanged,
|
|
11
|
+
* and the TTY renderer passes its capability-resolved set for ASCII fallback.
|
|
7
12
|
*/
|
|
8
13
|
/**
|
|
9
14
|
* Threshold before elapsed time appears on a live state or a committed tool
|
|
@@ -16,7 +21,7 @@ export declare function formatTokens(n: number): string;
|
|
|
16
21
|
/** `37s`, `2m08s` — durations at status-line width. */
|
|
17
22
|
export declare function formatElapsed(ms: number): string;
|
|
18
23
|
/** The live-line text for a phase. `awaiting-approval` never renders (the line hides). */
|
|
19
|
-
export declare function describePhase(phase: RenderPhase): string;
|
|
24
|
+
export declare function describePhase(phase: RenderPhase, glyph?: ThemeGlyphs): string;
|
|
20
25
|
/**
|
|
21
26
|
* Identity key for the elapsed clock: the clock resets when the phase becomes
|
|
22
27
|
* a *different activity*, not on every payload update — a thinking phase that
|
|
@@ -28,4 +33,4 @@ export declare function phaseIdentity(phase: RenderPhase | null): string;
|
|
|
28
33
|
* Elapsed appears only past {@link ELAPSED_AFTER_MS} — callers pass it only
|
|
29
34
|
* when they can keep it ticking honestly (no timer → no frozen number).
|
|
30
35
|
*/
|
|
31
|
-
export declare function composeStatusLine(progress: ProgressState | null, phase: RenderPhase | null, elapsedMs?: number): string;
|
|
36
|
+
export declare function composeStatusLine(progress: ProgressState | null, phase: RenderPhase | null, elapsedMs?: number, glyph?: ThemeGlyphs): string;
|
package/dist/render/state.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { UNICODE_GLYPHS } from "../theme/index.js";
|
|
1
2
|
/**
|
|
2
3
|
* The U.4 state→text mapping: pure data → string, like plan/render.ts and
|
|
3
4
|
* diff.ts, so both renderers (and tests) share one composition with no
|
|
4
5
|
* terminal in sight. Color is deliberately absent — the live line is drawn
|
|
5
6
|
* dim as a whole by the TTY renderer; state text is content, not chrome.
|
|
7
|
+
*
|
|
8
|
+
* Glyphs (ellipsis, token arrows, the ` · ` joiner) come from the theme (U.1);
|
|
9
|
+
* they default to the unicode table so colorless/test callers are unchanged,
|
|
10
|
+
* and the TTY renderer passes its capability-resolved set for ASCII fallback.
|
|
6
11
|
*/
|
|
7
12
|
/**
|
|
8
13
|
* Threshold before elapsed time appears on a live state or a committed tool
|
|
@@ -28,23 +33,24 @@ export function formatElapsed(ms) {
|
|
|
28
33
|
return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
29
34
|
}
|
|
30
35
|
/** The live-line text for a phase. `awaiting-approval` never renders (the line hides). */
|
|
31
|
-
export function describePhase(phase) {
|
|
36
|
+
export function describePhase(phase, glyph = UNICODE_GLYPHS) {
|
|
37
|
+
const e = glyph.ellipsis;
|
|
32
38
|
switch (phase.kind) {
|
|
33
39
|
case "thinking": {
|
|
34
40
|
const t = phase.tokens;
|
|
35
41
|
// Honest numbers only: no usage yet → no figure at all.
|
|
36
42
|
return t && t.input + t.output > 0
|
|
37
|
-
? `thinking
|
|
38
|
-
:
|
|
43
|
+
? `thinking${e} ${glyph.sep} tokens ${glyph.caretUp}${formatTokens(t.input)} ${glyph.caretDown}${formatTokens(t.output)}`
|
|
44
|
+
: `thinking${e}`;
|
|
39
45
|
}
|
|
40
46
|
case "calling-tool":
|
|
41
|
-
return `${phase.label}
|
|
47
|
+
return `${phase.label}${e}`;
|
|
42
48
|
case "awaiting-approval":
|
|
43
|
-
return
|
|
49
|
+
return `awaiting approval${e}`;
|
|
44
50
|
case "executing-step":
|
|
45
|
-
return
|
|
51
|
+
return `working${e}`;
|
|
46
52
|
case "subagent":
|
|
47
|
-
return `subagent: ${phase.label}
|
|
53
|
+
return `subagent: ${phase.label}${e}`;
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
/**
|
|
@@ -69,13 +75,13 @@ export function phaseIdentity(phase) {
|
|
|
69
75
|
* Elapsed appears only past {@link ELAPSED_AFTER_MS} — callers pass it only
|
|
70
76
|
* when they can keep it ticking honestly (no timer → no frozen number).
|
|
71
77
|
*/
|
|
72
|
-
export function composeStatusLine(progress, phase, elapsedMs) {
|
|
78
|
+
export function composeStatusLine(progress, phase, elapsedMs, glyph = UNICODE_GLYPHS) {
|
|
73
79
|
const parts = [];
|
|
74
80
|
if (progress)
|
|
75
81
|
parts.push(`[${progress.step}/${progress.of}] ${progress.title}`);
|
|
76
82
|
if (phase)
|
|
77
|
-
parts.push(describePhase(phase));
|
|
78
|
-
const line = parts.join(
|
|
83
|
+
parts.push(describePhase(phase, glyph));
|
|
84
|
+
const line = parts.join(` ${glyph.sep} `);
|
|
79
85
|
if (elapsedMs !== undefined && elapsedMs >= ELAPSED_AFTER_MS) {
|
|
80
86
|
return `${line} (${formatElapsed(elapsedMs)})`;
|
|
81
87
|
}
|