@cruxy/cli 0.13.0 → 0.14.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 -3
- package/dist/components/fuzzy.js +7 -1
- package/dist/onboarding/steps.js +4 -1
- package/dist/render/capabilities.d.ts +10 -2
- package/dist/render/capabilities.js +26 -6
- package/dist/render/index.d.ts +9 -5
- package/dist/render/index.js +12 -5
- package/dist/render/plain-renderer.d.ts +3 -3
- package/dist/render/plain-renderer.js +10 -2
- package/dist/render/screen-reader-renderer.d.ts +45 -0
- package/dist/render/screen-reader-renderer.js +75 -0
- package/dist/render/types.d.ts +15 -1
- package/dist/theme/resolve.d.ts +18 -7
- package/dist/theme/resolve.js +32 -10
- package/dist/theme/tokens.d.ts +16 -1
- package/dist/theme/tokens.js +27 -0
- package/package.json +1 -1
package/dist/approval/prompt.js
CHANGED
|
@@ -37,16 +37,30 @@ export async function promptForApproval(request, io) {
|
|
|
37
37
|
export function render(request, color) {
|
|
38
38
|
const t = themeForColor(color);
|
|
39
39
|
const destructive = request.tier === "destructive";
|
|
40
|
-
//
|
|
41
|
-
//
|
|
40
|
+
// Risk survives all three degradations (U.11): the mark carries it by *shape*
|
|
41
|
+
// (`!` vs `?`) for NO_COLOR, and the label carries it by *word*
|
|
42
|
+
// (`(destructive)` / `(mutate)` / `(read)`) — never by hue alone, and legible
|
|
43
|
+
// to a screen reader that would otherwise read `!` as "exclamation mark".
|
|
42
44
|
const mark = destructive ? t.danger(t.strong("!")) : t.warning("?");
|
|
43
|
-
const label =
|
|
45
|
+
const label = tierLabel(request.tier, t);
|
|
44
46
|
const lines = [];
|
|
45
47
|
lines.push(`${mark} cruxy wants to ${t.strong(request.summary)}${label}`);
|
|
46
48
|
lines.push(detail(request, t));
|
|
47
49
|
lines.push(choices(request.scope, t));
|
|
48
50
|
return lines.filter((l) => l !== "").join("\n") + " ";
|
|
49
51
|
}
|
|
52
|
+
/** The worded risk tag, colored by tier — always present, so meaning never
|
|
53
|
+
* rides on the `!`/`?` shape or its color alone. */
|
|
54
|
+
function tierLabel(tier, t) {
|
|
55
|
+
switch (tier) {
|
|
56
|
+
case "destructive":
|
|
57
|
+
return t.danger(t.strong(" (destructive)"));
|
|
58
|
+
case "mutate":
|
|
59
|
+
return t.warning(" (mutate)");
|
|
60
|
+
case "read":
|
|
61
|
+
return t.muted(" (read)");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
50
64
|
/** The action detail: a diff for file actions, the command + cwd for shell/test. */
|
|
51
65
|
function detail(request, t) {
|
|
52
66
|
if (request.action.kind === "shell" || request.action.kind === "test") {
|
package/dist/components/fuzzy.js
CHANGED
|
@@ -106,7 +106,9 @@ export async function fuzzyFind(items, opts, io = defaultComponentIO()) {
|
|
|
106
106
|
lines.push(t.heading(opts.title));
|
|
107
107
|
lines.push(`${t.accent(g.caret)} ${query}${t.muted(g.cursorBar)}`);
|
|
108
108
|
if (ranked.length === 0) {
|
|
109
|
-
|
|
109
|
+
// No dead-end: the only escapes (backspace, esc) stay advertised even
|
|
110
|
+
// when nothing matches — the user is never stranded with no visible exit.
|
|
111
|
+
lines.push(t.muted(" no results — backspace to widen · esc cancel"));
|
|
110
112
|
}
|
|
111
113
|
else {
|
|
112
114
|
// Keep the highlighted row inside the viewport.
|
|
@@ -122,6 +124,10 @@ export async function fuzzyFind(items, opts, io = defaultComponentIO()) {
|
|
|
122
124
|
if (hidden > 0)
|
|
123
125
|
lines.push(t.muted(` ${g.ellipsis} ${hidden} more`));
|
|
124
126
|
}
|
|
127
|
+
// Persistent key hint (U.11 keyboard-completeness): every operable key is
|
|
128
|
+
// advertised, mirroring selectList — type filters, arrows move, enter
|
|
129
|
+
// selects, esc cancels. Present in both the results and no-results states.
|
|
130
|
+
lines.push(t.muted(` type to filter ${g.sep} ${g.caretUp}/${g.caretDown} move ${g.sep} enter select ${g.sep} esc cancel`));
|
|
125
131
|
frame.render(lines);
|
|
126
132
|
};
|
|
127
133
|
io.keys.begin();
|
package/dist/onboarding/steps.js
CHANGED
|
@@ -93,7 +93,10 @@ export async function firstWinStep(io, deps) {
|
|
|
93
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
|
+
// Decline on an explicit `n`, and treat Ctrl-C / EOF / escape (readKey → "")
|
|
97
|
+
// as a clean cancel rather than "proceed": a cancel must never launch a task.
|
|
98
|
+
// Enter (readKey → "\n") keeps the `[Y/n]` default and runs the demo.
|
|
99
|
+
if (key === "n" || key === "")
|
|
97
100
|
return { status: "skipped" };
|
|
98
101
|
await deps.runTask(FIRST_WIN_PROMPT);
|
|
99
102
|
return { status: "ok" };
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { RenderCapabilities, RenderStream } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reduced-motion (U.11) — the ecosystem `NO_MOTION` signal, the explicit cruxy
|
|
4
|
+
* knob `CRUXY_REDUCED_MOTION`, and `CRUXY_NO_SPINNER` kept as an alias flowing
|
|
5
|
+
* through this one axis. A screen reader implies it (no live region to animate).
|
|
6
|
+
* The single source of the spinner gate.
|
|
7
|
+
*/
|
|
8
|
+
export declare function detectReducedMotion(env?: NodeJS.ProcessEnv): boolean;
|
|
2
9
|
/**
|
|
3
10
|
* Probe the environment once and reduce it to {@link RenderCapabilities}. Pure
|
|
4
11
|
* given its inputs (stream + env are injectable), so every row of the
|
|
@@ -6,7 +13,8 @@ import type { RenderCapabilities, RenderStream } from "./types.js";
|
|
|
6
13
|
*
|
|
7
14
|
* Color reuses {@link shouldUseColor} (NO_COLOR / FORCE_COLOR / TTY) with one
|
|
8
15
|
* refinement: `TERM=dumb` terminals get no color even though they are TTYs.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
16
|
+
* The axes are independent (U.1/U.11): a NO_COLOR terminal still supports
|
|
17
|
+
* in-place status updates; a dumb terminal supports neither; reduced motion and
|
|
18
|
+
* screen-reader mode compose orthogonally with color and unicode.
|
|
11
19
|
*/
|
|
12
20
|
export declare function detectCapabilities(stream?: RenderStream, env?: NodeJS.ProcessEnv): RenderCapabilities;
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { shouldUseColor } from "../errors/index.js";
|
|
2
|
-
import { detectUnicode } from "../theme/index.js";
|
|
2
|
+
import { detectScreenReader, detectUnicode } from "../theme/index.js";
|
|
3
|
+
/** Set-and-non-empty (the NO_COLOR convention): any non-empty value counts. */
|
|
4
|
+
function isSet(value) {
|
|
5
|
+
return value !== undefined && value !== "";
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Reduced-motion (U.11) — the ecosystem `NO_MOTION` signal, the explicit cruxy
|
|
9
|
+
* knob `CRUXY_REDUCED_MOTION`, and `CRUXY_NO_SPINNER` kept as an alias flowing
|
|
10
|
+
* through this one axis. A screen reader implies it (no live region to animate).
|
|
11
|
+
* The single source of the spinner gate.
|
|
12
|
+
*/
|
|
13
|
+
export function detectReducedMotion(env = process.env) {
|
|
14
|
+
return (isSet(env.NO_MOTION) ||
|
|
15
|
+
isSet(env.CRUXY_REDUCED_MOTION) ||
|
|
16
|
+
isSet(env.CRUXY_NO_SPINNER) ||
|
|
17
|
+
detectScreenReader(env));
|
|
18
|
+
}
|
|
3
19
|
/**
|
|
4
20
|
* Probe the environment once and reduce it to {@link RenderCapabilities}. Pure
|
|
5
21
|
* given its inputs (stream + env are injectable), so every row of the
|
|
@@ -7,20 +23,24 @@ import { detectUnicode } from "../theme/index.js";
|
|
|
7
23
|
*
|
|
8
24
|
* Color reuses {@link shouldUseColor} (NO_COLOR / FORCE_COLOR / TTY) with one
|
|
9
25
|
* refinement: `TERM=dumb` terminals get no color even though they are TTYs.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
26
|
+
* The axes are independent (U.1/U.11): a NO_COLOR terminal still supports
|
|
27
|
+
* in-place status updates; a dumb terminal supports neither; reduced motion and
|
|
28
|
+
* screen-reader mode compose orthogonally with color and unicode.
|
|
12
29
|
*/
|
|
13
30
|
export function detectCapabilities(stream = process.stdout, env = process.env) {
|
|
14
31
|
const tty = Boolean(stream.isTTY);
|
|
15
32
|
const dumb = env.TERM === "dumb";
|
|
16
33
|
const cursor = tty && !dumb;
|
|
34
|
+
const reducedMotion = detectReducedMotion(env);
|
|
17
35
|
return {
|
|
18
36
|
tty,
|
|
19
37
|
color: shouldUseColor(stream, env) && !dumb,
|
|
20
38
|
cursor,
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
// Motion is the single gate now: CRUXY_NO_SPINNER flows through it (alias),
|
|
40
|
+
// as do NO_MOTION / CRUXY_REDUCED_MOTION and an implied screen reader.
|
|
41
|
+
spinner: cursor && !reducedMotion,
|
|
42
|
+
reducedMotion,
|
|
43
|
+
screenReader: detectScreenReader(env),
|
|
24
44
|
// Unicode glyph safety (U.1) — independent of color. dumb / CRUXY_ASCII →
|
|
25
45
|
// ASCII glyphs; everything else (incl. pipes) keeps unicode.
|
|
26
46
|
unicode: detectUnicode(env),
|
package/dist/render/index.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { RenderStream, StreamRenderer } from "./types.js";
|
|
2
2
|
export type { ProgressState, RenderCapabilities, RenderPhase, RenderStream, StreamRenderer, TokenUsage, ToolLifecycleEvent, } from "./types.js";
|
|
3
|
-
export { detectCapabilities } from "./capabilities.js";
|
|
3
|
+
export { detectCapabilities, detectReducedMotion } from "./capabilities.js";
|
|
4
4
|
export { composeStatusLine, describePhase, ELAPSED_AFTER_MS, formatElapsed, formatTokens, phaseIdentity, } from "./state.js";
|
|
5
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
|
+
export { ScreenReaderRenderer } from "./screen-reader-renderer.js";
|
|
8
9
|
export { TtyRenderer } from "./tty-renderer.js";
|
|
9
10
|
/**
|
|
10
|
-
* Build the renderer for the detected environment
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Build the renderer for the detected environment (U.11 adds the first branch):
|
|
12
|
+
* - `screenReader` → the linear, worded {@link ScreenReaderRenderer}, regardless
|
|
13
|
+
* of cursor support (a screen-reader TTY must not get the live region);
|
|
14
|
+
* - else cursor-safe → the managed-live-region {@link TtyRenderer} (static, no
|
|
15
|
+
* timer, when `reducedMotion`);
|
|
16
|
+
* - else → the append-only {@link PlainRenderer} (pipes, CI, `TERM=dumb`).
|
|
17
|
+
* Everything downstream talks to {@link StreamRenderer} and never re-probes.
|
|
14
18
|
*/
|
|
15
19
|
export declare function createRenderer(out?: RenderStream, err?: RenderStream, env?: NodeJS.ProcessEnv): StreamRenderer;
|
package/dist/render/index.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { detectCapabilities } from "./capabilities.js";
|
|
2
2
|
import { PlainRenderer } from "./plain-renderer.js";
|
|
3
|
+
import { ScreenReaderRenderer } from "./screen-reader-renderer.js";
|
|
3
4
|
import { TtyRenderer } from "./tty-renderer.js";
|
|
4
|
-
export { detectCapabilities } from "./capabilities.js";
|
|
5
|
+
export { detectCapabilities, detectReducedMotion } from "./capabilities.js";
|
|
5
6
|
export { composeStatusLine, describePhase, ELAPSED_AFTER_MS, formatElapsed, formatTokens, phaseIdentity, } from "./state.js";
|
|
6
7
|
export { renderActionPreview, PREVIEW_MAX_LINES } from "./diff.js";
|
|
7
8
|
export { createStreamHighlighter, defaultLineHighlighter, } from "./highlight.js";
|
|
8
9
|
export { PlainRenderer } from "./plain-renderer.js";
|
|
10
|
+
export { ScreenReaderRenderer } from "./screen-reader-renderer.js";
|
|
9
11
|
export { TtyRenderer } from "./tty-renderer.js";
|
|
10
12
|
/**
|
|
11
|
-
* Build the renderer for the detected environment
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Build the renderer for the detected environment (U.11 adds the first branch):
|
|
14
|
+
* - `screenReader` → the linear, worded {@link ScreenReaderRenderer}, regardless
|
|
15
|
+
* of cursor support (a screen-reader TTY must not get the live region);
|
|
16
|
+
* - else cursor-safe → the managed-live-region {@link TtyRenderer} (static, no
|
|
17
|
+
* timer, when `reducedMotion`);
|
|
18
|
+
* - else → the append-only {@link PlainRenderer} (pipes, CI, `TERM=dumb`).
|
|
19
|
+
* Everything downstream talks to {@link StreamRenderer} and never re-probes.
|
|
15
20
|
*/
|
|
16
21
|
export function createRenderer(out = process.stdout, err = process.stderr, env = process.env) {
|
|
17
22
|
const caps = detectCapabilities(out, env);
|
|
23
|
+
if (caps.screenReader)
|
|
24
|
+
return new ScreenReaderRenderer(caps, out, err);
|
|
18
25
|
return caps.cursor
|
|
19
26
|
? new TtyRenderer(caps, out)
|
|
20
27
|
: new PlainRenderer(caps, out, err);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ActionPreview } from "../tools/types.js";
|
|
2
2
|
import { type Theme } from "../theme/index.js";
|
|
3
|
-
import type { RenderCapabilities, RenderStream, StreamRenderer, ToolLifecycleEvent } from "./types.js";
|
|
3
|
+
import type { ProgressState, RenderCapabilities, RenderPhase, RenderStream, StreamRenderer, ToolLifecycleEvent } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* The append-only renderer for pipes, CI, and cursor-less terminals. Emits no
|
|
6
6
|
* cursor-control sequences ever, and no color unless the capabilities say so
|
|
@@ -30,8 +30,8 @@ export declare class PlainRenderer implements StreamRenderer {
|
|
|
30
30
|
note(text: string): void;
|
|
31
31
|
preview(preview: ActionPreview): void;
|
|
32
32
|
status(): void;
|
|
33
|
-
setPhase(): void;
|
|
34
|
-
progress(): void;
|
|
33
|
+
setPhase(phase: RenderPhase | null): void;
|
|
34
|
+
progress(state: ProgressState | null): void;
|
|
35
35
|
toolLifecycle(event: ToolLifecycleEvent): void;
|
|
36
36
|
promptResolved(): void;
|
|
37
37
|
endTurn(): void;
|
|
@@ -59,12 +59,20 @@ export class PlainRenderer {
|
|
|
59
59
|
status() {
|
|
60
60
|
// Append-only medium: transient state is dropped by design.
|
|
61
61
|
}
|
|
62
|
-
setPhase
|
|
62
|
+
// setPhase / progress declare their StreamRenderer params because the
|
|
63
|
+
// ScreenReaderRenderer subclass overrides them to announce (the override must
|
|
64
|
+
// be signature-compatible). In the plain medium they are no-ops: there is no
|
|
65
|
+
// live region to update, so the guard simply returns.
|
|
66
|
+
setPhase(phase) {
|
|
63
67
|
// Phases are live-region state; there is no live region here (U.4).
|
|
68
|
+
if (phase !== null)
|
|
69
|
+
return;
|
|
64
70
|
}
|
|
65
|
-
progress() {
|
|
71
|
+
progress(state) {
|
|
66
72
|
// The committed plan trail (C.31, via PromptIO) is the record in this
|
|
67
73
|
// medium; a live [i/n] prefix would just duplicate it line by line.
|
|
74
|
+
if (state !== null)
|
|
75
|
+
return;
|
|
68
76
|
}
|
|
69
77
|
toolLifecycle(event) {
|
|
70
78
|
if (event.event === "start") {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PlainRenderer } from "./plain-renderer.js";
|
|
2
|
+
import type { ProgressState, RenderCapabilities, RenderPhase, RenderStream, ToolLifecycleEvent } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The screen-reader renderer (U.11): {@link PlainRenderer}'s linear,
|
|
5
|
+
* append-only, zero-cursor-control output — plus two things a screen reader
|
|
6
|
+
* needs that the plain path drops.
|
|
7
|
+
*
|
|
8
|
+
* 1. **Worded status.** Its theme resolves the {@link SCREEN_READER_GLYPHS}
|
|
9
|
+
* table (via `caps.screenReader`), so every inherited code path that writes
|
|
10
|
+
* `theme.glyph.success` prints `done` instead of `✓`, `failed` for `✗`, etc.
|
|
11
|
+
* Nothing here special-cases it — the glyph table does the work.
|
|
12
|
+
*
|
|
13
|
+
* 2. **Announced state.** A screen reader cannot see an in-place live region, so
|
|
14
|
+
* each state change is emitted as its own committed line (never a redraw):
|
|
15
|
+
* a phase becomes `working: read_file src/x.ts`, a tool run brackets as
|
|
16
|
+
* `working: …` → `done …`, and plan progress announces `step 2 of 5: title`.
|
|
17
|
+
* Announcements are deduped by phase *identity* so token-count updates within
|
|
18
|
+
* the same activity don't repeat a line.
|
|
19
|
+
*
|
|
20
|
+
* Everything else — text streaming, `endSegment`, `note`, `preview` — is
|
|
21
|
+
* inherited unchanged. This is the whole of screen-reader mode: no parallel
|
|
22
|
+
* path, no behavior change to the plain or TTY renderers.
|
|
23
|
+
*/
|
|
24
|
+
export declare class ScreenReaderRenderer extends PlainRenderer {
|
|
25
|
+
/** Identity of the last phase announced, so we speak each activity once. */
|
|
26
|
+
private lastPhaseId;
|
|
27
|
+
/** Text of the last progress line announced, to avoid repeats. */
|
|
28
|
+
private lastProgress;
|
|
29
|
+
constructor(caps: RenderCapabilities, out: RenderStream, err: RenderStream);
|
|
30
|
+
/**
|
|
31
|
+
* Announce a phase transition as a committed line. `null`, `awaiting-approval`
|
|
32
|
+
* (the prompt block is its own announcement), and `calling-tool` (announced by
|
|
33
|
+
* {@link toolLifecycle}) are silent; repeats of the same activity are deduped.
|
|
34
|
+
*/
|
|
35
|
+
setPhase(phase: RenderPhase | null): void;
|
|
36
|
+
/** Announce plan-step progress as `step i of n: title`, deduped. */
|
|
37
|
+
progress(state: ProgressState | null): void;
|
|
38
|
+
/**
|
|
39
|
+
* Bracket a tool call with two committed lines — `working: label` on start,
|
|
40
|
+
* `done/failed label` on end — so a screen-reader user hears both that a call
|
|
41
|
+
* began (crucial for long ones) and how it resolved. The end note + honest
|
|
42
|
+
* duration is the inherited plain behavior; only the start line is added.
|
|
43
|
+
*/
|
|
44
|
+
toolLifecycle(event: ToolLifecycleEvent): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PlainRenderer } from "./plain-renderer.js";
|
|
2
|
+
import { describePhase, phaseIdentity } from "./state.js";
|
|
3
|
+
/**
|
|
4
|
+
* The screen-reader renderer (U.11): {@link PlainRenderer}'s linear,
|
|
5
|
+
* append-only, zero-cursor-control output — plus two things a screen reader
|
|
6
|
+
* needs that the plain path drops.
|
|
7
|
+
*
|
|
8
|
+
* 1. **Worded status.** Its theme resolves the {@link SCREEN_READER_GLYPHS}
|
|
9
|
+
* table (via `caps.screenReader`), so every inherited code path that writes
|
|
10
|
+
* `theme.glyph.success` prints `done` instead of `✓`, `failed` for `✗`, etc.
|
|
11
|
+
* Nothing here special-cases it — the glyph table does the work.
|
|
12
|
+
*
|
|
13
|
+
* 2. **Announced state.** A screen reader cannot see an in-place live region, so
|
|
14
|
+
* each state change is emitted as its own committed line (never a redraw):
|
|
15
|
+
* a phase becomes `working: read_file src/x.ts`, a tool run brackets as
|
|
16
|
+
* `working: …` → `done …`, and plan progress announces `step 2 of 5: title`.
|
|
17
|
+
* Announcements are deduped by phase *identity* so token-count updates within
|
|
18
|
+
* the same activity don't repeat a line.
|
|
19
|
+
*
|
|
20
|
+
* Everything else — text streaming, `endSegment`, `note`, `preview` — is
|
|
21
|
+
* inherited unchanged. This is the whole of screen-reader mode: no parallel
|
|
22
|
+
* path, no behavior change to the plain or TTY renderers.
|
|
23
|
+
*/
|
|
24
|
+
export class ScreenReaderRenderer extends PlainRenderer {
|
|
25
|
+
/** Identity of the last phase announced, so we speak each activity once. */
|
|
26
|
+
lastPhaseId = "";
|
|
27
|
+
/** Text of the last progress line announced, to avoid repeats. */
|
|
28
|
+
lastProgress = "";
|
|
29
|
+
constructor(caps, out, err) {
|
|
30
|
+
super(caps, out, err);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Announce a phase transition as a committed line. `null`, `awaiting-approval`
|
|
34
|
+
* (the prompt block is its own announcement), and `calling-tool` (announced by
|
|
35
|
+
* {@link toolLifecycle}) are silent; repeats of the same activity are deduped.
|
|
36
|
+
*/
|
|
37
|
+
setPhase(phase) {
|
|
38
|
+
const id = phaseIdentity(phase);
|
|
39
|
+
if (id === this.lastPhaseId)
|
|
40
|
+
return;
|
|
41
|
+
this.lastPhaseId = id;
|
|
42
|
+
if (phase === null ||
|
|
43
|
+
phase.kind === "awaiting-approval" ||
|
|
44
|
+
phase.kind === "calling-tool") {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.note(describePhase(phase, this.theme.glyph));
|
|
48
|
+
}
|
|
49
|
+
/** Announce plan-step progress as `step i of n: title`, deduped. */
|
|
50
|
+
progress(state) {
|
|
51
|
+
if (state === null) {
|
|
52
|
+
this.lastProgress = "";
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const line = `step ${state.step} of ${state.of}: ${state.title}`;
|
|
56
|
+
if (line === this.lastProgress)
|
|
57
|
+
return;
|
|
58
|
+
this.lastProgress = line;
|
|
59
|
+
this.note(line);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Bracket a tool call with two committed lines — `working: label` on start,
|
|
63
|
+
* `done/failed label` on end — so a screen-reader user hears both that a call
|
|
64
|
+
* began (crucial for long ones) and how it resolved. The end note + honest
|
|
65
|
+
* duration is the inherited plain behavior; only the start line is added.
|
|
66
|
+
*/
|
|
67
|
+
toolLifecycle(event) {
|
|
68
|
+
if (event.event === "start") {
|
|
69
|
+
super.toolLifecycle(event); // records the start time (silent in plain)
|
|
70
|
+
this.note(`${this.theme.glyph.running}: ${event.label}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
super.toolLifecycle(event); // commits `done/failed label (duration)`
|
|
74
|
+
}
|
|
75
|
+
}
|
package/dist/render/types.d.ts
CHANGED
|
@@ -20,8 +20,22 @@ export interface RenderCapabilities {
|
|
|
20
20
|
color: boolean;
|
|
21
21
|
/** Cursor-control sequences are safe (`tty` and not `TERM=dumb`). */
|
|
22
22
|
cursor: boolean;
|
|
23
|
-
/** Animation is welcome (`cursor` and
|
|
23
|
+
/** Animation is welcome (`cursor` and motion is not reduced). */
|
|
24
24
|
spinner: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Motion is reduced (U.11): no spinner animation/timer, no in-place
|
|
27
|
+
* re-animation — state survives as static text, only movement stops. True
|
|
28
|
+
* under `CRUXY_NO_SPINNER` (alias) / `NO_MOTION` / `CRUXY_REDUCED_MOTION`, and
|
|
29
|
+
* implied by `screenReader`. The one axis the spinner gate keys off.
|
|
30
|
+
*/
|
|
31
|
+
reducedMotion: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Screen-reader mode (U.11): plain, linear, announce-friendly output — no
|
|
34
|
+
* live-region redraws (each state change is a committed line), no spinners,
|
|
35
|
+
* status glyphs rendered as words. Opt-in via `CRUXY_SCREEN_READER` /
|
|
36
|
+
* `ACCESSIBLE`; routes rendering to the linear path regardless of `cursor`.
|
|
37
|
+
*/
|
|
38
|
+
screenReader: boolean;
|
|
25
39
|
/** Unicode glyphs are safe (U.1) — false under `TERM=dumb` / `CRUXY_ASCII`;
|
|
26
40
|
* independent of `color`. Drives the theme's glyph table, not its stylers. */
|
|
27
41
|
unicode: boolean;
|
package/dist/theme/resolve.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { type Theme, type ThemeCapabilities } from "./tokens.js";
|
|
2
2
|
/**
|
|
3
|
-
* Theme resolution (U.1) — the ONE place picocolors is used. The
|
|
3
|
+
* Theme resolution (U.1/U.11) — the ONE place picocolors is used. The axes are
|
|
4
4
|
* fully independent by construction:
|
|
5
5
|
*
|
|
6
6
|
* - `color` drives the stylers only. `pc.createColors(false)` is the identity,
|
|
7
7
|
* so NO_COLOR yields zero ANSI bytes (dim/bold vanish too) — asserted in the
|
|
8
8
|
* tests.
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - the glyph table is chosen by `screenReader` (words) → else `unicode`
|
|
10
|
+
* (✓/✗) → else ASCII (`[ok]`/`[x]`). Never touches color: a NO_COLOR unicode
|
|
11
|
+
* terminal still prints ✓/✗ (glyphs aren't ANSI), a colored CRUXY_ASCII
|
|
12
|
+
* terminal prints a colored `[ok]`, and a screen reader gets colorless words.
|
|
12
13
|
*/
|
|
13
14
|
export declare function resolveTheme(caps: ThemeCapabilities): Theme;
|
|
14
15
|
/**
|
|
@@ -23,10 +24,20 @@ export declare function resolveTheme(caps: ThemeCapabilities): Theme;
|
|
|
23
24
|
* signature change.
|
|
24
25
|
*/
|
|
25
26
|
export declare function detectUnicode(env?: NodeJS.ProcessEnv): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the user asked for screen-reader mode (U.11) — opt-in via
|
|
29
|
+
* `CRUXY_SCREEN_READER` or the ecosystem `ACCESSIBLE` flag (set-and-non-empty).
|
|
30
|
+
* Deliberately NOT inferred from a pipe: auto-wording every non-TTY stream would
|
|
31
|
+
* change existing CI/plain output. The single source of the rule, called by
|
|
32
|
+
* `render/capabilities.ts` (to fill `RenderCapabilities.screenReader`) and by
|
|
33
|
+
* the boolean-seam {@link themeForColor} surfaces (approval, plan, onboarding).
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectScreenReader(env?: NodeJS.ProcessEnv): boolean;
|
|
26
36
|
/**
|
|
27
37
|
* Build a theme for a surface that only knows a `color` boolean (the U.3
|
|
28
|
-
* PromptIO, error formatting, plan/onboarding/CLI copy). Unicode
|
|
29
|
-
* from the environment so these surfaces still
|
|
30
|
-
*
|
|
38
|
+
* PromptIO, error formatting, plan/onboarding/CLI copy). Unicode and
|
|
39
|
+
* screen-reader mode are detected from the environment so these surfaces still
|
|
40
|
+
* degrade on dumb terminals and speak words to a screen reader, with no change
|
|
41
|
+
* to their public boolean signatures.
|
|
31
42
|
*/
|
|
32
43
|
export declare function themeForColor(color: boolean, env?: NodeJS.ProcessEnv): Theme;
|
package/dist/theme/resolve.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
|
-
import { ASCII_GLYPHS, UNICODE_GLYPHS, } from "./tokens.js";
|
|
2
|
+
import { ASCII_GLYPHS, SCREEN_READER_GLYPHS, UNICODE_GLYPHS, } from "./tokens.js";
|
|
3
3
|
/**
|
|
4
|
-
* Theme resolution (U.1) — the ONE place picocolors is used. The
|
|
4
|
+
* Theme resolution (U.1/U.11) — the ONE place picocolors is used. The axes are
|
|
5
5
|
* fully independent by construction:
|
|
6
6
|
*
|
|
7
7
|
* - `color` drives the stylers only. `pc.createColors(false)` is the identity,
|
|
8
8
|
* so NO_COLOR yields zero ANSI bytes (dim/bold vanish too) — asserted in the
|
|
9
9
|
* tests.
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - the glyph table is chosen by `screenReader` (words) → else `unicode`
|
|
11
|
+
* (✓/✗) → else ASCII (`[ok]`/`[x]`). Never touches color: a NO_COLOR unicode
|
|
12
|
+
* terminal still prints ✓/✗ (glyphs aren't ANSI), a colored CRUXY_ASCII
|
|
13
|
+
* terminal prints a colored `[ok]`, and a screen reader gets colorless words.
|
|
13
14
|
*/
|
|
14
15
|
export function resolveTheme(caps) {
|
|
15
16
|
const c = pc.createColors(caps.color);
|
|
16
|
-
const glyph = caps.
|
|
17
|
+
const glyph = caps.screenReader
|
|
18
|
+
? SCREEN_READER_GLYPHS
|
|
19
|
+
: caps.unicode
|
|
20
|
+
? UNICODE_GLYPHS
|
|
21
|
+
: ASCII_GLYPHS;
|
|
17
22
|
const strong = c.bold;
|
|
18
23
|
const indent = (text, level = 1) => {
|
|
19
24
|
const pad = " ".repeat(Math.max(0, level));
|
|
@@ -62,12 +67,29 @@ export function detectUnicode(env = process.env) {
|
|
|
62
67
|
return false;
|
|
63
68
|
return true;
|
|
64
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether the user asked for screen-reader mode (U.11) — opt-in via
|
|
72
|
+
* `CRUXY_SCREEN_READER` or the ecosystem `ACCESSIBLE` flag (set-and-non-empty).
|
|
73
|
+
* Deliberately NOT inferred from a pipe: auto-wording every non-TTY stream would
|
|
74
|
+
* change existing CI/plain output. The single source of the rule, called by
|
|
75
|
+
* `render/capabilities.ts` (to fill `RenderCapabilities.screenReader`) and by
|
|
76
|
+
* the boolean-seam {@link themeForColor} surfaces (approval, plan, onboarding).
|
|
77
|
+
*/
|
|
78
|
+
export function detectScreenReader(env = process.env) {
|
|
79
|
+
const set = (v) => v !== undefined && v !== "";
|
|
80
|
+
return set(env.CRUXY_SCREEN_READER) || set(env.ACCESSIBLE);
|
|
81
|
+
}
|
|
65
82
|
/**
|
|
66
83
|
* Build a theme for a surface that only knows a `color` boolean (the U.3
|
|
67
|
-
* PromptIO, error formatting, plan/onboarding/CLI copy). Unicode
|
|
68
|
-
* from the environment so these surfaces still
|
|
69
|
-
*
|
|
84
|
+
* PromptIO, error formatting, plan/onboarding/CLI copy). Unicode and
|
|
85
|
+
* screen-reader mode are detected from the environment so these surfaces still
|
|
86
|
+
* degrade on dumb terminals and speak words to a screen reader, with no change
|
|
87
|
+
* to their public boolean signatures.
|
|
70
88
|
*/
|
|
71
89
|
export function themeForColor(color, env = process.env) {
|
|
72
|
-
return resolveTheme({
|
|
90
|
+
return resolveTheme({
|
|
91
|
+
color,
|
|
92
|
+
unicode: detectUnicode(env),
|
|
93
|
+
screenReader: detectScreenReader(env),
|
|
94
|
+
});
|
|
73
95
|
}
|
package/dist/theme/tokens.d.ts
CHANGED
|
@@ -90,10 +90,16 @@ export interface Theme {
|
|
|
90
90
|
readonly color: boolean;
|
|
91
91
|
readonly unicode: boolean;
|
|
92
92
|
}
|
|
93
|
-
/** The
|
|
93
|
+
/** The axes a theme is resolved from — a structural subset of RenderCapabilities. */
|
|
94
94
|
export interface ThemeCapabilities {
|
|
95
95
|
color: boolean;
|
|
96
96
|
unicode: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Screen-reader mode (U.11): status glyphs render as words. Optional so every
|
|
99
|
+
* existing `{color, unicode}` caller is unchanged (defaults to off). When set
|
|
100
|
+
* it takes precedence over `unicode` for the glyph table.
|
|
101
|
+
*/
|
|
102
|
+
screenReader?: boolean;
|
|
97
103
|
}
|
|
98
104
|
/** The unicode glyph table (real terminals). */
|
|
99
105
|
export declare const UNICODE_GLYPHS: ThemeGlyphs;
|
|
@@ -102,3 +108,12 @@ export declare const UNICODE_GLYPHS: ThemeGlyphs;
|
|
|
102
108
|
* mojibake — the intentional U.1 degradation for unicode-unsafe terminals.
|
|
103
109
|
*/
|
|
104
110
|
export declare const ASCII_GLYPHS: ThemeGlyphs;
|
|
111
|
+
/**
|
|
112
|
+
* The screen-reader glyph table (U.11): the status glyphs a screen reader would
|
|
113
|
+
* otherwise announce as bare punctuation are spelled as words — `done`,
|
|
114
|
+
* `failed`, `working`, `pending` — so `✓ read_file` becomes `done read_file`.
|
|
115
|
+
* Structural glyphs stay ASCII-legible (a pointer/caret has no useful word).
|
|
116
|
+
* This is the entire "worded status" of screen-reader mode: swap the table,
|
|
117
|
+
* reuse every existing renderer/theme code path unchanged.
|
|
118
|
+
*/
|
|
119
|
+
export declare const SCREEN_READER_GLYPHS: ThemeGlyphs;
|
package/dist/theme/tokens.js
CHANGED
|
@@ -50,3 +50,30 @@ export const ASCII_GLYPHS = {
|
|
|
50
50
|
spinnerFrames: ["-", "\\", "|", "/"],
|
|
51
51
|
spinnerStatic: "~",
|
|
52
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* The screen-reader glyph table (U.11): the status glyphs a screen reader would
|
|
55
|
+
* otherwise announce as bare punctuation are spelled as words — `done`,
|
|
56
|
+
* `failed`, `working`, `pending` — so `✓ read_file` becomes `done read_file`.
|
|
57
|
+
* Structural glyphs stay ASCII-legible (a pointer/caret has no useful word).
|
|
58
|
+
* This is the entire "worded status" of screen-reader mode: swap the table,
|
|
59
|
+
* reuse every existing renderer/theme code path unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export const SCREEN_READER_GLYPHS = {
|
|
62
|
+
success: "done",
|
|
63
|
+
failure: "failed",
|
|
64
|
+
pending: "pending",
|
|
65
|
+
running: "working",
|
|
66
|
+
pointer: ">",
|
|
67
|
+
caret: ">",
|
|
68
|
+
arrow: "->",
|
|
69
|
+
caretUp: "up",
|
|
70
|
+
caretDown: "down",
|
|
71
|
+
cursorBar: "",
|
|
72
|
+
bullet: "-",
|
|
73
|
+
sep: "-",
|
|
74
|
+
ellipsis: "...",
|
|
75
|
+
play: ">",
|
|
76
|
+
// Unused in the linear screen-reader path (no live region), kept legible.
|
|
77
|
+
spinnerFrames: ["working"],
|
|
78
|
+
spinnerStatic: "working",
|
|
79
|
+
};
|