@aragon-agent/cli 0.4.3

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.
Files changed (156) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +558 -0
  3. package/dist/agent/coalesce.d.ts +14 -0
  4. package/dist/agent/coalesce.js +41 -0
  5. package/dist/agent/controller.d.ts +132 -0
  6. package/dist/agent/controller.js +332 -0
  7. package/dist/agent/headless.d.ts +22 -0
  8. package/dist/agent/headless.js +74 -0
  9. package/dist/agent/reducer.d.ts +181 -0
  10. package/dist/agent/reducer.js +420 -0
  11. package/dist/agent/system-prompt.d.ts +30 -0
  12. package/dist/agent/system-prompt.js +58 -0
  13. package/dist/agent/usage.d.ts +13 -0
  14. package/dist/agent/usage.js +41 -0
  15. package/dist/cli.d.ts +11 -0
  16. package/dist/cli.js +482 -0
  17. package/dist/commands/builtins.d.ts +16 -0
  18. package/dist/commands/builtins.js +223 -0
  19. package/dist/commands/registry.d.ts +48 -0
  20. package/dist/commands/registry.js +45 -0
  21. package/dist/commands/skills.d.ts +22 -0
  22. package/dist/commands/skills.js +625 -0
  23. package/dist/config/app-paths.d.ts +11 -0
  24. package/dist/config/app-paths.js +16 -0
  25. package/dist/config/env.d.ts +28 -0
  26. package/dist/config/env.js +92 -0
  27. package/dist/config/load.d.ts +48 -0
  28. package/dist/config/load.js +158 -0
  29. package/dist/config/migrate-legacy-state.d.ts +78 -0
  30. package/dist/config/migrate-legacy-state.js +181 -0
  31. package/dist/config/schema.d.ts +249 -0
  32. package/dist/config/schema.js +247 -0
  33. package/dist/config/store.d.ts +44 -0
  34. package/dist/config/store.js +126 -0
  35. package/dist/input/keymap.d.ts +20 -0
  36. package/dist/input/keymap.js +85 -0
  37. package/dist/session/persist.d.ts +24 -0
  38. package/dist/session/persist.js +51 -0
  39. package/dist/skills/archive.d.ts +41 -0
  40. package/dist/skills/archive.js +152 -0
  41. package/dist/skills/cli-commands.d.ts +52 -0
  42. package/dist/skills/cli-commands.js +502 -0
  43. package/dist/skills/fetch-source.d.ts +104 -0
  44. package/dist/skills/fetch-source.js +448 -0
  45. package/dist/skills/installer.d.ts +117 -0
  46. package/dist/skills/installer.js +414 -0
  47. package/dist/skills/lock.d.ts +75 -0
  48. package/dist/skills/lock.js +223 -0
  49. package/dist/skills/manifest.d.ts +47 -0
  50. package/dist/skills/manifest.js +95 -0
  51. package/dist/skills/node-host.d.ts +23 -0
  52. package/dist/skills/node-host.js +59 -0
  53. package/dist/skills/paths.d.ts +61 -0
  54. package/dist/skills/paths.js +107 -0
  55. package/dist/skills/service.d.ts +256 -0
  56. package/dist/skills/service.js +730 -0
  57. package/dist/skills/tools.d.ts +35 -0
  58. package/dist/skills/tools.js +164 -0
  59. package/dist/skills/updater.d.ts +101 -0
  60. package/dist/skills/updater.js +330 -0
  61. package/dist/skills/usage.d.ts +72 -0
  62. package/dist/skills/usage.js +216 -0
  63. package/dist/tools/bash-tool.d.ts +12 -0
  64. package/dist/tools/bash-tool.js +131 -0
  65. package/dist/tools/diff.d.ts +15 -0
  66. package/dist/tools/diff.js +50 -0
  67. package/dist/tools/fs-tools.d.ts +16 -0
  68. package/dist/tools/fs-tools.js +215 -0
  69. package/dist/tools/index.d.ts +93 -0
  70. package/dist/tools/index.js +157 -0
  71. package/dist/tools/search-tools.d.ts +11 -0
  72. package/dist/tools/search-tools.js +225 -0
  73. package/dist/ui/App.d.ts +24 -0
  74. package/dist/ui/App.js +522 -0
  75. package/dist/ui/AutocompletePopup.d.ts +21 -0
  76. package/dist/ui/AutocompletePopup.js +19 -0
  77. package/dist/ui/Composer.d.ts +40 -0
  78. package/dist/ui/Composer.js +61 -0
  79. package/dist/ui/Header.d.ts +30 -0
  80. package/dist/ui/Header.js +25 -0
  81. package/dist/ui/Logo.d.ts +50 -0
  82. package/dist/ui/Logo.js +85 -0
  83. package/dist/ui/Markdown.d.ts +23 -0
  84. package/dist/ui/Markdown.js +122 -0
  85. package/dist/ui/PromptInput.d.ts +81 -0
  86. package/dist/ui/PromptInput.js +383 -0
  87. package/dist/ui/SessionOpener.d.ts +30 -0
  88. package/dist/ui/SessionOpener.js +12 -0
  89. package/dist/ui/StatusBar.d.ts +39 -0
  90. package/dist/ui/StatusBar.js +43 -0
  91. package/dist/ui/ToastStack.d.ts +24 -0
  92. package/dist/ui/ToastStack.js +38 -0
  93. package/dist/ui/Transcript.d.ts +62 -0
  94. package/dist/ui/Transcript.js +128 -0
  95. package/dist/ui/capabilities.d.ts +25 -0
  96. package/dist/ui/capabilities.js +59 -0
  97. package/dist/ui/console-bridge.d.ts +19 -0
  98. package/dist/ui/console-bridge.js +52 -0
  99. package/dist/ui/density.d.ts +26 -0
  100. package/dist/ui/density.js +34 -0
  101. package/dist/ui/entries/AssistantEntry.d.ts +23 -0
  102. package/dist/ui/entries/AssistantEntry.js +13 -0
  103. package/dist/ui/entries/EntryFrame.d.ts +23 -0
  104. package/dist/ui/entries/EntryFrame.js +6 -0
  105. package/dist/ui/entries/ToolCard.d.ts +38 -0
  106. package/dist/ui/entries/ToolCard.js +72 -0
  107. package/dist/ui/entries/ToolPreview.d.ts +57 -0
  108. package/dist/ui/entries/ToolPreview.js +144 -0
  109. package/dist/ui/entries/UserEntry.d.ts +14 -0
  110. package/dist/ui/entries/UserEntry.js +5 -0
  111. package/dist/ui/exit-snapshot.d.ts +27 -0
  112. package/dist/ui/exit-snapshot.js +26 -0
  113. package/dist/ui/gauge.d.ts +24 -0
  114. package/dist/ui/gauge.js +36 -0
  115. package/dist/ui/glyphs.d.ts +87 -0
  116. package/dist/ui/glyphs.js +127 -0
  117. package/dist/ui/gradient.d.ts +11 -0
  118. package/dist/ui/gradient.js +50 -0
  119. package/dist/ui/layout/AppShell.d.ts +24 -0
  120. package/dist/ui/layout/AppShell.js +9 -0
  121. package/dist/ui/layout/Gutter.d.ts +67 -0
  122. package/dist/ui/layout/Gutter.js +42 -0
  123. package/dist/ui/layout/OverlayFrame.d.ts +62 -0
  124. package/dist/ui/layout/OverlayFrame.js +92 -0
  125. package/dist/ui/layout/ScrollViewport.d.ts +33 -0
  126. package/dist/ui/layout/ScrollViewport.js +71 -0
  127. package/dist/ui/layout/budget.d.ts +42 -0
  128. package/dist/ui/layout/budget.js +47 -0
  129. package/dist/ui/layout/frame.d.ts +58 -0
  130. package/dist/ui/layout/frame.js +75 -0
  131. package/dist/ui/layout/overlay-window.d.ts +43 -0
  132. package/dist/ui/layout/overlay-window.js +51 -0
  133. package/dist/ui/layout/scroll.d.ts +28 -0
  134. package/dist/ui/layout/scroll.js +76 -0
  135. package/dist/ui/layout/useTerminalSize.d.ts +13 -0
  136. package/dist/ui/layout/useTerminalSize.js +50 -0
  137. package/dist/ui/markdown-blocks.d.ts +62 -0
  138. package/dist/ui/markdown-blocks.js +120 -0
  139. package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
  140. package/dist/ui/overlays/ConfirmDialog.js +25 -0
  141. package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
  142. package/dist/ui/overlays/HelpOverlay.js +60 -0
  143. package/dist/ui/overlays/ModelPicker.d.ts +29 -0
  144. package/dist/ui/overlays/ModelPicker.js +29 -0
  145. package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
  146. package/dist/ui/overlays/SettingsScreen.js +106 -0
  147. package/dist/ui/palettes.d.ts +79 -0
  148. package/dist/ui/palettes.js +115 -0
  149. package/dist/ui/screen.d.ts +27 -0
  150. package/dist/ui/screen.js +57 -0
  151. package/dist/ui/theme.d.ts +78 -0
  152. package/dist/ui/theme.js +127 -0
  153. package/dist/ui/transcript-text.d.ts +27 -0
  154. package/dist/ui/transcript-text.js +110 -0
  155. package/package.json +73 -0
  156. package/skills/skill-creator/SKILL.md +96 -0
