@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,76 @@
1
+ /**
2
+ * Scroll maths for the self-drawn viewport (spec §4.5). Pure, React-free.
3
+ *
4
+ * `offset` counts LINES FROM THE BOTTOM — equivalently, the number of rows
5
+ * currently hidden BELOW the viewport. `0` means pinned to the newest output;
6
+ * `overflowLines` means parked at the very top. Counting from the bottom is what
7
+ * makes auto-follow free: when new content arrives `overflowLines` grows while
8
+ * `offset` stays 0, so the viewport keeps showing the tail without anyone
9
+ * calling a "scroll to bottom" routine.
10
+ *
11
+ * The rows hidden ABOVE are therefore `overflowLines - offset`. Mixing the two
12
+ * up inverts both the bottom-snap test and the "N new lines" hint.
13
+ */
14
+ /**
15
+ * Rows of context kept across a page turn, and — not a coincidence — the
16
+ * bottom-snap tolerance. The tolerance must be >= whatever a page turn can
17
+ * leave behind, or PgDn parks the user in a fake off-bottom state that they
18
+ * cannot press their way out of.
19
+ */
20
+ const PAGE_CONTEXT_LINES = 2;
21
+ export const BOTTOM_SNAP_TOLERANCE = PAGE_CONTEXT_LINES;
22
+ export function clampScroll(offset, overflowLines) {
23
+ const max = Math.max(0, overflowLines);
24
+ if (!Number.isFinite(offset) || offset < 0)
25
+ return 0;
26
+ return Math.min(Math.floor(offset), max);
27
+ }
28
+ export function pageSize(viewportHeight) {
29
+ return Math.max(1, Math.floor(viewportHeight) - PAGE_CONTEXT_LINES);
30
+ }
31
+ export function isPinnedToBottom(state) {
32
+ return state.offset === 0;
33
+ }
34
+ /** Intents that move toward the newest output; only these snap to the bottom. */
35
+ function isDownward(intent) {
36
+ return intent === 'lineDown' || intent === 'pageDown' || intent === 'toBottom';
37
+ }
38
+ export function applyScroll(state, intent, metrics) {
39
+ const overflowLines = Math.max(0, metrics.content - metrics.viewport);
40
+ const page = pageSize(metrics.viewport);
41
+ let next;
42
+ switch (intent) {
43
+ case 'lineUp':
44
+ next = state.offset + 1;
45
+ break;
46
+ case 'lineDown':
47
+ next = state.offset - 1;
48
+ break;
49
+ case 'pageUp':
50
+ next = state.offset + page;
51
+ break;
52
+ case 'pageDown':
53
+ next = state.offset - page;
54
+ break;
55
+ case 'toTop':
56
+ next = overflowLines;
57
+ break;
58
+ case 'toBottom':
59
+ next = 0;
60
+ break;
61
+ }
62
+ const clamped = clampScroll(next, overflowLines);
63
+ // `offset` IS the number of rows hidden below the viewport, so proximity to
64
+ // the bottom is `offset <= tolerance` — not `overflowLines - offset`, which
65
+ // measures the distance to the TOP. Without the snap, a PgDn that lands one
66
+ // row short parks the user in a fake off-bottom state (a stuck `↑1` and a
67
+ // permanent "new lines" hint) that no further PgDn can clear, because the
68
+ // hint row itself shrinks the viewport and keeps re-introducing the residue.
69
+ //
70
+ // Downward intents only: with the correct comparison, snapping on the way up
71
+ // would turn `lineUp` from a pinned viewport into a no-op.
72
+ if (isDownward(intent) && clamped <= BOTTOM_SNAP_TOLERANCE) {
73
+ return { offset: 0 };
74
+ }
75
+ return { offset: clamped };
76
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Terminal size hook (spec §4.11).
3
+ *
4
+ * Ink recomputes its own layout on resize, but the React layer needs the new
5
+ * `rows` to update `height={frameHeight(rows)}`. Without this hook the frame
6
+ * keeps its old height after the window grows and leaves a dead band at the
7
+ * bottom of the screen.
8
+ */
9
+ export interface TerminalSize {
10
+ rows: number;
11
+ cols: number;
12
+ }
13
+ export declare function useTerminalSize(): TerminalSize;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Terminal size hook (spec §4.11).
3
+ *
4
+ * Ink recomputes its own layout on resize, but the React layer needs the new
5
+ * `rows` to update `height={frameHeight(rows)}`. Without this hook the frame
6
+ * keeps its old height after the window grows and leaves a dead band at the
7
+ * bottom of the screen.
8
+ */
9
+ import { useEffect, useState } from 'react';
10
+ import { useStdout } from 'ink';
11
+ import { FALLBACK_COLS, FALLBACK_ROWS } from './frame.js';
12
+ /** Coalesce the burst of resize events a window drag produces. */
13
+ const RESIZE_THROTTLE_MS = 50;
14
+ function readSize(stdout) {
15
+ const rows = stdout?.rows;
16
+ const cols = stdout?.columns;
17
+ return {
18
+ rows: rows && rows > 0 ? rows : FALLBACK_ROWS,
19
+ cols: cols && cols > 0 ? cols : FALLBACK_COLS,
20
+ };
21
+ }
22
+ export function useTerminalSize() {
23
+ const { stdout } = useStdout();
24
+ const [size, setSize] = useState(() => readSize(stdout));
25
+ useEffect(() => {
26
+ const stream = stdout;
27
+ if (!stream || typeof stream.on !== 'function')
28
+ return undefined;
29
+ let timer = null;
30
+ const onResize = () => {
31
+ if (timer)
32
+ clearTimeout(timer);
33
+ timer = setTimeout(() => {
34
+ timer = null;
35
+ const next = readSize(stream);
36
+ setSize((prev) => (prev.rows === next.rows && prev.cols === next.cols ? prev : next));
37
+ }, RESIZE_THROTTLE_MS);
38
+ };
39
+ stream.on('resize', onResize);
40
+ return () => {
41
+ if (timer)
42
+ clearTimeout(timer);
43
+ if (typeof stream.off === 'function')
44
+ stream.off('resize', onResize);
45
+ else if (typeof stream.removeListener === 'function')
46
+ stream.removeListener('resize', onResize);
47
+ };
48
+ }, [stdout]);
49
+ return size;
50
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Markdown parsing layer (spec §4.7). Pure and React-free so the grammar can be
3
+ * unit-tested without rendering anything.
4
+ *
5
+ * Scope is deliberately small — this is not CommonMark. It covers what a coding
6
+ * agent's answers actually contain: fenced code, headings, lists, quotes, rules,
7
+ * tables, and inline emphasis / code / strikethrough.
8
+ */
9
+ /** Heading levels are rendered with three distinct weights, not one (§4.7). */
10
+ export interface HeadingBlock {
11
+ kind: 'heading';
12
+ level: 1 | 2 | 3 | 4 | 5 | 6;
13
+ text: string;
14
+ }
15
+ export interface RuleBlock {
16
+ kind: 'rule';
17
+ }
18
+ export interface TableBlock {
19
+ kind: 'table';
20
+ /** Already column-aligned; render as plain monospace rows. */
21
+ rows: string[][];
22
+ align: ('left' | 'right' | 'center')[];
23
+ }
24
+ export declare function parseHeading(line: string): HeadingBlock | null;
25
+ export declare function isRule(line: string): boolean;
26
+ /** Split one `| a | b |` row into trimmed cells. Returns null if not a row. */
27
+ export declare function parseTableRow(line: string): string[] | null;
28
+ /** Is this the `|---|:--:|` divider that makes the preceding row a header? */
29
+ export declare function isTableDivider(line: string): boolean;
30
+ export declare function tableAlignments(dividerLine: string): TableBlock['align'];
31
+ /**
32
+ * Pad a parsed table into fixed-width columns. Returns one string per row —
33
+ * no borders, because a bordered table inside an already-indented gutter wraps
34
+ * on any realistic terminal.
35
+ */
36
+ export declare function formatTable(rows: string[][], align: TableBlock['align']): string[];
37
+ export type InlineSpan = {
38
+ kind: 'text';
39
+ text: string;
40
+ } | {
41
+ kind: 'code';
42
+ text: string;
43
+ } | {
44
+ kind: 'bold';
45
+ text: string;
46
+ } | {
47
+ kind: 'italic';
48
+ text: string;
49
+ } | {
50
+ kind: 'strike';
51
+ text: string;
52
+ };
53
+ /**
54
+ * Two passes, and that ordering is the fix (§4.7).
55
+ *
56
+ * The old single regex alternated `code | bold | italic` at the same level, so
57
+ * `**bold with `code` inside**` matched neither branch cleanly and rendered as
58
+ * literal asterisks. Splitting code out FIRST — and never looking for emphasis
59
+ * inside a code span — resolves both cases: emphasis around code now works, and
60
+ * asterisks inside code stay literal, which is what a shell snippet needs.
61
+ */
62
+ export declare function parseInline(text: string): InlineSpan[];
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Markdown parsing layer (spec §4.7). Pure and React-free so the grammar can be
3
+ * unit-tested without rendering anything.
4
+ *
5
+ * Scope is deliberately small — this is not CommonMark. It covers what a coding
6
+ * agent's answers actually contain: fenced code, headings, lists, quotes, rules,
7
+ * tables, and inline emphasis / code / strikethrough.
8
+ */
9
+ const HEADING_RE = /^(#{1,6})\s+(.*)$/;
10
+ /** `---`, `***` or `___`, three or more, nothing else on the line. */
11
+ const RULE_RE = /^(?:-{3,}|\*{3,}|_{3,})$/;
12
+ const TABLE_ROW_RE = /^\s*\|(.+)\|\s*$/;
13
+ const TABLE_DIVIDER_CELL_RE = /^:?-{1,}:?$/;
14
+ export function parseHeading(line) {
15
+ const m = HEADING_RE.exec(line);
16
+ if (!m)
17
+ return null;
18
+ return { kind: 'heading', level: m[1].length, text: m[2] ?? '' };
19
+ }
20
+ export function isRule(line) {
21
+ return RULE_RE.test(line.trim());
22
+ }
23
+ /** Split one `| a | b |` row into trimmed cells. Returns null if not a row. */
24
+ export function parseTableRow(line) {
25
+ const m = TABLE_ROW_RE.exec(line);
26
+ if (!m)
27
+ return null;
28
+ return m[1].split('|').map((c) => c.trim());
29
+ }
30
+ /** Is this the `|---|:--:|` divider that makes the preceding row a header? */
31
+ export function isTableDivider(line) {
32
+ const cells = parseTableRow(line);
33
+ if (!cells || cells.length === 0)
34
+ return false;
35
+ return cells.every((c) => TABLE_DIVIDER_CELL_RE.test(c));
36
+ }
37
+ export function tableAlignments(dividerLine) {
38
+ const cells = parseTableRow(dividerLine) ?? [];
39
+ return cells.map((c) => {
40
+ const left = c.startsWith(':');
41
+ const right = c.endsWith(':');
42
+ if (left && right)
43
+ return 'center';
44
+ if (right)
45
+ return 'right';
46
+ return 'left';
47
+ });
48
+ }
49
+ /**
50
+ * Pad a parsed table into fixed-width columns. Returns one string per row —
51
+ * no borders, because a bordered table inside an already-indented gutter wraps
52
+ * on any realistic terminal.
53
+ */
54
+ export function formatTable(rows, align) {
55
+ const colCount = Math.max(...rows.map((r) => r.length), 0);
56
+ const widths = [];
57
+ for (let c = 0; c < colCount; c += 1) {
58
+ widths[c] = Math.max(...rows.map((r) => (r[c] ?? '').length), 0);
59
+ }
60
+ return rows.map((row) => row
61
+ .map((cell, c) => {
62
+ const w = widths[c] ?? 0;
63
+ const a = align[c] ?? 'left';
64
+ if (a === 'right')
65
+ return cell.padStart(w);
66
+ if (a === 'center') {
67
+ const total = w - cell.length;
68
+ const left = Math.floor(total / 2);
69
+ return ' '.repeat(left) + cell + ' '.repeat(total - left);
70
+ }
71
+ return cell.padEnd(w);
72
+ })
73
+ .join(' ')
74
+ .trimEnd());
75
+ }
76
+ const CODE_RE = /`[^`]+`/g;
77
+ const EMPHASIS_RE = /(\*\*[^*]+\*\*|~~[^~]+~~|\*[^*]+\*)/g;
78
+ /**
79
+ * Two passes, and that ordering is the fix (§4.7).
80
+ *
81
+ * The old single regex alternated `code | bold | italic` at the same level, so
82
+ * `**bold with `code` inside**` matched neither branch cleanly and rendered as
83
+ * literal asterisks. Splitting code out FIRST — and never looking for emphasis
84
+ * inside a code span — resolves both cases: emphasis around code now works, and
85
+ * asterisks inside code stay literal, which is what a shell snippet needs.
86
+ */
87
+ export function parseInline(text) {
88
+ const spans = [];
89
+ let last = 0;
90
+ let m;
91
+ CODE_RE.lastIndex = 0;
92
+ while ((m = CODE_RE.exec(text)) !== null) {
93
+ if (m.index > last)
94
+ pushEmphasis(spans, text.slice(last, m.index));
95
+ spans.push({ kind: 'code', text: m[0].slice(1, -1) });
96
+ last = m.index + m[0].length;
97
+ }
98
+ if (last < text.length)
99
+ pushEmphasis(spans, text.slice(last));
100
+ return spans;
101
+ }
102
+ function pushEmphasis(out, text) {
103
+ let last = 0;
104
+ let m;
105
+ EMPHASIS_RE.lastIndex = 0;
106
+ while ((m = EMPHASIS_RE.exec(text)) !== null) {
107
+ if (m.index > last)
108
+ out.push({ kind: 'text', text: text.slice(last, m.index) });
109
+ const token = m[0];
110
+ if (token.startsWith('**'))
111
+ out.push({ kind: 'bold', text: token.slice(2, -2) });
112
+ else if (token.startsWith('~~'))
113
+ out.push({ kind: 'strike', text: token.slice(2, -2) });
114
+ else
115
+ out.push({ kind: 'italic', text: token.slice(1, -1) });
116
+ last = m.index + token.length;
117
+ }
118
+ if (last < text.length)
119
+ out.push({ kind: 'text', text: text.slice(last) });
120
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Confirm dialog — Yes/No gate for `confirmTools` mode. Resolves the pending
3
+ * tool-execution promise (spec §3.6). `y` = approve; `n`/Enter/Esc = reject
4
+ * (Enter follows the safe default shown by the capital N in the "(y/N)" prompt,
5
+ * since this gates mutating tools like write_file/edit_file/bash).
6
+ */
7
+ import React from 'react';
8
+ import type { Theme } from '../theme.js';
9
+ import type { TermCapabilities } from '../capabilities.js';
10
+ export interface ConfirmState {
11
+ summary: string;
12
+ resolve: (approved: boolean) => void;
13
+ }
14
+ interface ConfirmDialogProps {
15
+ state: ConfirmState;
16
+ maxRows: number;
17
+ cols: number;
18
+ theme: Theme;
19
+ caps: TermCapabilities;
20
+ onClose: () => void;
21
+ }
22
+ /**
23
+ * Self-managed (mode B) and deliberately without a position indicator: five
24
+ * rows do not need scrolling, and `state.summary` may itself be multi-line, so
25
+ * element slicing would be meaningless here. The frame is used purely so the
26
+ * title and border match the other three overlays; the existing `useInput` is
27
+ * untouched.
28
+ */
29
+ export declare function ConfirmDialog({ state, maxRows, cols, theme, caps, onClose, }: ConfirmDialogProps): React.ReactElement;
30
+ export {};
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text, useInput } from 'ink';
3
+ import { pickGlyphs } from '../glyphs.js';
4
+ import { OverlayFrame } from '../layout/OverlayFrame.js';
5
+ /**
6
+ * Self-managed (mode B) and deliberately without a position indicator: five
7
+ * rows do not need scrolling, and `state.summary` may itself be multi-line, so
8
+ * element slicing would be meaningless here. The frame is used purely so the
9
+ * title and border match the other three overlays; the existing `useInput` is
10
+ * untouched.
11
+ */
12
+ export function ConfirmDialog({ state, maxRows, cols, theme, caps, onClose, }) {
13
+ const glyphs = pickGlyphs(caps);
14
+ useInput((input, key) => {
15
+ if (input === 'y' || input === 'Y') {
16
+ state.resolve(true);
17
+ onClose();
18
+ }
19
+ else if (input === 'n' || input === 'N' || key.return || key.escape) {
20
+ state.resolve(false);
21
+ onClose();
22
+ }
23
+ });
24
+ return (_jsxs(OverlayFrame, { title: "Confirm action", hint: `y approve ${glyphs.midDot} n / Enter / Esc reject`, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: [_jsx(Text, { color: theme.assistant, children: state.summary }), _jsx(Text, { color: theme.muted, children: "Proceed? (y/N)" })] }));
25
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Help overlay — keybindings and slash-command reference (spec §5.2 / §5.3).
3
+ *
4
+ * Emits a flat array of ONE-ROW elements for `OverlayFrame`'s controlled mode
5
+ * rather than a nested tree. This content is 29 rows tall against a 15-row
6
+ * viewport on a 30-row terminal; before v0.4.0 everything past `/clear` was
7
+ * clipped by `overflow: hidden` with no scrollbar, no indicator, and no way to
8
+ * reach it. Slicing is done by element, so every row must be exactly one row —
9
+ * hence the unconditional `wrap="truncate"`: the first keybinding description
10
+ * alone is 100 characters with its padding and would wrap on any normal
11
+ * terminal, silently desynchronising the position indicator (R-8).
12
+ */
13
+ import React from 'react';
14
+ import type { Theme } from '../theme.js';
15
+ import type { TermCapabilities } from '../capabilities.js';
16
+ /** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
17
+ export declare function helpRows(theme: Theme, caps: TermCapabilities): React.ReactElement[];
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { pickGlyphs } from '../glyphs.js';
4
+ const KEY_PAD = 24;
5
+ function keyRows(times) {
6
+ return [
7
+ ['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
8
+ ['Alt+Enter / Shift+Enter', 'Insert newline'],
9
+ ['Esc', 'Abort run / close overlay'],
10
+ [`Ctrl+C ${times}2`, 'Exit'],
11
+ ['Ctrl+L', 'Redraw the frame'],
12
+ ['Ctrl+T', 'Toggle thinking blocks'],
13
+ ['PgUp / PgDn', 'Scroll a page (transcript, or this overlay)'],
14
+ ['Shift+Up / Shift+Down', 'Scroll the transcript a line (full-screen mode)'],
15
+ ['Up / Down', 'Prompt history (empty input)'],
16
+ ];
17
+ }
18
+ const COMMANDS = [
19
+ ['/help', 'Show this help'],
20
+ ['/model', 'Open the model picker'],
21
+ ['/settings', 'Open the settings screen'],
22
+ ['/theme <name>', 'auto | warm | cool | light'],
23
+ ['/thinking <level>', 'Set thinking level'],
24
+ ['/tools', 'List active tools'],
25
+ ['/clear', 'Clear the visible transcript'],
26
+ ['/reset', 'New conversation'],
27
+ ['/cwd [dir]', 'Show or change the tool working directory'],
28
+ ['/save [file]', 'Save the session to JSON'],
29
+ ['/resume [file]', 'Load a saved session'],
30
+ ['/copy', 'Copy the last answer to the clipboard'],
31
+ ['/exit', 'Exit'],
32
+ ];
33
+ const SKILL_ROWS = [
34
+ ['/skills', 'List installed skills'],
35
+ ['/skills info <name>', 'Show a skill\'s source, files and status'],
36
+ ['/skills install <src>', 'Install from a dir, git repo, or https URL'],
37
+ ['/skills enable|disable', 'Turn a skill on or off'],
38
+ ['/skills policy [mode]', 'Tool ceiling from allowed-tools: off | warn | enforce'],
39
+ ['/skills unload', 'Drop the tool ceiling (does not reclaim context)'],
40
+ ['/skills usage [--reset]', 'Show or delete the local skill use counters'],
41
+ ['/<skill-name> [args]', 'Run a skill directly ($ARGUMENTS / $1..$9)'],
42
+ ['skill / skill_install', 'Tools the model uses to load and install skills'],
43
+ ];
44
+ /** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
45
+ export function helpRows(theme, caps) {
46
+ const glyphs = pickGlyphs(caps);
47
+ const rows = [];
48
+ const section = (title) => (_jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: title }, `s-${title}`));
49
+ const pair = (k, d) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.primary, children: k.padEnd(KEY_PAD) }), _jsx(Text, { color: theme.muted, children: d })] }, `r-${k}`));
50
+ rows.push(section('Keybindings'));
51
+ for (const [k, d] of keyRows(glyphs.times))
52
+ rows.push(pair(k, d));
53
+ rows.push(section('Slash commands'));
54
+ for (const [c, d] of COMMANDS)
55
+ rows.push(pair(c, d));
56
+ rows.push(section('Skills'));
57
+ for (const [c, d] of SKILL_ROWS)
58
+ rows.push(pair(c, d));
59
+ return rows;
60
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Model picker overlay. Sources its provider set from the adapter-backed
3
+ * providers only (R5) so it never offers a model whose provider would fail at
4
+ * run with `Unknown LLM provider`. Lists each provider's builtin models.
5
+ *
6
+ * Uses `OverlayFrame`'s SELF-MANAGED mode (§4.4 / R-12). `ink-select-input`
7
+ * windows itself via `limit` and registers its own `useInput` with `isFocused`
8
+ * defaulting to true; Ink delivers every key to every mounted handler, so a
9
+ * frame that also claimed the arrow keys would move the selection twice per
10
+ * press. Slicing it by element would be worse still — it is one component, not
11
+ * a list of rows. So the frame draws chrome only, and `App` registers no arrow
12
+ * keys while this overlay is open.
13
+ */
14
+ import React from 'react';
15
+ import type { ModelRegistry } from '@aragon-agent/core';
16
+ import type { Theme } from '../theme.js';
17
+ import type { TermCapabilities } from '../capabilities.js';
18
+ interface ModelPickerProps {
19
+ registry: ModelRegistry;
20
+ currentProvider: string;
21
+ currentModel: string;
22
+ maxRows: number;
23
+ cols: number;
24
+ theme: Theme;
25
+ caps: TermCapabilities;
26
+ onSelect: (provider: string, model: string) => void;
27
+ }
28
+ export declare function ModelPicker({ registry, currentProvider, currentModel, maxRows, cols, theme, caps, onSelect, }: ModelPickerProps): React.ReactElement;
29
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import SelectInput from 'ink-select-input';
3
+ import { ADAPTER_PROVIDERS } from '../../config/schema.js';
4
+ import { pickGlyphs } from '../glyphs.js';
5
+ import { OverlayFrame } from '../layout/OverlayFrame.js';
6
+ import { overlayListLimit } from '../layout/overlay-window.js';
7
+ export function ModelPicker({ registry, currentProvider, currentModel, maxRows, cols, theme, caps, onSelect, }) {
8
+ const glyphs = pickGlyphs(caps);
9
+ const items = [];
10
+ const seen = new Set();
11
+ for (const provider of ADAPTER_PROVIDERS) {
12
+ for (const model of registry.getModels(provider)) {
13
+ const value = `${provider}:${model.id}`;
14
+ if (seen.has(value))
15
+ continue;
16
+ seen.add(value);
17
+ const current = provider === currentProvider && model.id === currentModel ? ` ${glyphs.check}` : '';
18
+ items.push({
19
+ label: `${provider} ${glyphs.midDot} ${model.name} (${model.id})${current}`,
20
+ value,
21
+ });
22
+ }
23
+ }
24
+ const initialIndex = Math.max(0, items.findIndex((it) => it.value === `${currentProvider}:${currentModel}`));
25
+ return (_jsx(OverlayFrame, { title: "Select a model", hint: `Enter choose ${glyphs.midDot} Esc cancel`, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: _jsx(SelectInput, { items: items, limit: overlayListLimit(maxRows), initialIndex: initialIndex >= 0 ? initialIndex : 0, onSelect: (item) => {
26
+ const [provider, ...rest] = item.value.split(':');
27
+ onSelect(provider, rest.join(':'));
28
+ } }) }));
29
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Settings screen — edit provider / model / base URL / thinking / max tokens /
3
+ * API key for the active provider (spec §3.5 / §5.2). Up/Down move between
4
+ * fields; Left/Right cycle enum fields; typing edits text/secret fields;
5
+ * Enter saves; Esc (handled by the App) closes without saving.
6
+ *
7
+ * The API key is never printed: it renders as dots while editing and masked
8
+ * otherwise.
9
+ */
10
+ import React from 'react';
11
+ import type { ThinkingLevel } from '@aragon-agent/core';
12
+ import type { Theme } from '../theme.js';
13
+ import type { TermCapabilities } from '../capabilities.js';
14
+ export interface SettingsValues {
15
+ provider: string;
16
+ model: string;
17
+ baseUrl: string;
18
+ thinkingLevel: ThinkingLevel;
19
+ maxTokens: string;
20
+ apiKey: string;
21
+ }
22
+ interface SettingsScreenProps {
23
+ initial: SettingsValues;
24
+ /** Resolved keys per provider so switching providers reloads the right key. */
25
+ apiKeys: Record<string, string | undefined>;
26
+ maxRows: number;
27
+ cols: number;
28
+ /** Owned by `App`, like every other overlay scroll offset. */
29
+ scrollOffset: number;
30
+ /** Clamped offset reported back to `App`; see `OverlayFrame`. */
31
+ onScrollClamp?: (offset: number) => void;
32
+ theme: Theme;
33
+ caps: TermCapabilities;
34
+ onSave: (values: SettingsValues) => void;
35
+ }
36
+ export declare function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, }: SettingsScreenProps): React.ReactElement;
37
+ export {};
@@ -0,0 +1,106 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Settings screen — edit provider / model / base URL / thinking / max tokens /
4
+ * API key for the active provider (spec §3.5 / §5.2). Up/Down move between
5
+ * fields; Left/Right cycle enum fields; typing edits text/secret fields;
6
+ * Enter saves; Esc (handled by the App) closes without saving.
7
+ *
8
+ * The API key is never printed: it renders as dots while editing and masked
9
+ * otherwise.
10
+ */
11
+ import { useState } from 'react';
12
+ import { Text, useInput } from 'ink';
13
+ import { ADAPTER_PROVIDERS, THINKING_LEVELS, maskSecret } from '../../config/schema.js';
14
+ import { pickGlyphs } from '../glyphs.js';
15
+ import { OverlayFrame } from '../layout/OverlayFrame.js';
16
+ const FIELDS = [
17
+ { key: 'provider', label: 'Provider', kind: 'enum', options: ADAPTER_PROVIDERS },
18
+ { key: 'model', label: 'Model', kind: 'text' },
19
+ { key: 'baseUrl', label: 'Base URL', kind: 'text' },
20
+ { key: 'thinkingLevel', label: 'Thinking', kind: 'enum', options: THINKING_LEVELS },
21
+ { key: 'maxTokens', label: 'Max tokens', kind: 'text' },
22
+ { key: 'apiKey', label: 'API key', kind: 'secret' },
23
+ ];
24
+ export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, }) {
25
+ const [values, setValues] = useState(initial);
26
+ const [index, setIndex] = useState(0);
27
+ const glyphs = pickGlyphs(caps);
28
+ const field = FIELDS[index];
29
+ const cycle = (dir) => {
30
+ if (field.kind !== 'enum' || !field.options)
31
+ return;
32
+ const opts = field.options;
33
+ const cur = opts.indexOf(String(values[field.key]));
34
+ const next = (cur + dir + opts.length) % opts.length;
35
+ const nextVal = opts[next];
36
+ setValues((prev) => {
37
+ const updated = { ...prev, [field.key]: nextVal };
38
+ // Clear the key editor on provider switch. Never round-trip a resolved
39
+ // (possibly env-only) key into the editor — saving would then persist a
40
+ // secret the user never typed into the 0600 config file.
41
+ if (field.key === 'provider') {
42
+ updated.apiKey = '';
43
+ }
44
+ return updated;
45
+ });
46
+ };
47
+ useInput((input, key) => {
48
+ if (key.escape)
49
+ return; // App closes the overlay.
50
+ if (key.upArrow) {
51
+ setIndex((i) => (i - 1 + FIELDS.length) % FIELDS.length);
52
+ return;
53
+ }
54
+ if (key.downArrow || key.tab) {
55
+ setIndex((i) => (i + 1) % FIELDS.length);
56
+ return;
57
+ }
58
+ if (key.return) {
59
+ onSave(values);
60
+ return;
61
+ }
62
+ if (field.kind === 'enum') {
63
+ if (key.leftArrow)
64
+ cycle(-1);
65
+ if (key.rightArrow)
66
+ cycle(1);
67
+ return;
68
+ }
69
+ // text / secret editing
70
+ if (key.backspace || key.delete) {
71
+ setValues((prev) => ({ ...prev, [field.key]: String(prev[field.key]).slice(0, -1) }));
72
+ return;
73
+ }
74
+ if (input && !key.ctrl && !key.meta) {
75
+ setValues((prev) => ({ ...prev, [field.key]: String(prev[field.key]) + input }));
76
+ }
77
+ });
78
+ const displayValue = (f, selected) => {
79
+ const raw = String(values[f.key] ?? '');
80
+ if (f.kind === 'secret') {
81
+ // Indicate an already-configured key (env or file) without revealing it or
82
+ // its length — the editor stays empty until the user types a replacement.
83
+ const configured = (apiKeys[values.provider] ?? '').length > 0;
84
+ if (raw.length > 0) {
85
+ return selected
86
+ ? glyphs.maskDot.repeat(Math.min(raw.length, 24))
87
+ : maskSecret(raw, { maskChar: glyphs.maskDot, ellipsis: glyphs.ellipsis });
88
+ }
89
+ if (configured)
90
+ return selected ? '(configured - type to replace)' : '(configured)';
91
+ return selected ? '(type to set)' : '(none)';
92
+ }
93
+ if (f.kind === 'enum')
94
+ return raw;
95
+ return raw.length > 0 ? raw : selected ? '(type to set)' : '(none)';
96
+ };
97
+ // One element per row, each `wrap="truncate"` — the contract for OverlayFrame's
98
+ // controlled mode. The old title alone was 68 characters and wrapped below 70
99
+ // columns, adding a row the frame could not account for (P2-9 / R-8).
100
+ const rows = FIELDS.map((f, i) => {
101
+ const selected = i === index;
102
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: selected ? theme.accent : theme.muted, children: selected ? `${glyphs.caret} ` : ' ' }), _jsx(Text, { color: selected ? theme.assistant : theme.muted, children: (f.key === 'apiKey' ? `API key (${values.provider})` : f.label).padEnd(18) }), _jsx(Text, { color: selected ? theme.primary : theme.muted, children: displayValue(f, selected) }), f.kind === 'enum' && selected && (_jsxs(Text, { color: theme.muted, children: [' (', glyphs.arrowLeft, "/", glyphs.arrowRight, ')'] }))] }, f.key));
103
+ });
104
+ rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: "Enter saves. Keys persist to the config file (0600)." }, "__save"));
105
+ return (_jsx(OverlayFrame, { title: "Settings", hint: `${glyphs.arrowUp}${glyphs.arrowDown} field ${glyphs.midDot} ${glyphs.arrowLeft}${glyphs.arrowRight} change ${glyphs.midDot} Enter save ${glyphs.midDot} Esc close`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: scrollOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
106
+ }