@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.
- package/CHANGELOG.md +292 -0
- package/README.md +558 -0
- package/dist/agent/coalesce.d.ts +14 -0
- package/dist/agent/coalesce.js +41 -0
- package/dist/agent/controller.d.ts +132 -0
- package/dist/agent/controller.js +332 -0
- package/dist/agent/headless.d.ts +22 -0
- package/dist/agent/headless.js +74 -0
- package/dist/agent/reducer.d.ts +181 -0
- package/dist/agent/reducer.js +420 -0
- package/dist/agent/system-prompt.d.ts +30 -0
- package/dist/agent/system-prompt.js +58 -0
- package/dist/agent/usage.d.ts +13 -0
- package/dist/agent/usage.js +41 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +482 -0
- package/dist/commands/builtins.d.ts +16 -0
- package/dist/commands/builtins.js +223 -0
- package/dist/commands/registry.d.ts +48 -0
- package/dist/commands/registry.js +45 -0
- package/dist/commands/skills.d.ts +22 -0
- package/dist/commands/skills.js +625 -0
- package/dist/config/app-paths.d.ts +11 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/env.d.ts +28 -0
- package/dist/config/env.js +92 -0
- package/dist/config/load.d.ts +48 -0
- package/dist/config/load.js +158 -0
- package/dist/config/migrate-legacy-state.d.ts +78 -0
- package/dist/config/migrate-legacy-state.js +181 -0
- package/dist/config/schema.d.ts +249 -0
- package/dist/config/schema.js +247 -0
- package/dist/config/store.d.ts +44 -0
- package/dist/config/store.js +126 -0
- package/dist/input/keymap.d.ts +20 -0
- package/dist/input/keymap.js +85 -0
- package/dist/session/persist.d.ts +24 -0
- package/dist/session/persist.js +51 -0
- package/dist/skills/archive.d.ts +41 -0
- package/dist/skills/archive.js +152 -0
- package/dist/skills/cli-commands.d.ts +52 -0
- package/dist/skills/cli-commands.js +502 -0
- package/dist/skills/fetch-source.d.ts +104 -0
- package/dist/skills/fetch-source.js +448 -0
- package/dist/skills/installer.d.ts +117 -0
- package/dist/skills/installer.js +414 -0
- package/dist/skills/lock.d.ts +75 -0
- package/dist/skills/lock.js +223 -0
- package/dist/skills/manifest.d.ts +47 -0
- package/dist/skills/manifest.js +95 -0
- package/dist/skills/node-host.d.ts +23 -0
- package/dist/skills/node-host.js +59 -0
- package/dist/skills/paths.d.ts +61 -0
- package/dist/skills/paths.js +107 -0
- package/dist/skills/service.d.ts +256 -0
- package/dist/skills/service.js +730 -0
- package/dist/skills/tools.d.ts +35 -0
- package/dist/skills/tools.js +164 -0
- package/dist/skills/updater.d.ts +101 -0
- package/dist/skills/updater.js +330 -0
- package/dist/skills/usage.d.ts +72 -0
- package/dist/skills/usage.js +216 -0
- package/dist/tools/bash-tool.d.ts +12 -0
- package/dist/tools/bash-tool.js +131 -0
- package/dist/tools/diff.d.ts +15 -0
- package/dist/tools/diff.js +50 -0
- package/dist/tools/fs-tools.d.ts +16 -0
- package/dist/tools/fs-tools.js +215 -0
- package/dist/tools/index.d.ts +93 -0
- package/dist/tools/index.js +157 -0
- package/dist/tools/search-tools.d.ts +11 -0
- package/dist/tools/search-tools.js +225 -0
- package/dist/ui/App.d.ts +24 -0
- package/dist/ui/App.js +522 -0
- package/dist/ui/AutocompletePopup.d.ts +21 -0
- package/dist/ui/AutocompletePopup.js +19 -0
- package/dist/ui/Composer.d.ts +40 -0
- package/dist/ui/Composer.js +61 -0
- package/dist/ui/Header.d.ts +30 -0
- package/dist/ui/Header.js +25 -0
- package/dist/ui/Logo.d.ts +50 -0
- package/dist/ui/Logo.js +85 -0
- package/dist/ui/Markdown.d.ts +23 -0
- package/dist/ui/Markdown.js +122 -0
- package/dist/ui/PromptInput.d.ts +81 -0
- package/dist/ui/PromptInput.js +383 -0
- package/dist/ui/SessionOpener.d.ts +30 -0
- package/dist/ui/SessionOpener.js +12 -0
- package/dist/ui/StatusBar.d.ts +39 -0
- package/dist/ui/StatusBar.js +43 -0
- package/dist/ui/ToastStack.d.ts +24 -0
- package/dist/ui/ToastStack.js +38 -0
- package/dist/ui/Transcript.d.ts +62 -0
- package/dist/ui/Transcript.js +128 -0
- package/dist/ui/capabilities.d.ts +25 -0
- package/dist/ui/capabilities.js +59 -0
- package/dist/ui/console-bridge.d.ts +19 -0
- package/dist/ui/console-bridge.js +52 -0
- package/dist/ui/density.d.ts +26 -0
- package/dist/ui/density.js +34 -0
- package/dist/ui/entries/AssistantEntry.d.ts +23 -0
- package/dist/ui/entries/AssistantEntry.js +13 -0
- package/dist/ui/entries/EntryFrame.d.ts +23 -0
- package/dist/ui/entries/EntryFrame.js +6 -0
- package/dist/ui/entries/ToolCard.d.ts +38 -0
- package/dist/ui/entries/ToolCard.js +72 -0
- package/dist/ui/entries/ToolPreview.d.ts +57 -0
- package/dist/ui/entries/ToolPreview.js +144 -0
- package/dist/ui/entries/UserEntry.d.ts +14 -0
- package/dist/ui/entries/UserEntry.js +5 -0
- package/dist/ui/exit-snapshot.d.ts +27 -0
- package/dist/ui/exit-snapshot.js +26 -0
- package/dist/ui/gauge.d.ts +24 -0
- package/dist/ui/gauge.js +36 -0
- package/dist/ui/glyphs.d.ts +87 -0
- package/dist/ui/glyphs.js +127 -0
- package/dist/ui/gradient.d.ts +11 -0
- package/dist/ui/gradient.js +50 -0
- package/dist/ui/layout/AppShell.d.ts +24 -0
- package/dist/ui/layout/AppShell.js +9 -0
- package/dist/ui/layout/Gutter.d.ts +67 -0
- package/dist/ui/layout/Gutter.js +42 -0
- package/dist/ui/layout/OverlayFrame.d.ts +62 -0
- package/dist/ui/layout/OverlayFrame.js +92 -0
- package/dist/ui/layout/ScrollViewport.d.ts +33 -0
- package/dist/ui/layout/ScrollViewport.js +71 -0
- package/dist/ui/layout/budget.d.ts +42 -0
- package/dist/ui/layout/budget.js +47 -0
- package/dist/ui/layout/frame.d.ts +58 -0
- package/dist/ui/layout/frame.js +75 -0
- package/dist/ui/layout/overlay-window.d.ts +43 -0
- package/dist/ui/layout/overlay-window.js +51 -0
- package/dist/ui/layout/scroll.d.ts +28 -0
- package/dist/ui/layout/scroll.js +76 -0
- package/dist/ui/layout/useTerminalSize.d.ts +13 -0
- package/dist/ui/layout/useTerminalSize.js +50 -0
- package/dist/ui/markdown-blocks.d.ts +62 -0
- package/dist/ui/markdown-blocks.js +120 -0
- package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
- package/dist/ui/overlays/ConfirmDialog.js +25 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
- package/dist/ui/overlays/HelpOverlay.js +60 -0
- package/dist/ui/overlays/ModelPicker.d.ts +29 -0
- package/dist/ui/overlays/ModelPicker.js +29 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
- package/dist/ui/overlays/SettingsScreen.js +106 -0
- package/dist/ui/palettes.d.ts +79 -0
- package/dist/ui/palettes.js +115 -0
- package/dist/ui/screen.d.ts +27 -0
- package/dist/ui/screen.js +57 -0
- package/dist/ui/theme.d.ts +78 -0
- package/dist/ui/theme.js +127 -0
- package/dist/ui/transcript-text.d.ts +27 -0
- package/dist/ui/transcript-text.js +110 -0
- package/package.json +73 -0
- 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 {};
|