@@ -0,0 +1,128 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Transcript (spec §3.8). Splits the entry list into a settled prefix rendered
4
+ * once into Ink's `<Static>` (native terminal scrollback, never re-rendered)
5
+ * and a live tail rendered normally. This kills per-token full re-renders and
6
+ * flicker on long/fast sessions.
7
+ *
8
+ * `computeSettledCount` is pure and exported for `transcript-static.test.ts`.
9
+ * The settled boundary is held MONOTONIC at render time: a previously-settled
10
+ * entry never leaves `<Static>` (which cannot un-print it) even if it later
11
+ * becomes expanded, so the live tail can never duplicate a scrolled-off entry.
12
+ *
13
+ * `EntryView` is shared by both render paths ON PURPOSE (§4.3 / §12). Forking it
14
+ * to freeze inline's v0.2.0 output would mean writing every future entry-layer
15
+ * change twice and watching the two drift; inline is a degradation path, not a
16
+ * parallel product. What inline still guarantees is its GEOMETRY contract — no
17
+ * fixed frame, `<Static>` keeps the settled history, no private ANSI — not its
18
+ * pixel-for-pixel appearance.
19
+ */
20
+ import { useRef } from 'react';
21
+ import { Box, Static, Text } from 'ink';
22
+ import Spinner from 'ink-spinner';
23
+ import { pickGlyphs } from './glyphs.js';
24
+ import { separationRows } from './density.js';
25
+ import { EntryFrame } from './entries/EntryFrame.js';
26
+ import { UserEntry } from './entries/UserEntry.js';
27
+ import { AssistantEntry } from './entries/AssistantEntry.js';
28
+ import { ToolCard, statusColor, toolEntryGlyph } from './entries/ToolCard.js';
29
+ /** Keep the last K entries in the live region so a mutating tail stays hot. */
30
+ const LIVE_TAIL = 1;
31
+ /**
32
+ * Largest prefix length P such that `entries[0..P)` are all terminal, none is
33
+ * within the last `LIVE_TAIL` entries, and none is currently expanded. Pure and
34
+ * non-decreasing as entries terminalize (expanding a card can lower it — the
35
+ * component clamps with a monotonic high-water mark to avoid Static duplication).
36
+ */
37
+ export function computeSettledCount(entries, expandedToolIds) {
38
+ const limit = entries.length - LIVE_TAIL;
39
+ let count = 0;
40
+ for (let i = 0; i < entries.length; i += 1) {
41
+ if (i >= limit)
42
+ break;
43
+ const e = entries[i];
44
+ if (expandedToolIds[e.id])
45
+ break;
46
+ if (e.kind === 'assistant' && e.streaming)
47
+ break;
48
+ if (e.kind === 'tool' && e.status !== 'done' && e.status !== 'error')
49
+ break;
50
+ count = i + 1;
51
+ }
52
+ return count;
53
+ }
54
+ function noticeColor(level, theme) {
55
+ if (level === 'error')
56
+ return theme.noticeError;
57
+ if (level === 'warn')
58
+ return theme.noticeWarn;
59
+ return theme.noticeInfo;
60
+ }
61
+ function noticeSymbol(level, theme) {
62
+ if (level === 'error')
63
+ return theme.symbols.error;
64
+ if (level === 'warn')
65
+ return theme.symbols.warn;
66
+ return theme.symbols.info;
67
+ }
68
+ function EntryView({ entry, prev, expanded, thinkingVisible, reducedMotion, density, theme, caps, }) {
69
+ const glyphs = pickGlyphs(caps);
70
+ const separation = separationRows(prev, entry, density);
71
+ const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
72
+ switch (entry.kind) {
73
+ case 'user':
74
+ return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme }));
75
+ case 'assistant': {
76
+ // While text streams, the spinner IS the role marker. Braille dots are
77
+ // both an animation and a Unicode-only glyph, so reduced motion and an
78
+ // ASCII terminal fall back to the same static marker (P2-11 / A-10).
79
+ const animate = entry.streaming && !reducedMotion && caps.unicode;
80
+ return frame(animate ? _jsx(Spinner, { type: "dots" }) : glyphs.assistant, theme.primary, _jsx(AssistantEntry, { text: entry.text, thinking: entry.thinking, thinkingOpen: entry.thinkingOpen, thinkingVisible: thinkingVisible, streaming: entry.streaming, aborted: entry.aborted, theme: theme, caps: caps }));
81
+ }
82
+ case 'tool':
83
+ return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
84
+ case 'notice':
85
+ return frame(noticeSymbol(entry.level, theme), noticeColor(entry.level, theme), _jsx(Text, { color: noticeColor(entry.level, theme), children: entry.text }));
86
+ default:
87
+ return null;
88
+ }
89
+ }
90
+ /**
91
+ * Full-screen transcript body — the same entry renderers with NO `<Static>`.
92
+ *
93
+ * `<Static>` prints above the live frame; once the frame is `rows - 1` tall
94
+ * there is exactly one visible line up there, so the mechanism stops being a
95
+ * history view and starts being a leak. Full-screen therefore owns its history
96
+ * inside `ScrollViewport` instead.
97
+ *
98
+ * `computeSettledCount` is deliberately NOT called here: its only job is to keep
99
+ * an entry that already reached `<Static>` from flowing back into the live
100
+ * region, and with no Static there is nothing to protect. The settled high-water
101
+ * mark feeds nothing else — `/save` serializes `entries` in full — so leaving it
102
+ * un-advanced has no side effect.
103
+ *
104
+ * Rendering budget (I-3): yoga lays out every child even when `overflow: hidden`
105
+ * clips it, so cost grows linearly with the entry count. Only the last
106
+ * `windowSize` entries are rendered; the reducer and `/save` keep everything.
107
+ */
108
+ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, windowSize, }) {
109
+ const size = Math.max(1, Math.floor(windowSize));
110
+ const visible = entries.length > size ? entries.slice(-size) : entries;
111
+ const collapsed = entries.length - visible.length;
112
+ const glyphs = pickGlyphs(caps);
113
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [collapsed > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: [glyphs.ellipsis, " ", collapsed, " earlier ", collapsed === 1 ? 'entry' : 'entries', " collapsed (/save exports the full session)"] })), visible.map((entry, i) => (_jsx(EntryView, { entry: entry, prev: i > 0 ? visible[i - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }, entry.id)))] }));
114
+ }
115
+ export function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, }) {
116
+ const highWater = useRef(0);
117
+ const prevLen = useRef(0);
118
+ const raw = computeSettledCount(entries, expandedToolIds);
119
+ // The transcript shrank (clear / reset / restore) — drop the high-water mark.
120
+ if (entries.length < prevLen.current)
121
+ highWater.current = 0;
122
+ prevLen.current = entries.length;
123
+ const settled = Math.min(entries.length, Math.max(highWater.current, raw));
124
+ highWater.current = settled;
125
+ const settledEntries = entries.slice(0, settled);
126
+ const liveEntries = entries.slice(settled);
127
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: settledEntries, children: (entry, index) => (_jsx(EntryView, { entry: entry, prev: index > 0 ? settledEntries[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }, entry.id)) }), _jsx(Box, { flexDirection: "column", children: liveEntries.map((entry, i) => (_jsx(EntryView, { entry: entry, prev: i > 0 ? liveEntries[i - 1] : entries[settled - 1], expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }, entry.id))) })] }));
128
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Terminal capability detection (spec §3.2).
3
+ *
4
+ * A *pure* `detectCapabilities(env, stdout)` that reports how much color and
5
+ * which glyph set the current terminal can render, so the theme engine can
6
+ * degrade truecolor → 256 → 16 → monochrome and swap Unicode box/round glyphs
7
+ * for ASCII on legacy terminals (e.g. `cmd.exe`). Keeping it env-in / value-out
8
+ * makes it unit-testable without a real TTY.
9
+ */
10
+ export interface TermCapabilities {
11
+ /** 0 none · 1 basic-16 · 2 ansi-256 · 3 truecolor. */
12
+ colorLevel: 0 | 1 | 2 | 3;
13
+ /** May we use box/round glyphs, spinners, and emoji-ish symbols? */
14
+ unicode: boolean;
15
+ }
16
+ type EnvLike = Record<string, string | undefined>;
17
+ interface StdoutLike {
18
+ isTTY?: boolean;
19
+ }
20
+ /**
21
+ * Detect the terminal's color depth and Unicode support. `stdout` is accepted
22
+ * for future TTY-aware refinements and kept in the signature for callers.
23
+ */
24
+ export declare function detectCapabilities(env: EnvLike, _stdout?: StdoutLike): TermCapabilities;
25
+ export {};
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Terminal capability detection (spec §3.2).
3
+ *
4
+ * A *pure* `detectCapabilities(env, stdout)` that reports how much color and
5
+ * which glyph set the current terminal can render, so the theme engine can
6
+ * degrade truecolor → 256 → 16 → monochrome and swap Unicode box/round glyphs
7
+ * for ASCII on legacy terminals (e.g. `cmd.exe`). Keeping it env-in / value-out
8
+ * makes it unit-testable without a real TTY.
9
+ */
10
+ import chalk from 'chalk';
11
+ /** Resolve the effective color depth from environment signals + chalk. */
12
+ function detectColorLevel(env) {
13
+ // Explicit opt-outs win (https://no-color.org — any value, even empty).
14
+ if (env.NO_COLOR !== undefined)
15
+ return 0;
16
+ if (env.TERM === 'dumb')
17
+ return 0;
18
+ // A truecolor-capable terminal advertises itself.
19
+ const colorterm = env.COLORTERM?.toLowerCase();
20
+ if (colorterm === 'truecolor' || colorterm === '24bit')
21
+ return 3;
22
+ // FORCE_COLOR is an explicit override of the auto-detected depth.
23
+ const force = env.FORCE_COLOR;
24
+ if (force !== undefined) {
25
+ if (force === '0' || force === 'false')
26
+ return 0;
27
+ if (force === '2')
28
+ return 2;
29
+ if (force === '3')
30
+ return 3;
31
+ return 1; // '1' | 'true' | '' → at least basic color
32
+ }
33
+ // Fall back to chalk's own platform detection.
34
+ const level = chalk.level;
35
+ return level >= 0 && level <= 3 ? level : 0;
36
+ }
37
+ /** Decide whether Unicode box/round glyphs are safe to emit. */
38
+ function detectUnicode(env) {
39
+ const locale = [env.LC_ALL, env.LC_CTYPE, env.LANG].filter(Boolean).join(' ').toUpperCase();
40
+ if (locale.includes('UTF'))
41
+ return true;
42
+ if (env.WT_SESSION)
43
+ return true; // Windows Terminal
44
+ const program = env.TERM_PROGRAM;
45
+ if (program === 'vscode' || program === 'iTerm.app' || program === 'Apple_Terminal')
46
+ return true;
47
+ // Legacy cmd.exe (no TERM, no WT_SESSION) and unknown terminals fall back to ASCII.
48
+ return false;
49
+ }
50
+ /**
51
+ * Detect the terminal's color depth and Unicode support. `stdout` is accepted
52
+ * for future TTY-aware refinements and kept in the signature for callers.
53
+ */
54
+ export function detectCapabilities(env, _stdout) {
55
+ return {
56
+ colorLevel: detectColorLevel(env),
57
+ unicode: detectUnicode(env),
58
+ };
59
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Console bridge — invariant I-4 (spec §4.9).
3
+ *
4
+ * `patchConsole: true` makes Ink do `log.clear() → stdout.write(data) →
5
+ * log(lastOutput)`. Under a fixed frame that write pushes the frame down a row
6
+ * and permanently corrupts the line accounting described in §4.2: from then on
7
+ * `eraseLines(previousLineCount)` is off by one and the frame slowly eats the
8
+ * content above it.
9
+ *
10
+ * So full-screen mode runs with `patchConsole: false` and routes `console.*`
11
+ * into the transcript as notice entries, where the output is actually READABLE
12
+ * instead of merely being somewhere on screen. Inline mode keeps Ink's patching.
13
+ */
14
+ import type { NoticeLevel } from '../agent/reducer.js';
15
+ /**
16
+ * Hijack `console.*` for the lifetime of the returned restore function.
17
+ * Returns a restore callback; calling it twice is harmless.
18
+ */
19
+ export declare function installConsoleBridge(sink: (level: NoticeLevel, text: string) => void): () => void;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Console bridge — invariant I-4 (spec §4.9).
3
+ *
4
+ * `patchConsole: true` makes Ink do `log.clear() → stdout.write(data) →
5
+ * log(lastOutput)`. Under a fixed frame that write pushes the frame down a row
6
+ * and permanently corrupts the line accounting described in §4.2: from then on
7
+ * `eraseLines(previousLineCount)` is off by one and the frame slowly eats the
8
+ * content above it.
9
+ *
10
+ * So full-screen mode runs with `patchConsole: false` and routes `console.*`
11
+ * into the transcript as notice entries, where the output is actually READABLE
12
+ * instead of merely being somewhere on screen. Inline mode keeps Ink's patching.
13
+ */
14
+ import { format } from 'node:util';
15
+ const LEVEL_BY_METHOD = {
16
+ log: 'info',
17
+ info: 'info',
18
+ warn: 'warn',
19
+ error: 'error',
20
+ debug: 'info',
21
+ };
22
+ /**
23
+ * Hijack `console.*` for the lifetime of the returned restore function.
24
+ * Returns a restore callback; calling it twice is harmless.
25
+ */
26
+ export function installConsoleBridge(sink) {
27
+ const methods = Object.keys(LEVEL_BY_METHOD);
28
+ const originals = new Map();
29
+ for (const method of methods) {
30
+ originals.set(method, console[method].bind(console));
31
+ console[method] = (...args) => {
32
+ const text = format(...args);
33
+ if (text.length === 0)
34
+ return;
35
+ try {
36
+ sink(LEVEL_BY_METHOD[method], text);
37
+ }
38
+ catch {
39
+ // A failing sink must never turn a stray console.log into a crash.
40
+ }
41
+ };
42
+ }
43
+ let restored = false;
44
+ return () => {
45
+ if (restored)
46
+ return;
47
+ restored = true;
48
+ for (const [method, original] of originals) {
49
+ console[method] = original;
50
+ }
51
+ };
52
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Entry spacing policy (spec §4.3).
3
+ *
4
+ * Replaces four unconditional `marginTop={1}` sites (`UserEntry`,
5
+ * `AssistantEntry`, `ToolCard`, the notice branch in `Transcript`). Those spent
6
+ * a blank row on EVERY entry, so a plain "ask -> read a file -> answer" turn was
7
+ * 17 rows tall against a 13-16 row viewport: the user could only ever see the
8
+ * current step.
9
+ *
10
+ * Blank space now marks the one boundary that means something — the start of a
11
+ * new user turn. Everything inside a turn stays contiguous, and the left rail
12
+ * (§4.3) provides the grouping the blank rows used to.
13
+ *
14
+ * Pure and free of React so `density.test.ts` can enumerate the whole matrix.
15
+ */
16
+ import type { Entry } from '../agent/reducer.js';
17
+ import type { DensityMode } from '../config/schema.js';
18
+ export type { DensityMode };
19
+ /**
20
+ * Blank rows to insert BEFORE `next`. `prev === undefined` means `next` is the
21
+ * first item in the list.
22
+ *
23
+ * This is the ONLY place allowed to decide entry spacing: if a component also
24
+ * sets its own `marginTop`, the two stack and the turn boundary becomes 2 rows.
25
+ */
26
+ export declare function separationRows(prev: Entry | undefined, next: Entry, mode: DensityMode): 0 | 1;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Entry spacing policy (spec §4.3).
3
+ *
4
+ * Replaces four unconditional `marginTop={1}` sites (`UserEntry`,
5
+ * `AssistantEntry`, `ToolCard`, the notice branch in `Transcript`). Those spent
6
+ * a blank row on EVERY entry, so a plain "ask -> read a file -> answer" turn was
7
+ * 17 rows tall against a 13-16 row viewport: the user could only ever see the
8
+ * current step.
9
+ *
10
+ * Blank space now marks the one boundary that means something — the start of a
11
+ * new user turn. Everything inside a turn stays contiguous, and the left rail
12
+ * (§4.3) provides the grouping the blank rows used to.
13
+ *
14
+ * Pure and free of React so `density.test.ts` can enumerate the whole matrix.
15
+ */
16
+ /**
17
+ * Blank rows to insert BEFORE `next`. `prev === undefined` means `next` is the
18
+ * first item in the list.
19
+ *
20
+ * This is the ONLY place allowed to decide entry spacing: if a component also
21
+ * sets its own `marginTop`, the two stack and the turn boundary becomes 2 rows.
22
+ */
23
+ export function separationRows(prev, next, mode) {
24
+ if (!prev)
25
+ return 0;
26
+ if (mode === 'compact')
27
+ return 0;
28
+ // Consecutive tool calls read as one action; keep them tight.
29
+ if (prev.kind === 'tool' && next.kind === 'tool')
30
+ return 0;
31
+ if (next.kind === 'user')
32
+ return 1;
33
+ return 0;
34
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Render a streaming assistant entry: an optional collapsible thinking block
3
+ * above the markdown-rendered answer text. Empty entries render nothing (a turn
4
+ * that only produced tool calls has no assistant text).
5
+ *
6
+ * The role glyph and spacing belong to `EntryFrame` (§4.3) — including the
7
+ * streaming spinner, which IS the role marker while text is arriving.
8
+ */
9
+ import React from 'react';
10
+ import type { Theme } from '../theme.js';
11
+ import type { TermCapabilities } from '../capabilities.js';
12
+ interface AssistantEntryProps {
13
+ text: string;
14
+ thinking?: string;
15
+ thinkingOpen: boolean;
16
+ thinkingVisible: boolean;
17
+ streaming: boolean;
18
+ aborted?: boolean;
19
+ theme: Theme;
20
+ caps: TermCapabilities;
21
+ }
22
+ export declare function AssistantEntry(props: AssistantEntryProps): React.ReactElement | null;
23
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { pickGlyphs } from '../glyphs.js';
4
+ import { Markdown } from '../Markdown.js';
5
+ export function AssistantEntry(props) {
6
+ const { text, thinking, thinkingOpen, thinkingVisible, streaming, aborted, theme, caps } = props;
7
+ const glyphs = pickGlyphs(caps);
8
+ const hasThinking = !!thinking && thinking.trim().length > 0;
9
+ const hasText = text.trim().length > 0;
10
+ if (!hasThinking && !hasText && !streaming)
11
+ return null;
12
+ return (_jsxs(Box, { flexDirection: "column", children: [hasThinking && thinkingVisible && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [glyphs.thinking, " thinking", thinkingOpen ? glyphs.ellipsis : ''] }), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: thinking.split('\n').map((line, i) => (_jsx(Text, { color: theme.thinking, dimColor: true, children: line }, i))) })] })), (hasText || streaming) && (_jsxs(Box, { flexDirection: "column", children: [hasText ? (_jsx(Markdown, { text: text, theme: theme, caps: caps })) : (_jsx(Text, { color: theme.muted, children: glyphs.ellipsis })), aborted && _jsx(Text, { color: theme.noticeWarn, children: "[aborted]" })] }))] }));
13
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * EntryFrame (spec §4.3) — the single shell every transcript entry renders
3
+ * inside, and the ONLY place entry spacing is applied.
4
+ *
5
+ * Keeping both the rail and the spacing here is what makes `density.ts` a real
6
+ * policy rather than a suggestion: an entry component that also set its own
7
+ * `marginTop` would silently double the gap at turn boundaries.
8
+ */
9
+ import React from 'react';
10
+ export interface EntryFrameProps {
11
+ /** Role marker shown on the entry's first row (may be a live spinner). */
12
+ glyph: React.ReactNode;
13
+ /** Rail / glyph colour — for tool cards this is the status colour (§5.3). */
14
+ color?: string;
15
+ /** Rail character drawn on the turn-boundary spacer row (`separation === 1`). */
16
+ continuation: string;
17
+ /** From `separationRows()`. Rendered as a rail stub, never a bare margin. */
18
+ separation: 0 | 1;
19
+ /** Muted colour for the spacer row so it recedes behind real content. */
20
+ spacerColor?: string;
21
+ children: React.ReactNode;
22
+ }
23
+ export declare function EntryFrame({ glyph, color, continuation, separation, spacerColor, children, }: EntryFrameProps): React.ReactElement;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ import { Gutter, RailSpacer } from '../layout/Gutter.js';
4
+ export function EntryFrame({ glyph, color, continuation, separation, spacerColor, children, }) {
5
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [separation === 1 && _jsx(RailSpacer, { continuation: continuation, color: spacerColor }), _jsx(Gutter, { glyph: glyph, color: color, children: children })] }));
6
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Tool-call card (spec §3.5): a per-tool glyph, the tool name, a compact args
3
+ * summary, a colored status badge, and a duration, over a collapsible rich
4
+ * preview delegated to `<ToolPreview>`. Collapsed to the first 8 lines with a
5
+ * `+M lines (Ctrl+O)` footer; `Ctrl+O` / `/expand` toggle the full preview.
6
+ *
7
+ * Two v0.4.0 changes (§4.1 / §4.3):
8
+ * - capabilities arrive as a prop. This used to infer Unicode support by
9
+ * comparing `theme.symbols.gaugeFull` against a literal block character, so
10
+ * editing an unrelated gauge glyph would have silently sent every tool icon
11
+ * back to ASCII;
12
+ * - the preview's round border is gone. It cost 2 rows and 4 columns per card,
13
+ * and nested visibly inside markdown code blocks. A left rail carries the
14
+ * same grouping in 1 column and 0 rows.
15
+ */
16
+ import React from 'react';
17
+ import type { Theme } from '../theme.js';
18
+ import type { TermCapabilities } from '../capabilities.js';
19
+ import type { ToolStatus } from '../../agent/reducer.js';
20
+ interface ToolCardProps {
21
+ name: string;
22
+ label: string;
23
+ argsRaw: string;
24
+ args?: Record<string, unknown>;
25
+ status: ToolStatus;
26
+ preview?: string;
27
+ durationMs?: number;
28
+ isError?: boolean;
29
+ expanded?: boolean;
30
+ reducedMotion?: boolean;
31
+ theme: Theme;
32
+ caps: TermCapabilities;
33
+ }
34
+ export declare function statusColor(status: ToolStatus, theme: Theme): string | undefined;
35
+ /** The tool's own glyph, used by `EntryView` as the entry's rail marker. */
36
+ export declare function toolEntryGlyph(name: string, caps: TermCapabilities): string;
37
+ export declare function ToolCard(props: ToolCardProps): React.ReactElement;
38
+ export {};
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { pickGlyphs, toolGlyph } from '../glyphs.js';
5
+ import { railBorderProps } from '../layout/Gutter.js';
6
+ import { formatDuration } from '../../agent/usage.js';
7
+ import { ToolPreview, bashBadge } from './ToolPreview.js';
8
+ /** Number of preview lines shown before collapse. */
9
+ const COLLAPSED_LINES = 8;
10
+ export function statusColor(status, theme) {
11
+ switch (status) {
12
+ case 'pending':
13
+ return theme.toolPending;
14
+ case 'running':
15
+ return theme.toolRunning;
16
+ case 'done':
17
+ return theme.toolDone;
18
+ case 'error':
19
+ return theme.toolError;
20
+ }
21
+ }
22
+ /** One-line summary of the tool arguments for the card header. */
23
+ function summarizeArgs(name, ellipsis, args, argsRaw) {
24
+ if (args) {
25
+ if (name === 'bash' && typeof args.command === 'string')
26
+ return args.command;
27
+ if (typeof args.path === 'string')
28
+ return args.path;
29
+ if (typeof args.pattern === 'string')
30
+ return args.pattern;
31
+ const keys = Object.keys(args);
32
+ if (keys.length > 0)
33
+ return keys.map((k) => `${k}=${short(args[k], ellipsis)}`).join(' ');
34
+ }
35
+ return (argsRaw ?? '').slice(0, 60);
36
+ }
37
+ function short(v, ellipsis) {
38
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
39
+ return s.length > 40 ? `${s.slice(0, 40)}${ellipsis}` : s;
40
+ }
41
+ /** The badge label shown to the right of the tool name. */
42
+ function badgeLabel(name, status, preview) {
43
+ if (name === 'bash' && (status === 'done' || status === 'error')) {
44
+ return bashBadge(status, preview).text;
45
+ }
46
+ switch (status) {
47
+ case 'pending':
48
+ return 'queued';
49
+ case 'running':
50
+ return 'running';
51
+ case 'done':
52
+ return 'done';
53
+ case 'error':
54
+ return 'failed';
55
+ }
56
+ }
57
+ /** The tool's own glyph, used by `EntryView` as the entry's rail marker. */
58
+ export function toolEntryGlyph(name, caps) {
59
+ return toolGlyph(pickGlyphs(caps), name);
60
+ }
61
+ export function ToolCard(props) {
62
+ const { name, argsRaw, args, status, preview, durationMs, isError, expanded, reducedMotion, theme, caps, } = props;
63
+ const glyphs = pickGlyphs(caps);
64
+ const color = statusColor(status, theme);
65
+ const previewText = preview ?? '';
66
+ const previewLines = previewText.length > 0 ? previewText.split('\n') : [];
67
+ const visibleLines = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES);
68
+ const hidden = previewLines.length - visibleLines.length;
69
+ const showBody = previewLines.length > 0 && (status === 'done' || status === 'error');
70
+ const badge = status === 'running' && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " running"] })) : (_jsxs(Text, { color: color, children: [status === 'running' ? `${glyphs.spinnerStill} ` : '', badgeLabel(name, status, previewText)] }));
71
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: name }), _jsxs(Text, { color: theme.muted, children: [" ", summarizeArgs(name, glyphs.ellipsis, args, argsRaw), " "] }), badge, durationMs !== undefined && (_jsxs(Text, { color: theme.muted, children: [" (", formatDuration(durationMs), ")"] }))] }), showBody && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, isError ? theme.toolError : theme.border), children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] }))] }));
72
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Per-tool rich preview (spec §3.5). Selects a renderer by tool name: colored
3
+ * unified diffs (`edit_file`), a byte/path summary (`write_file`), a dimmed
4
+ * line-number gutter (`read_file`), a status-authoritative exit badge with a
5
+ * best-effort footer parse (`bash`), a dir/file/size listing (`list_dir`), a
6
+ * flat path list (`glob`), and a plain fallback (`grep` / default).
7
+ *
8
+ * The classification helpers are pure and exported so they are unit-testable
9
+ * without Ink.
10
+ */
11
+ import React from 'react';
12
+ import type { Theme } from '../theme.js';
13
+ import { type ToolStatus } from '../../agent/reducer.js';
14
+ export type DiffClass = 'add' | 'remove' | 'meta' | 'context';
15
+ /**
16
+ * Classify a unified-diff line. Meta prefixes (`@@`, `---`, `+++`) are tested
17
+ * FIRST so a `--- path` header is not mis-read as a single-char removal (P2-8);
18
+ * the `Applied edit to <path>:` lead-in falls through to `context`.
19
+ */
20
+ export declare function classifyDiffLine(line: string): DiffClass;
21
+ /**
22
+ * Derive the bash badge. The exit status is authoritative (the tool returns an
23
+ * error result iff the exit code is non-zero), so the label is `exit 0` /
24
+ * `failed` from `status`; a real `[exit code N]` / `[signal X]` last line is
25
+ * parsed best-effort for the exact number, falling back to the status label
26
+ * when the footer was truncated out of the stored preview (P1-2).
27
+ */
28
+ export declare function bashBadge(status: ToolStatus, preview: string): {
29
+ text: string;
30
+ ok: boolean;
31
+ };
32
+ /** Parse the real `Wrote <n> bytes to <path>` result — bytes+path only (P2-7). */
33
+ export declare function parseWriteFile(text: string): {
34
+ bytes: number;
35
+ path: string;
36
+ } | null;
37
+ export interface ListRow {
38
+ kind: 'dir' | 'file';
39
+ name: string;
40
+ size?: string;
41
+ }
42
+ /** Parse a real `list_dir` row (`dir <name>/` / `file <name> (<n>b)`) (P2-9). */
43
+ export declare function parseListRow(line: string): ListRow | null;
44
+ /**
45
+ * No `caps` here on purpose. The only glyph this component ever spelled was the
46
+ * preview truncation marker, and that now comes from the constant its producer
47
+ * writes (`PREVIEW_TRUNCATION_MARK`), so there is nothing left to degrade — and
48
+ * a prop no body reads is exactly the kind of dead weight the last round removed.
49
+ */
50
+ interface ToolPreviewProps {
51
+ name: string;
52
+ lines: string[];
53
+ theme: Theme;
54
+ isError?: boolean;
55
+ }
56
+ export declare function ToolPreview({ name, lines, theme, isError }: ToolPreviewProps): React.ReactElement;
57
+ export {};