@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,383 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * PromptInput (spec §3.6 / §3.10) — a controlled multiline input on Ink's
4
+ * `useInput` with: full line editing (Home/End, word jump/delete, kill-line via
5
+ * `input/keymap`), multi-line vertical cursor navigation, prompt-history recall
6
+ * at the edges, a slash-command palette, and `@file` path completion.
7
+ *
8
+ * The buffer/cursor transforms (`applyEdit`, `moveVertical`) and the suggestion
9
+ * computations (`slashSuggestions`, `fileTokenAt`) are pure module functions,
10
+ * exported for `input.test.ts`, so the component itself stays thin.
11
+ */
12
+ import { useEffect, useRef, useState } from 'react';
13
+ import { Box, Text, useInput } from 'ink';
14
+ import { glob as tinyGlob } from 'tinyglobby';
15
+ import { pickGlyphs } from './glyphs.js';
16
+ import { recognize } from '../input/keymap.js';
17
+ import { AutocompletePopup } from './AutocompletePopup.js';
18
+ // ---------------------------------------------------------------------------
19
+ // Pure editing helpers (unit-tested in input.test.ts)
20
+ // ---------------------------------------------------------------------------
21
+ const isSpace = (ch) => !!ch && /\s/.test(ch);
22
+ function lineStart(buffer, cursor) {
23
+ const nl = buffer.lastIndexOf('\n', cursor - 1);
24
+ return nl === -1 ? 0 : nl + 1;
25
+ }
26
+ function lineEnd(buffer, cursor) {
27
+ const nl = buffer.indexOf('\n', cursor);
28
+ return nl === -1 ? buffer.length : nl;
29
+ }
30
+ function prevWord(buffer, cursor) {
31
+ let i = cursor;
32
+ while (i > 0 && isSpace(buffer[i - 1]))
33
+ i -= 1;
34
+ while (i > 0 && !isSpace(buffer[i - 1]))
35
+ i -= 1;
36
+ return i;
37
+ }
38
+ function nextWord(buffer, cursor) {
39
+ let i = cursor;
40
+ while (i < buffer.length && isSpace(buffer[i]))
41
+ i += 1;
42
+ while (i < buffer.length && !isSpace(buffer[i]))
43
+ i += 1;
44
+ return i;
45
+ }
46
+ /** Apply an edit intent as a pure buffer/cursor transform. */
47
+ export function applyEdit(buffer, cursor, intent) {
48
+ switch (intent) {
49
+ case 'home':
50
+ return { buffer, cursor: lineStart(buffer, cursor) };
51
+ case 'end':
52
+ return { buffer, cursor: lineEnd(buffer, cursor) };
53
+ case 'wordLeft':
54
+ return { buffer, cursor: prevWord(buffer, cursor) };
55
+ case 'wordRight':
56
+ return { buffer, cursor: nextWord(buffer, cursor) };
57
+ case 'deleteWordBack': {
58
+ const start = prevWord(buffer, cursor);
59
+ return { buffer: buffer.slice(0, start) + buffer.slice(cursor), cursor: start };
60
+ }
61
+ case 'killToStart': {
62
+ const start = lineStart(buffer, cursor);
63
+ return { buffer: buffer.slice(0, start) + buffer.slice(cursor), cursor: start };
64
+ }
65
+ case 'killToEnd': {
66
+ const end = lineEnd(buffer, cursor);
67
+ return { buffer: buffer.slice(0, cursor) + buffer.slice(end), cursor };
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * Move the cursor up/down one line preserving the column. Returns `null` at the
73
+ * top/bottom edge (or for a single-line buffer) so the caller can fall back to
74
+ * prompt-history recall.
75
+ */
76
+ export function moveVertical(buffer, cursor, dir) {
77
+ const lines = buffer.split('\n');
78
+ if (lines.length < 2)
79
+ return null;
80
+ const before = buffer.slice(0, cursor);
81
+ const idx = before.split('\n').length - 1;
82
+ const col = before.length - (before.lastIndexOf('\n') + 1);
83
+ const target = dir === 'up' ? idx - 1 : idx + 1;
84
+ if (target < 0 || target >= lines.length)
85
+ return null;
86
+ let start = 0;
87
+ for (let i = 0; i < target; i += 1)
88
+ start += lines[i].length + 1;
89
+ return { cursor: start + Math.min(col, lines[target].length) };
90
+ }
91
+ /**
92
+ * Slash-command suggestions when the buffer is a bare `/word` (no space yet).
93
+ *
94
+ * The character class covers `-` and `:` as well as `\w` (D16). Skill names are
95
+ * kebab-case and the namespaced fallback is `/skill:<name>`; with the original
96
+ * `^\/(\w*)$` a user typing `/my-sk` got NO popup at all — not an error message,
97
+ * just silence, which reads as "that command does not exist".
98
+ */
99
+ export function slashSuggestions(buffer, commands) {
100
+ const m = /^\/([\w:-]*)$/.exec(buffer);
101
+ if (!m)
102
+ return null;
103
+ const q = m[1].toLowerCase();
104
+ return commands
105
+ .filter((c) => c.name.toLowerCase().startsWith(q))
106
+ .map((c) => ({ label: `/${c.name}`, hint: c.description }));
107
+ }
108
+ /** The `@file` token under the cursor (`@` + non-space run), if any. */
109
+ export function fileTokenAt(buffer, cursor) {
110
+ let start = cursor;
111
+ while (start > 0 && !isSpace(buffer[start - 1]))
112
+ start -= 1;
113
+ if (buffer[start] !== '@')
114
+ return null;
115
+ let end = cursor;
116
+ while (end < buffer.length && !isSpace(buffer[end]))
117
+ end += 1;
118
+ return { start, end, query: buffer.slice(start + 1, end) };
119
+ }
120
+ /** A leading C0 control byte or ESC sequence should never be inserted raw. */
121
+ function isControlSeq(input) {
122
+ if (input.length === 0)
123
+ return true;
124
+ const code = input.charCodeAt(0);
125
+ return code < 0x20 || code === 0x7f;
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Component
129
+ // ---------------------------------------------------------------------------
130
+ export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, onSubmit, onHelp, bordered, onDraftChange, }) {
131
+ const [buffer, setBuffer] = useState('');
132
+ const [cursor, setCursor] = useState(0);
133
+ const [historyIndex, setHistoryIndex] = useState(null);
134
+ const [dismissed, setDismissed] = useState(false);
135
+ const [sel, setSel] = useState(0);
136
+ const [fileMatches, setFileMatches] = useState([]);
137
+ // --- Derived suggestion state (shared by render + input handler). --------
138
+ const slashSug = dismissed ? null : slashSuggestions(buffer, commands);
139
+ const fileTok = fileTokenAt(buffer, cursor);
140
+ const fileQuery = fileTok ? fileTok.query : null;
141
+ const fileActive = !dismissed && !!fileTok && fileTok.query.length >= 1 && fileMatches.length > 0;
142
+ const popupKind = slashSug && slashSug.length > 0 ? 'slash' : fileActive ? 'file' : null;
143
+ const popupItems = popupKind === 'slash'
144
+ ? slashSug
145
+ : popupKind === 'file'
146
+ ? fileMatches.map((p) => ({ label: p }))
147
+ : [];
148
+ const clampedSel = popupItems.length > 0 ? Math.min(sel, popupItems.length - 1) : 0;
149
+ // --- Debounced `@file` scan. ---------------------------------------------
150
+ useEffect(() => {
151
+ if (fileQuery === null || fileQuery.length < 1) {
152
+ setFileMatches([]);
153
+ return;
154
+ }
155
+ let cancelled = false;
156
+ const id = setTimeout(() => {
157
+ tinyGlob('**/*', {
158
+ cwd,
159
+ dot: false,
160
+ onlyFiles: true,
161
+ ignore: ['**/node_modules/**', '**/.git/**'],
162
+ })
163
+ .then((matches) => {
164
+ if (cancelled)
165
+ return;
166
+ const q = fileQuery.toLowerCase();
167
+ setFileMatches(matches.filter((m) => m.toLowerCase().includes(q)).slice(0, 20));
168
+ })
169
+ .catch(() => {
170
+ if (!cancelled)
171
+ setFileMatches([]);
172
+ });
173
+ }, 120);
174
+ return () => {
175
+ cancelled = true;
176
+ clearTimeout(id);
177
+ };
178
+ }, [fileQuery, cwd]);
179
+ // --- Draft presence, reported only on the transition. ---------------------
180
+ // Notifying on every keystroke would re-render the whole App tree per key.
181
+ const hasDraft = buffer.length > 0;
182
+ const lastReportedDraft = useRef(null);
183
+ useEffect(() => {
184
+ if (!onDraftChange)
185
+ return;
186
+ if (lastReportedDraft.current === hasDraft)
187
+ return;
188
+ lastReportedDraft.current = hasDraft;
189
+ onDraftChange(hasDraft);
190
+ }, [hasDraft, onDraftChange]);
191
+ // --- Mutators. -----------------------------------------------------------
192
+ const resetDraftFlags = () => {
193
+ setHistoryIndex(null);
194
+ setDismissed(false);
195
+ setSel(0);
196
+ };
197
+ const insert = (text) => {
198
+ setBuffer(buffer.slice(0, cursor) + text + buffer.slice(cursor));
199
+ setCursor(cursor + text.length);
200
+ resetDraftFlags();
201
+ };
202
+ const submit = (text) => {
203
+ onSubmit(text);
204
+ setBuffer('');
205
+ setCursor(0);
206
+ resetDraftFlags();
207
+ };
208
+ const recallUp = () => {
209
+ if (history.length === 0)
210
+ return;
211
+ const idx = historyIndex === null ? history.length - 1 : Math.max(0, historyIndex - 1);
212
+ const value = history[idx] ?? '';
213
+ setHistoryIndex(idx);
214
+ setBuffer(value);
215
+ setCursor(value.length);
216
+ };
217
+ const recallDown = () => {
218
+ if (historyIndex === null)
219
+ return;
220
+ const idx = historyIndex + 1;
221
+ if (idx >= history.length) {
222
+ setHistoryIndex(null);
223
+ setBuffer('');
224
+ setCursor(0);
225
+ }
226
+ else {
227
+ const value = history[idx] ?? '';
228
+ setHistoryIndex(idx);
229
+ setBuffer(value);
230
+ setCursor(value.length);
231
+ }
232
+ };
233
+ const completeSelection = () => {
234
+ const chosen = popupItems[clampedSel];
235
+ if (!chosen)
236
+ return;
237
+ if (popupKind === 'slash') {
238
+ const next = `${chosen.label} `;
239
+ setBuffer(next);
240
+ setCursor(next.length);
241
+ }
242
+ else if (popupKind === 'file' && fileTok) {
243
+ const next = buffer.slice(0, fileTok.start) + chosen.label + buffer.slice(fileTok.end);
244
+ setBuffer(next);
245
+ setCursor(fileTok.start + chosen.label.length);
246
+ }
247
+ resetDraftFlags();
248
+ };
249
+ const verticalOrHistory = (dir) => {
250
+ const moved = moveVertical(buffer, cursor, dir);
251
+ if (moved) {
252
+ setCursor(moved.cursor);
253
+ return;
254
+ }
255
+ // At an edge: recall history only from an empty buffer or while already
256
+ // stepping through it — never clobber an in-progress draft (see spec note).
257
+ if (buffer.length === 0 || historyIndex !== null) {
258
+ if (dir === 'up')
259
+ recallUp();
260
+ else
261
+ recallDown();
262
+ }
263
+ };
264
+ // --- Input handling. -----------------------------------------------------
265
+ useInput((input, key) => {
266
+ // Popup navigation owns Up/Down/Tab/→/Enter/Esc while it is open.
267
+ if (popupItems.length > 0) {
268
+ if (key.upArrow) {
269
+ setSel(Math.max(0, clampedSel - 1));
270
+ return;
271
+ }
272
+ if (key.downArrow) {
273
+ setSel(Math.min(popupItems.length - 1, clampedSel + 1));
274
+ return;
275
+ }
276
+ if (key.tab || (key.rightArrow && cursor >= buffer.length)) {
277
+ completeSelection();
278
+ return;
279
+ }
280
+ if (key.return && popupKind === 'slash') {
281
+ const label = popupItems[clampedSel]?.label;
282
+ if (label)
283
+ submit(label);
284
+ return;
285
+ }
286
+ if (key.escape) {
287
+ setDismissed(true);
288
+ return;
289
+ }
290
+ // Any other key falls through so typing keeps filtering the popup.
291
+ }
292
+ if (key.escape)
293
+ return; // App owns abort / close-overlay.
294
+ // Line-editing intents (Home/End/word/kill) recognized from raw keys.
295
+ const intent = recognize(input, key);
296
+ if (intent) {
297
+ const next = applyEdit(buffer, cursor, intent);
298
+ setBuffer(next.buffer);
299
+ setCursor(next.cursor);
300
+ resetDraftFlags();
301
+ return;
302
+ }
303
+ if (key.ctrl)
304
+ return; // App owns Ctrl+C / L / T / O.
305
+ if (key.return) {
306
+ if (key.meta || key.shift) {
307
+ insert('\n');
308
+ return;
309
+ }
310
+ if (buffer.trim().length === 0)
311
+ return;
312
+ submit(buffer);
313
+ return;
314
+ }
315
+ if (key.backspace || key.delete) {
316
+ if (cursor > 0) {
317
+ setBuffer(buffer.slice(0, cursor - 1) + buffer.slice(cursor));
318
+ setCursor(cursor - 1);
319
+ resetDraftFlags();
320
+ }
321
+ return;
322
+ }
323
+ if (key.leftArrow) {
324
+ setCursor(Math.max(0, cursor - 1));
325
+ return;
326
+ }
327
+ if (key.rightArrow) {
328
+ setCursor(Math.min(buffer.length, cursor + 1));
329
+ return;
330
+ }
331
+ // `!key.shift` is load-bearing: Shift+↑/↓ is the viewport's line-scroll
332
+ // binding (§4.10). Without the guard the history recall below swallows it
333
+ // and scrolling appears to be broken for no visible reason.
334
+ if (key.upArrow && !key.shift) {
335
+ verticalOrHistory('up');
336
+ return;
337
+ }
338
+ if (key.downArrow && !key.shift) {
339
+ verticalOrHistory('down');
340
+ return;
341
+ }
342
+ // `?` on an empty idle input opens the help overlay.
343
+ if (input === '?' && buffer.length === 0 && !running && onHelp) {
344
+ onHelp();
345
+ return;
346
+ }
347
+ // Printable input — drop any unrecognized control/escape byte.
348
+ if (input && !key.tab && !isControlSeq(input))
349
+ insert(input);
350
+ }, { isActive });
351
+ const glyphs = pickGlyphs(caps);
352
+ const marker = running ? glyphs.steer : glyphs.caret;
353
+ const markerColor = running ? theme.toolRunning : theme.primary;
354
+ const placeholder = running
355
+ ? `Type to steer the run, Esc to abort${glyphs.ellipsis}`
356
+ : `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
357
+ const lines = renderWithCursor(buffer, cursor, isActive);
358
+ const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", children: buffer.length === 0 ? (_jsx(Text, { color: theme.muted, children: placeholder })) : (lines.map((line, i) => _jsx(Text, { children: line }, i))) })] }));
359
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupItems.length > 0 && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows })), bordered ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: bordered.color, paddingX: 1, children: inputRow })) : (inputRow)] }));
360
+ }
361
+ /** Split the buffer into lines and render an inverse cursor at `cursor`. */
362
+ function renderWithCursor(buffer, cursor, active) {
363
+ if (!active)
364
+ return buffer.split('\n');
365
+ const before = buffer.slice(0, cursor);
366
+ const at = buffer[cursor] ?? ' ';
367
+ const after = buffer.slice(cursor + 1);
368
+ const beforeLines = before.split('\n');
369
+ const afterLines = after.split('\n');
370
+ const nodes = [];
371
+ const lastBeforeIdx = beforeLines.length - 1;
372
+ for (let i = 0; i < lastBeforeIdx; i += 1) {
373
+ nodes.push(_jsx(Text, { children: beforeLines[i] }, `b${i}`));
374
+ }
375
+ const cursorChar = at === '\n' ? ' ' : at;
376
+ const firstAfter = afterLines[0] ?? '';
377
+ nodes.push(_jsxs(Text, { children: [beforeLines[lastBeforeIdx], _jsx(Text, { inverse: true, children: cursorChar }), at === '\n' ? '' : firstAfter] }, "cursor-line"));
378
+ const remainingAfter = at === '\n' ? afterLines : afterLines.slice(1);
379
+ for (let i = 0; i < remainingAfter.length; i += 1) {
380
+ nodes.push(_jsx(Text, { children: remainingAfter[i] }, `a${i}`));
381
+ }
382
+ return nodes;
383
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * SessionOpener (spec §4.2 / §4.8) — the opening wordmark plus the getting
3
+ * started card, rendered as the FIRST BLOCK OF VIEWPORT CONTENT rather than as
4
+ * part of the fixed header.
5
+ *
6
+ * That relocation is the whole point. As header chrome the wordmark occupied
7
+ * permanent vertical budget for what is a one-off greeting, so it had to be
8
+ * yanked away the instant the first message arrived — a 7-row jump and a full
9
+ * reflow. As content it simply scrolls out of view like anything else, which is
10
+ * both calmer and what the reference implementation does.
11
+ *
12
+ * Absorbs the former `Welcome.tsx`, whose 8-row round-bordered card spent 4 rows
13
+ * on a border and two blank lines — over half of a short terminal's empty-session
14
+ * viewport carrying no information.
15
+ */
16
+ import React from 'react';
17
+ import type { Theme } from './theme.js';
18
+ import type { TermCapabilities } from './capabilities.js';
19
+ import { type OpenerVariant } from './Logo.js';
20
+ export interface SessionOpenerProps {
21
+ variant: OpenerVariant;
22
+ version: string;
23
+ cwd: string;
24
+ hasKey: boolean;
25
+ /** Drives the last-ditch squeeze to a single line on very short terminals. */
26
+ viewportRows: number;
27
+ theme: Theme;
28
+ caps: TermCapabilities;
29
+ }
30
+ export declare function SessionOpener({ variant, version, cwd, hasKey, viewportRows, theme, caps, }: SessionOpenerProps): React.ReactElement;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { pickGlyphs } from './glyphs.js';
4
+ import { Logo } from './Logo.js';
5
+ const EXAMPLE = 'Explain what this repository does';
6
+ /** Below this the opener keeps only the safety warning. */
7
+ const TERSE_VIEWPORT_ROWS = 10;
8
+ export function SessionOpener({ variant, version, cwd, hasKey, viewportRows, theme, caps, }) {
9
+ const glyphs = pickGlyphs(caps);
10
+ const terse = viewportRows < TERSE_VIEWPORT_ROWS;
11
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [!terse && variant !== 'none' && _jsx(Logo, { variant: variant, theme: theme, caps: caps }), !terse && (_jsx(Text, { wrap: "truncate", children: _jsxs(Text, { color: theme.muted, children: ["v", version, ' ', cwd] }) })), _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Full permission, no sandbox: bash runs and files are written directly."] }), !hasKey && (_jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.arrowRight, " Run /settings (or set ANTHROPIC_API_KEY) to get started."] })), !terse && hasKey && (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: theme.accent, children: [glyphs.bullet, " "] }), _jsx(Text, { color: theme.hintFg ?? theme.muted, children: EXAMPLE })] }))] }));
12
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Status bar (spec §4.8). Width-aware, borderless, EXACTLY one row: a left
3
+ * cluster (run status, model, thinking level) and a right cluster (context
4
+ * gauge, tokens, cost, tokens/sec + elapsed while running, off-bottom `↑N`).
5
+ * Clusters drop out gracefully on narrow terminals.
6
+ *
7
+ * Dropping the round border here buys back 3 rows of a 24-row terminal — an
8
+ * eighth of the screen was being spent framing a single line of text.
9
+ */
10
+ import React from 'react';
11
+ import type { Theme } from './theme.js';
12
+ import type { TermCapabilities } from './capabilities.js';
13
+ import type { UsageTotal } from '../agent/reducer.js';
14
+ interface StatusBarProps {
15
+ model: string;
16
+ provider: string;
17
+ usageTotal: UsageTotal;
18
+ contextTokens: number;
19
+ contextWindow: number;
20
+ contextWindowKnown: boolean;
21
+ status: 'idle' | 'running';
22
+ elapsedMs: number;
23
+ thinkingLevel: string;
24
+ tokPerSec: number;
25
+ theme: Theme;
26
+ caps: TermCapabilities;
27
+ /**
28
+ * Lines scrolled off the bottom; > 0 renders `↑N`. Inline mode has no
29
+ * self-drawn viewport, so it simply never sends anything but 0 — the bar
30
+ * needs no render-mode branch of its own.
31
+ */
32
+ scrolledLines?: number;
33
+ /**
34
+ * Ctrl+L redraw carrier — invariant I-5 (spec §4.13). See `redrawChar`.
35
+ */
36
+ redrawNonce?: number;
37
+ }
38
+ export declare function StatusBar(props: StatusBarProps): React.ReactElement;
39
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import { pickGlyphs } from './glyphs.js';
4
+ import { formatCost, formatTokens, formatDuration } from '../agent/usage.js';
5
+ import { buildGauge } from './gauge.js';
6
+ /**
7
+ * I-5 — DO NOT "CLEAN THIS UP". Ink short-circuits identical output at TWO
8
+ * gates (`ink.js:132` and `log-update.js:13`), so a fixed frame whose React
9
+ * output has not changed is never repainted. `Ctrl+L` therefore cannot work by
10
+ * writing an escape sequence; it works by making the rendered STRING differ.
11
+ * This alternates the status bar's left gutter between U+0020 and U+00A0: both
12
+ * render one invisible column, so there is zero visual or layout difference,
13
+ * but the bytes differ and both gates open.
14
+ *
15
+ * It must sit at the START of the line: Ink `trimEnd()`s every row (output.js),
16
+ * and JS `trimEnd` strips U+00A0 too — a trailing carrier would be deleted and
17
+ * Ctrl+L would degrade to a silent no-op.
18
+ */
19
+ /** Built by code point so no formatter can normalize it back to a plain space. */
20
+ const NBSP = String.fromCharCode(0x00a0);
21
+ function redrawChar(nonce) {
22
+ return nonce % 2 === 0 ? ' ' : NBSP;
23
+ }
24
+ export function StatusBar(props) {
25
+ const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, } = props;
26
+ const { stdout } = useStdout();
27
+ const cols = stdout?.columns ?? 80;
28
+ const running = status === 'running';
29
+ const glyphs = pickGlyphs(caps);
30
+ const pct = contextWindow > 0 ? Math.min(100, Math.round((contextTokens / contextWindow) * 100)) : 0;
31
+ const gauge = buildGauge(pct, cols < 72 ? 8 : 12, theme, caps);
32
+ const pctLabel = `${contextWindowKnown ? '' : '~'}${gauge.pct}%`;
33
+ const tokens = `${formatTokens(usageTotal.inputTokens)}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
34
+ const statusGlyph = running ? theme.symbols.toolRunning : theme.symbols.toolPending;
35
+ return (
36
+ // `flexShrink={0}` on the left cluster is not cosmetic. When the two clusters
37
+ // over-subscribe the row, yoga shrinks the flexible children and Ink's text
38
+ // measurement drops a character from EACH of them — the bar renders "idl"
39
+ // and "? hel" rather than truncating cleanly at one end. Pinning the left
40
+ // cluster keeps the run status and model intact and makes the degradation
41
+ // land in one predictable place.
42
+ _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [redrawChar(redrawNonce), statusGlyph, " ", running ? 'running' : 'idle'] }), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
43
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Toast strip (spec §4.8). Ephemeral acks created by App/commands and
3
+ * auto-dismissed by App's TTL effect; the reducer stays timer-free.
4
+ *
5
+ * In `fullscreen` this ALWAYS occupies exactly one row — an empty row when there
6
+ * is nothing to say. Rendering conditionally would make the viewport height (and
7
+ * therefore the whole transcript) jump every time a toast appears or expires.
8
+ * There is deliberately no `rows` threshold: jitter is a layout property, not a
9
+ * space property, and a short terminal suffers it proportionally MORE, so the
10
+ * tier that most needs the fixed row is exactly the one a threshold would skip.
11
+ * Only the newest toast is shown; older ones collapse into a `+N` counter.
12
+ *
13
+ * `inline` keeps the v0.2.0 conditional 0–3 row stack: without a fixed frame,
14
+ * growing and shrinking is just normal document flow.
15
+ */
16
+ import React from 'react';
17
+ import type { Theme } from './theme.js';
18
+ import type { Toast } from '../agent/reducer.js';
19
+ import type { RenderMode } from './layout/frame.js';
20
+ export declare function ToastStack({ toasts, theme, mode, }: {
21
+ toasts: Toast[];
22
+ theme: Theme;
23
+ mode?: RenderMode;
24
+ }): React.ReactElement | null;
@@ -0,0 +1,38 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const MAX_VISIBLE = 3;
4
+ function toastColor(level, theme) {
5
+ switch (level) {
6
+ case 'success':
7
+ return theme.toast.success;
8
+ case 'warn':
9
+ return theme.toast.warn;
10
+ case 'error':
11
+ return theme.toast.error;
12
+ default:
13
+ return theme.toast.info;
14
+ }
15
+ }
16
+ function toastGlyph(level, theme) {
17
+ switch (level) {
18
+ case 'success':
19
+ return theme.symbols.toolDone;
20
+ case 'warn':
21
+ return theme.symbols.warn;
22
+ case 'error':
23
+ return theme.symbols.error;
24
+ default:
25
+ return theme.symbols.info;
26
+ }
27
+ }
28
+ export function ToastStack({ toasts, theme, mode = 'inline', }) {
29
+ if (mode === 'fullscreen') {
30
+ const latest = toasts[toasts.length - 1];
31
+ const extra = Math.max(0, toasts.length - 1);
32
+ return (_jsx(Box, { flexShrink: 0, children: latest ? (_jsxs(Text, { wrap: "truncate", color: toastColor(latest.level, theme), children: [' ', toastGlyph(latest.level, theme), " ", latest.text, extra > 0 ? ` +${extra}` : ''] })) : (_jsx(Text, { children: " " })) }));
33
+ }
34
+ const shown = toasts.slice(-MAX_VISIBLE);
35
+ if (shown.length === 0)
36
+ return null;
37
+ return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: shown.map((t) => (_jsxs(Text, { color: toastColor(t.level, theme), children: [toastGlyph(t.level, theme), " ", t.text] }, t.id))) }));
38
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Transcript (spec §3.8). Splits the entry list into a settled prefix rendered
3
+ * once into Ink's `<Static>` (native terminal scrollback, never re-rendered)
4
+ * and a live tail rendered normally. This kills per-token full re-renders and
5
+ * flicker on long/fast sessions.
6
+ *
7
+ * `computeSettledCount` is pure and exported for `transcript-static.test.ts`.
8
+ * The settled boundary is held MONOTONIC at render time: a previously-settled
9
+ * entry never leaves `<Static>` (which cannot un-print it) even if it later
10
+ * becomes expanded, so the live tail can never duplicate a scrolled-off entry.
11
+ *
12
+ * `EntryView` is shared by both render paths ON PURPOSE (§4.3 / §12). Forking it
13
+ * to freeze inline's v0.2.0 output would mean writing every future entry-layer
14
+ * change twice and watching the two drift; inline is a degradation path, not a
15
+ * parallel product. What inline still guarantees is its GEOMETRY contract — no
16
+ * fixed frame, `<Static>` keeps the settled history, no private ANSI — not its
17
+ * pixel-for-pixel appearance.
18
+ */
19
+ import React from 'react';
20
+ import { type Theme } from './theme.js';
21
+ import type { TermCapabilities } from './capabilities.js';
22
+ import { type DensityMode } from './density.js';
23
+ import type { Entry } from '../agent/reducer.js';
24
+ /**
25
+ * Largest prefix length P such that `entries[0..P)` are all terminal, none is
26
+ * within the last `LIVE_TAIL` entries, and none is currently expanded. Pure and
27
+ * non-decreasing as entries terminalize (expanding a card can lower it — the
28
+ * component clamps with a monotonic high-water mark to avoid Static duplication).
29
+ */
30
+ export declare function computeSettledCount(entries: Entry[], expandedToolIds: Record<string, true>): number;
31
+ interface TranscriptProps {
32
+ entries: Entry[];
33
+ expandedToolIds: Record<string, true>;
34
+ thinkingVisible: boolean;
35
+ reducedMotion: boolean;
36
+ density: DensityMode;
37
+ theme: Theme;
38
+ caps: TermCapabilities;
39
+ }
40
+ /**
41
+ * Full-screen transcript body — the same entry renderers with NO `<Static>`.
42
+ *
43
+ * `<Static>` prints above the live frame; once the frame is `rows - 1` tall
44
+ * there is exactly one visible line up there, so the mechanism stops being a
45
+ * history view and starts being a leak. Full-screen therefore owns its history
46
+ * inside `ScrollViewport` instead.
47
+ *
48
+ * `computeSettledCount` is deliberately NOT called here: its only job is to keep
49
+ * an entry that already reached `<Static>` from flowing back into the live
50
+ * region, and with no Static there is nothing to protect. The settled high-water
51
+ * mark feeds nothing else — `/save` serializes `entries` in full — so leaving it
52
+ * un-advanced has no side effect.
53
+ *
54
+ * Rendering budget (I-3): yoga lays out every child even when `overflow: hidden`
55
+ * clips it, so cost grows linearly with the entry count. Only the last
56
+ * `windowSize` entries are rendered; the reducer and `/save` keep everything.
57
+ */
58
+ export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, windowSize, }: TranscriptProps & {
59
+ windowSize: number;
60
+ }): React.ReactElement;
61
+ export declare function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, }: TranscriptProps): React.ReactElement;
62
+ export {};