@aragon-agent/cli 0.6.3 → 0.6.4
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 +105 -0
- package/README.md +95 -2
- package/dist/agent/controller.d.ts +119 -0
- package/dist/agent/controller.js +275 -1
- package/dist/agent/reducer.d.ts +69 -0
- package/dist/agent/reducer.js +100 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +84 -22
- package/dist/commands/builtins.js +92 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +34 -0
- package/dist/compaction/compactor.js +158 -37
- package/dist/compaction/types.d.ts +36 -0
- package/dist/compaction/wiring.d.ts +9 -0
- package/dist/compaction/wiring.js +22 -1
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +48 -1
- package/dist/config/load.d.ts +11 -0
- package/dist/config/load.js +39 -1
- package/dist/config/schema.d.ts +85 -0
- package/dist/config/schema.js +33 -0
- package/dist/config/store.js +21 -1
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +2 -0
- package/dist/logging/install.js +53 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +36 -1
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/App.d.ts +15 -2
- package/dist/ui/App.js +256 -7
- package/dist/ui/Composer.d.ts +54 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +49 -3
- package/dist/ui/PromptInput.js +184 -52
- package/dist/ui/StatusBar.d.ts +19 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/Transcript.js +29 -1
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- package/dist/ui/layout/virtual-window.js +51 -1
- package/dist/ui/overlays/HelpOverlay.js +3 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.js +14 -1
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +16 -0
- package/dist/ui/screen.js +45 -2
- package/dist/ui/selection/selection-controller.d.ts +1 -1
- package/dist/ui/transcript-text.js +49 -0
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -47
- package/dist/input/stdin-mouse-filter.js +0 -153
package/dist/ui/PromptInput.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* PromptInput (spec §3.6 / §3.10) — a controlled multiline input on Ink's
|
|
4
4
|
* `useInput` with: full line editing (Home/End, word jump/delete, kill-line via
|
|
@@ -25,7 +25,12 @@ import { glob as tinyGlob } from 'tinyglobby';
|
|
|
25
25
|
import { pickGlyphs } from './glyphs.js';
|
|
26
26
|
import { recognize } from '../input/keymap.js';
|
|
27
27
|
import { AutocompletePopup } from './AutocompletePopup.js';
|
|
28
|
-
import { editorReducer, INITIAL_EDITOR_STATE } from './editor-reducer.js';
|
|
28
|
+
import { editorReducer, INITIAL_EDITOR_STATE, } from './editor-reducer.js';
|
|
29
|
+
import { hasPasteFrame, splitPasteFrames } from './paste-frames.js';
|
|
30
|
+
import { allocatePasteId, expandPastes, expandRangeOverTokens } from './paste-tokens.js';
|
|
31
|
+
import { draftMaxRows } from './composer-limits.js';
|
|
32
|
+
import { layoutComposer, splitRowAtColumn } from './composer-rows.js';
|
|
33
|
+
import { PASTE_DRAFT_MAX_BYTES, PASTE_MAX_BLOCKS, formatPasteSize, } from '../input/limits.js';
|
|
29
34
|
// ---------------------------------------------------------------------------
|
|
30
35
|
// The scroll chip (§4.2)
|
|
31
36
|
// ---------------------------------------------------------------------------
|
|
@@ -63,6 +68,63 @@ export function scrollChip(scrolledLines, cols, glyphs) {
|
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
70
|
// ---------------------------------------------------------------------------
|
|
71
|
+
// The draft overflow indicator (section 5.5)
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
/** `^3 v7` at its widest, so the cell is a CONSTANT (the P2-4 discipline). */
|
|
74
|
+
const OVERFLOW_CELLS = 12;
|
|
75
|
+
const OVERFLOW_MAX_N = 999;
|
|
76
|
+
/**
|
|
77
|
+
* Build the "there is more draft above / below this window" indicator, or `null`
|
|
78
|
+
* when the whole draft is on screen.
|
|
79
|
+
*
|
|
80
|
+
* Every glyph comes from `pickGlyphs` and none from a literal (I-5): on a legacy
|
|
81
|
+
* console this renders `^3 v7` rather than mojibake, and `glyphs.test.ts`'s
|
|
82
|
+
* static scan fails the build if that rule is broken here.
|
|
83
|
+
*/
|
|
84
|
+
export function overflowChip(hiddenAbove, hiddenBelow, glyphs) {
|
|
85
|
+
if (hiddenAbove <= 0 && hiddenBelow <= 0)
|
|
86
|
+
return null;
|
|
87
|
+
const show = (n) => (n > OVERFLOW_MAX_N ? `${OVERFLOW_MAX_N}+` : String(n));
|
|
88
|
+
const parts = [];
|
|
89
|
+
if (hiddenAbove > 0)
|
|
90
|
+
parts.push(`${glyphs.arrowUp}${show(hiddenAbove)}`);
|
|
91
|
+
if (hiddenBelow > 0)
|
|
92
|
+
parts.push(`${glyphs.arrowDown}${show(hiddenBelow)}`);
|
|
93
|
+
return { text: parts.join(' '), cells: OVERFLOW_CELLS };
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Draft-level paste limits (section 5.3, the P1-4 enforcement point)
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
/**
|
|
99
|
+
* Would these segments push the draft past a limit? Returns the refusal text, or
|
|
100
|
+
* `null` when the paste is allowed.
|
|
101
|
+
*
|
|
102
|
+
* Checked BEFORE `dispatch`, so a refusal dispatches nothing at all and the draft
|
|
103
|
+
* is left byte-for-byte unchanged (AC-9). The one-dispatch-per-key rule is
|
|
104
|
+
* unaffected for the same reason.
|
|
105
|
+
*/
|
|
106
|
+
export function draftLimitRefusal(segments, editor) {
|
|
107
|
+
let liveBytes = 0;
|
|
108
|
+
for (const record of editor.pastes.values())
|
|
109
|
+
liveBytes += Buffer.byteLength(record.text, 'utf8');
|
|
110
|
+
let blocks = editor.pastes.size;
|
|
111
|
+
for (const segment of segments) {
|
|
112
|
+
if (segment.kind !== 'paste')
|
|
113
|
+
continue;
|
|
114
|
+
liveBytes += Buffer.byteLength(segment.text, 'utf8');
|
|
115
|
+
blocks += 1;
|
|
116
|
+
}
|
|
117
|
+
if (blocks > PASTE_MAX_BLOCKS) {
|
|
118
|
+
return (`Too many pasted blocks in one message (limit is ${PASTE_MAX_BLOCKS}). ` +
|
|
119
|
+
'Nothing was inserted.');
|
|
120
|
+
}
|
|
121
|
+
if (liveBytes > PASTE_DRAFT_MAX_BYTES) {
|
|
122
|
+
return (`Pasted content in this message would reach ${formatPasteSize(liveBytes)}; ` +
|
|
123
|
+
`limit is ${formatPasteSize(PASTE_DRAFT_MAX_BYTES)}. Nothing was inserted.`);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
66
128
|
// Pure editing helpers (unit-tested in input.test.ts)
|
|
67
129
|
// ---------------------------------------------------------------------------
|
|
68
130
|
const isSpace = (ch) => !!ch && /\s/.test(ch);
|
|
@@ -90,9 +152,25 @@ function nextWord(buffer, cursor) {
|
|
|
90
152
|
i += 1;
|
|
91
153
|
return i;
|
|
92
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Delete `[from, to)`, first growing the range over any token it clips (G5 /
|
|
157
|
+
* D-8).
|
|
158
|
+
*
|
|
159
|
+
* A no-op growth on a buffer with no tokens, so every pre-existing assertion in
|
|
160
|
+
* `input.test.ts` holds byte-for-byte. With a token present it is what stops
|
|
161
|
+
* `Ctrl+W` / `Ctrl+U` / `Ctrl+K` leaving half a label behind — which would be a
|
|
162
|
+
* label the user then sends INSTEAD of the text it stands for.
|
|
163
|
+
*/
|
|
164
|
+
function killRange(buffer, from, to) {
|
|
165
|
+
const range = expandRangeOverTokens(buffer, from, to);
|
|
166
|
+
return { buffer: buffer.slice(0, range.from) + buffer.slice(range.to), cursor: range.from };
|
|
167
|
+
}
|
|
93
168
|
/** Apply an edit intent as a pure buffer/cursor transform. */
|
|
94
169
|
export function applyEdit(buffer, cursor, intent) {
|
|
95
170
|
switch (intent) {
|
|
171
|
+
// Cursor MOVEMENT is deliberately not token-aware (D-8): arrows, Home/End
|
|
172
|
+
// and word jumps step by character, because re-deriving `moveVertical` around
|
|
173
|
+
// a synthetic unit buys a correctness the user cannot notice.
|
|
96
174
|
case 'home':
|
|
97
175
|
return { buffer, cursor: lineStart(buffer, cursor) };
|
|
98
176
|
case 'end':
|
|
@@ -101,18 +179,12 @@ export function applyEdit(buffer, cursor, intent) {
|
|
|
101
179
|
return { buffer, cursor: prevWord(buffer, cursor) };
|
|
102
180
|
case 'wordRight':
|
|
103
181
|
return { buffer, cursor: nextWord(buffer, cursor) };
|
|
104
|
-
case 'deleteWordBack':
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
case '
|
|
109
|
-
|
|
110
|
-
return { buffer: buffer.slice(0, start) + buffer.slice(cursor), cursor: start };
|
|
111
|
-
}
|
|
112
|
-
case 'killToEnd': {
|
|
113
|
-
const end = lineEnd(buffer, cursor);
|
|
114
|
-
return { buffer: buffer.slice(0, cursor) + buffer.slice(end), cursor };
|
|
115
|
-
}
|
|
182
|
+
case 'deleteWordBack':
|
|
183
|
+
return killRange(buffer, prevWord(buffer, cursor), cursor);
|
|
184
|
+
case 'killToStart':
|
|
185
|
+
return killRange(buffer, lineStart(buffer, cursor), cursor);
|
|
186
|
+
case 'killToEnd':
|
|
187
|
+
return killRange(buffer, cursor, lineEnd(buffer, cursor));
|
|
116
188
|
}
|
|
117
189
|
}
|
|
118
190
|
/**
|
|
@@ -174,7 +246,7 @@ function isControlSeq(input) {
|
|
|
174
246
|
// ---------------------------------------------------------------------------
|
|
175
247
|
// Component
|
|
176
248
|
// ---------------------------------------------------------------------------
|
|
177
|
-
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, scrolledLines = 0, }) {
|
|
249
|
+
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines = 0, }) {
|
|
178
250
|
const { stdout } = useStdout();
|
|
179
251
|
const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
|
|
180
252
|
const { buffer, cursor, historyIndex, dismissed, sel } = editor;
|
|
@@ -221,18 +293,62 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
221
293
|
clearTimeout(id);
|
|
222
294
|
};
|
|
223
295
|
}, [fileQuery, cwd]);
|
|
224
|
-
// ---
|
|
225
|
-
//
|
|
296
|
+
// --- Layout (section 5.5). -----------------------------------------------
|
|
297
|
+
//
|
|
298
|
+
// COMPUTED HERE, ABOVE THE REPORTING EFFECT, because the number the effect
|
|
299
|
+
// sends upward has to be the number this component actually renders (I-8).
|
|
300
|
+
// Deriving it twice — once for the frame and once for the budget — is exactly
|
|
301
|
+
// the trap `BottomStatusRow.tsx:5-27` documents.
|
|
302
|
+
const glyphs = pickGlyphs(caps);
|
|
303
|
+
const cols = stdout?.columns ?? 80;
|
|
304
|
+
const terminalRows = stdout?.rows ?? 24;
|
|
305
|
+
// Full-screen only: inline mode has no self-drawn viewport, so `scrolledLines`
|
|
306
|
+
// is always 0 there and the branch is unreachable rather than merely unused.
|
|
307
|
+
// The guard is explicit so it stays that way.
|
|
308
|
+
const chip = bordered ? scrollChip(scrolledLines, cols, glyphs) : null;
|
|
309
|
+
// Err NARROW. Wrapping wider than the box Ink gives us would make Ink wrap a
|
|
310
|
+
// second time, and then `layout.rows.length` is a lie — which is the one thing
|
|
311
|
+
// this module may not be.
|
|
312
|
+
const chromeCols = bordered ? 2 /* border */ + 2 /* paddingX */ + 2 /* marker */ : 2;
|
|
313
|
+
const baseCols = Math.max(8, cols - chromeCols - (chip ? chip.cells + 1 : 0));
|
|
314
|
+
const measure = (wrapCols) => layoutComposer({
|
|
315
|
+
buffer,
|
|
316
|
+
cursor,
|
|
317
|
+
cols: wrapCols,
|
|
318
|
+
maxRows: draftMaxRows(terminalRows),
|
|
319
|
+
active: isActive,
|
|
320
|
+
});
|
|
321
|
+
// TWO PASSES, AND THE SECOND ONE IS LOAD-BEARING (I-8). The overflow indicator
|
|
322
|
+
// rides the last rendered row INSIDE the box the draft wraps in, so its cells
|
|
323
|
+
// have to come out of the wrap width. Measured at the full width instead, a
|
|
324
|
+
// last row that fills the line is wrapped a SECOND time by Ink, and the
|
|
325
|
+
// composer then draws one row more than `layout.rows.length` — which is the
|
|
326
|
+
// number this component hands to `viewportRows`. That is defect C again, in
|
|
327
|
+
// precisely the situation this feature exists for: a draft too tall to show.
|
|
328
|
+
//
|
|
329
|
+
// It settles in two. Narrowing can only ADD rows, so a draft that overflowed
|
|
330
|
+
// the first pass still overflows the second, and a third pass would measure
|
|
331
|
+
// the same width as the second.
|
|
332
|
+
const wide = measure(baseCols);
|
|
333
|
+
const overflows = wide.hiddenAbove > 0 || wide.hiddenBelow > 0;
|
|
334
|
+
const usableCols = overflows ? Math.max(8, baseCols - OVERFLOW_CELLS - 1) : baseCols;
|
|
335
|
+
const layout = overflows ? measure(usableCols) : wide;
|
|
336
|
+
const draftRowCount = Math.max(1, layout.rows.length);
|
|
337
|
+
// --- Draft presence and height, reported only on a transition. -----------
|
|
338
|
+
// Notifying on every keystroke would re-render the whole App tree per key. The
|
|
339
|
+
// row count changes on a line break or a wrap boundary, not per character
|
|
340
|
+
// (R-6), so widening this callback does not widen how often it fires.
|
|
226
341
|
const hasDraft = buffer.length > 0;
|
|
227
342
|
const lastReportedDraft = useRef(null);
|
|
228
343
|
useEffect(() => {
|
|
229
344
|
if (!onDraftChange)
|
|
230
345
|
return;
|
|
231
|
-
|
|
346
|
+
const last = lastReportedDraft.current;
|
|
347
|
+
if (last && last.hasDraft === hasDraft && last.rows === draftRowCount)
|
|
232
348
|
return;
|
|
233
|
-
lastReportedDraft.current = hasDraft;
|
|
234
|
-
onDraftChange(hasDraft);
|
|
235
|
-
}, [hasDraft, onDraftChange]);
|
|
349
|
+
lastReportedDraft.current = { hasDraft, rows: draftRowCount };
|
|
350
|
+
onDraftChange({ hasDraft, rows: draftRowCount });
|
|
351
|
+
}, [hasDraft, draftRowCount, onDraftChange]);
|
|
236
352
|
// --- Mutators. -----------------------------------------------------------
|
|
237
353
|
// ONE DISPATCH EACH, and that is the rule §3.4 asks a reviewer to check line
|
|
238
354
|
// by line. Two dispatches from one key would put the legacy root back to two
|
|
@@ -240,8 +356,16 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
240
356
|
const insert = (text) => {
|
|
241
357
|
dispatch({ type: 'insert', text });
|
|
242
358
|
};
|
|
359
|
+
/**
|
|
360
|
+
* Expand every token back to its payload, then send (D-7 / D-9).
|
|
361
|
+
*
|
|
362
|
+
* `onSubmit`'s signature is unchanged, so `Composer`, `App.handleSubmit`,
|
|
363
|
+
* `runSlashInput` and `submitMessage` are all untouched — and a slash command
|
|
364
|
+
* therefore sees the EXPANDED text, which is what `/skill foo <pasted body>`
|
|
365
|
+
* needs. `clear` releases every payload in the same dispatch.
|
|
366
|
+
*/
|
|
243
367
|
const submit = (text) => {
|
|
244
|
-
onSubmit(text);
|
|
368
|
+
onSubmit(expandPastes(text, editor.pastes));
|
|
245
369
|
dispatch({ type: 'clear' });
|
|
246
370
|
};
|
|
247
371
|
const recallUp = () => {
|
|
@@ -382,11 +506,32 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
382
506
|
onHelp();
|
|
383
507
|
return;
|
|
384
508
|
}
|
|
509
|
+
// A PASTE (section 5.4). It sits AFTER every key branch so a paste can
|
|
510
|
+
// never be shadowed by a key test, and BEFORE `isControlSeq` because a
|
|
511
|
+
// framed string starts with NUL and would otherwise be dropped.
|
|
512
|
+
//
|
|
513
|
+
// The text runs inside `segments` are already sanitised (I-14): Ink drains
|
|
514
|
+
// its whole buffer in one `read()`, so this same string can carry the `\r`
|
|
515
|
+
// of a user who pasted and pressed Enter inside the burst window.
|
|
516
|
+
if (hasPasteFrame(input)) {
|
|
517
|
+
const runs = splitPasteFrames(input);
|
|
518
|
+
const segments = runs.map((run) => run.kind === 'paste' ? { kind: 'paste', text: run.text, id: allocatePasteId() } : run);
|
|
519
|
+
if (segments.length === 0)
|
|
520
|
+
return;
|
|
521
|
+
const refusal = draftLimitRefusal(segments, editor);
|
|
522
|
+
if (refusal) {
|
|
523
|
+
// A refusal dispatches NOTHING, so the draft is byte-for-byte
|
|
524
|
+
// unchanged (AC-9) — and it is never silent (D-11).
|
|
525
|
+
onNotice?.('warn', refusal);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
dispatch({ type: 'input', segments });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
385
531
|
// Printable input — drop any unrecognized control/escape byte.
|
|
386
532
|
if (input && !key.tab && !isControlSeq(input))
|
|
387
533
|
insert(input);
|
|
388
534
|
}, { isActive });
|
|
389
|
-
const glyphs = pickGlyphs(caps);
|
|
390
535
|
const marker = running ? glyphs.steer : glyphs.caret;
|
|
391
536
|
const markerColor = running ? theme.toolRunning : theme.primary;
|
|
392
537
|
const placeholder = running
|
|
@@ -394,12 +539,22 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
394
539
|
: agentMode === 'plan'
|
|
395
540
|
? `Describe what you want to build; I'll research and plan it first${glyphs.ellipsis}`
|
|
396
541
|
: `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
542
|
+
const overflow = overflowChip(layout.hiddenAbove, layout.hiddenBelow, glyphs);
|
|
543
|
+
const tokenColor = theme.hintFg ?? theme.muted;
|
|
544
|
+
const renderSegments = (segments, keyPrefix) => segments.map((segment, i) => (_jsx(Text, { color: segment.kind === 'token' ? tokenColor : undefined, children: segment.text }, `${keyPrefix}${i}`)));
|
|
545
|
+
const rowNodes = layout.rows.map((row, i) => {
|
|
546
|
+
if (i !== layout.cursorRow)
|
|
547
|
+
return _jsx(_Fragment, { children: renderSegments(row.segments, `s${i}.`) });
|
|
548
|
+
// The caret is drawn as one inverse cell, split at the column
|
|
549
|
+
// `layoutComposer` reported — the same width table it wrapped with (I-9).
|
|
550
|
+
const split = splitRowAtColumn(row, layout.cursorCol);
|
|
551
|
+
return (_jsxs(_Fragment, { children: [renderSegments(split.before, `b${i}.`), _jsx(Text, { inverse: true, children: split.at ? split.at.text : ' ' }), renderSegments(split.after, `a${i}.`)] }));
|
|
552
|
+
});
|
|
553
|
+
const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (_jsx(Text, { color: theme.muted, children: placeholder })) : (rowNodes.map((node, i) => overflow && i === rowNodes.length - 1 ? (
|
|
554
|
+
// The overflow indicator rides the LAST rendered row rather than
|
|
555
|
+
// taking one of its own: an extra row would put the composer's
|
|
556
|
+
// real height one above the `draftRows` it just reported (I-8).
|
|
557
|
+
_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { children: node }) }), _jsx(Box, { flexShrink: 0, width: overflow.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: overflow.text }) })] }, i)) : (_jsx(Text, { children: node }, i)))) }), chip && (
|
|
403
558
|
// `alignSelf="flex-start"` pins the chip to the FIRST row of a multi-line
|
|
404
559
|
// draft. Yoga's default `stretch` sizes this box to the full height of the
|
|
405
560
|
// editor; it renders on the top row either way today, and relying on that
|
|
@@ -412,26 +567,3 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
412
567
|
_jsx(Box, { flexShrink: 0, alignSelf: "flex-start", marginLeft: 1, width: chip.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: chip.text }) }))] }));
|
|
413
568
|
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)] }));
|
|
414
569
|
}
|
|
415
|
-
/** Split the buffer into lines and render an inverse cursor at `cursor`. */
|
|
416
|
-
function renderWithCursor(buffer, cursor, active) {
|
|
417
|
-
if (!active)
|
|
418
|
-
return buffer.split('\n');
|
|
419
|
-
const before = buffer.slice(0, cursor);
|
|
420
|
-
const at = buffer[cursor] ?? ' ';
|
|
421
|
-
const after = buffer.slice(cursor + 1);
|
|
422
|
-
const beforeLines = before.split('\n');
|
|
423
|
-
const afterLines = after.split('\n');
|
|
424
|
-
const nodes = [];
|
|
425
|
-
const lastBeforeIdx = beforeLines.length - 1;
|
|
426
|
-
for (let i = 0; i < lastBeforeIdx; i += 1) {
|
|
427
|
-
nodes.push(_jsx(Text, { children: beforeLines[i] }, `b${i}`));
|
|
428
|
-
}
|
|
429
|
-
const cursorChar = at === '\n' ? ' ' : at;
|
|
430
|
-
const firstAfter = afterLines[0] ?? '';
|
|
431
|
-
nodes.push(_jsxs(Text, { children: [beforeLines[lastBeforeIdx], _jsx(Text, { inverse: true, children: cursorChar }), at === '\n' ? '' : firstAfter] }, "cursor-line"));
|
|
432
|
-
const remainingAfter = at === '\n' ? afterLines : afterLines.slice(1);
|
|
433
|
-
for (let i = 0; i < remainingAfter.length; i += 1) {
|
|
434
|
-
nodes.push(_jsx(Text, { children: remainingAfter[i] }, `a${i}`));
|
|
435
|
-
}
|
|
436
|
-
return nodes;
|
|
437
|
-
}
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -88,6 +88,25 @@ interface StatusBarProps {
|
|
|
88
88
|
done: number;
|
|
89
89
|
total: number;
|
|
90
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Live background services (background-service-supervision §3.8), or absent
|
|
93
|
+
* when none are running — an ordinary session's bar is then unchanged.
|
|
94
|
+
*
|
|
95
|
+
* LEFT CLUSTER, after the todo chip, and it DEGRADES rather than hiding below
|
|
96
|
+
* `PROC_LIMITS.statusCompactCols`. That is the `todoActive` / `teamActive`
|
|
97
|
+
* ladder rather than `compactionActive`'s hide-below-threshold one, and the
|
|
98
|
+
* choice is deliberate: `compact` and `fast` describe something the CLI is
|
|
99
|
+
* doing to itself, whereas this is a PROCESS ON THE USER'S MACHINE HOLDING A
|
|
100
|
+
* PORT. A narrow terminal is a reason to shorten it, never a reason to stop
|
|
101
|
+
* saying it.
|
|
102
|
+
*
|
|
103
|
+
* `live` counts `starting | ready | running`. The transcript records events
|
|
104
|
+
* about a service (D-11); this is where "what is running right now" lives,
|
|
105
|
+
* alongside `/bg` and `bash_output`.
|
|
106
|
+
*/
|
|
107
|
+
servicesActive?: {
|
|
108
|
+
live: number;
|
|
109
|
+
};
|
|
91
110
|
/**
|
|
92
111
|
* Live retry state (llm-api-retry-backoff §6.6), or absent when nothing is
|
|
93
112
|
* retrying — an ordinary session's bar is then unchanged.
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -9,6 +9,7 @@ import { FAST_LIMITS } from '../fast/limits.js';
|
|
|
9
9
|
import { COMPACTION_LIMITS } from '../compaction/limits.js';
|
|
10
10
|
import { TEAM_LIMITS } from '../team/limits.js';
|
|
11
11
|
import { TODO_LIMITS } from '../todo/limits.js';
|
|
12
|
+
import { PROC_LIMITS } from '../proc/limits.js';
|
|
12
13
|
/** Below this the `eco` chip costs the context gauge more than it is worth. */
|
|
13
14
|
const ECO_MIN_COLS = 72;
|
|
14
15
|
/**
|
|
@@ -30,7 +31,7 @@ function redrawChar(nonce) {
|
|
|
30
31
|
return nonce % 2 === 0 ? ' ' : NBSP;
|
|
31
32
|
}
|
|
32
33
|
export function StatusBar(props) {
|
|
33
|
-
const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, retryActive, ecoRung = 0, fastActive, compactionActive, gaugeMarks, contextEstimated = false, } = props;
|
|
34
|
+
const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, servicesActive, retryActive, ecoRung = 0, fastActive, compactionActive, gaugeMarks, contextEstimated = false, } = props;
|
|
34
35
|
const { stdout } = useStdout();
|
|
35
36
|
const cols = stdout?.columns ?? 80;
|
|
36
37
|
const running = status === 'running';
|
|
@@ -66,5 +67,7 @@ export function StatusBar(props) {
|
|
|
66
67
|
? `agents ${teamActive.running}/${teamActive.total}`
|
|
67
68
|
: `[${teamActive.running}]`] })), retryActive && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', formatRetryChip(retryActive, cols)] })), todoActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= TODO_LIMITS.statusCompactCols
|
|
68
69
|
? `todo ${todoActive.done}/${todoActive.total}`
|
|
69
|
-
: `[${todoActive.done}/${todoActive.total}]`] })),
|
|
70
|
+
: `[${todoActive.done}/${todoActive.total}]`] })), servicesActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= PROC_LIMITS.statusCompactCols
|
|
71
|
+
? `svc ${servicesActive.live}`
|
|
72
|
+
: `[${servicesActive.live}]`] })), 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: [compactionActive && cols >= COMPACTION_LIMITS.statusCompactCols && (_jsxs(Text, { color: compactionActive.inFlight ? theme.accent : theme.muted, children: [compactionActive.inFlight ? 'compacting' : 'compact', ' '] })), fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), 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)] }))] })] }));
|
|
70
73
|
}
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -40,6 +40,7 @@ import { TodoCard, todoCardColor } from './entries/TodoCard.js';
|
|
|
40
40
|
import { RetryCard, retryCardColor } from './entries/RetryCard.js';
|
|
41
41
|
import { FastCard, fastCardColor } from './entries/FastCard.js';
|
|
42
42
|
import { CompactionCard, compactionCardColor } from './entries/CompactionCard.js';
|
|
43
|
+
import { ServiceCard, serviceCardColor, serviceGlyph } from './entries/ServiceCard.js';
|
|
43
44
|
import { MeasuredEntry } from './layout/MeasuredEntry.js';
|
|
44
45
|
import { useViewportGeometry } from './layout/viewport-geometry.js';
|
|
45
46
|
import { estimateEntryRows, heightKey, selectWindow, VIRTUAL_LIMITS, } from './layout/virtual-window.js';
|
|
@@ -107,6 +108,31 @@ export function computeSettledCount(entries, expandedToolIds) {
|
|
|
107
108
|
// which is the other half.
|
|
108
109
|
if (e.kind === 'compaction' && e.live)
|
|
109
110
|
break;
|
|
111
|
+
/**
|
|
112
|
+
* A SERVICE BLOCKS ONLY WHILE IT IS `starting` (D-11 / P0-4).
|
|
113
|
+
*
|
|
114
|
+
* Every clause above is bounded by an OPERATION — a dispatch, a turn, one
|
|
115
|
+
* LLM call, one countdown — which is why each of them can safely pin this
|
|
116
|
+
* prefix scan: the wait is short and it ends. A service is bounded by the
|
|
117
|
+
* USER'S INTENT. A dev server left up for an hour is the normal case, not
|
|
118
|
+
* the pathological one, so treating it like the other five would pin the
|
|
119
|
+
* boundary at the card and re-render the whole transcript from there on
|
|
120
|
+
* EVERY FRAME, FOREVER, on exactly the long sessions where it is least
|
|
121
|
+
* likely to be noticed.
|
|
122
|
+
*
|
|
123
|
+
* `starting` is different, and it is different for the reason that makes
|
|
124
|
+
* every other clause here legitimate: it is bounded by `readyTimeoutMs`,
|
|
125
|
+
* after which the supervisor calls the service `running` whatever happened.
|
|
126
|
+
*
|
|
127
|
+
* The cost of the rest is that a printed card stops mirroring live state —
|
|
128
|
+
* which is what a transcript IS. A terminal transition appends a NEW one-row
|
|
129
|
+
* entry rather than rewriting a card `<Static>` has already drawn, and live
|
|
130
|
+
* state lives in the status chip, `/bg` and `bash_output`. Weakening this to
|
|
131
|
+
* "any non-terminal service blocks" is the change D-11 exists to prevent;
|
|
132
|
+
* see condition 5 of the design's review verdict before making it.
|
|
133
|
+
*/
|
|
134
|
+
if (e.kind === 'service' && e.status === 'starting')
|
|
135
|
+
break;
|
|
110
136
|
count = i + 1;
|
|
111
137
|
}
|
|
112
138
|
return count;
|
|
@@ -148,7 +174,7 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
148
174
|
const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
|
|
149
175
|
switch (entry.kind) {
|
|
150
176
|
case 'user':
|
|
151
|
-
return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme }));
|
|
177
|
+
return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme, caps: caps }));
|
|
152
178
|
case 'assistant': {
|
|
153
179
|
// While text streams, the spinner IS the role marker. Braille dots are
|
|
154
180
|
// both an animation and a Unicode-only glyph, so reduced motion and an
|
|
@@ -208,6 +234,8 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
208
234
|
elapsedMs: entry.live && nowSec !== undefined && entry.startedAt !== undefined
|
|
209
235
|
? Math.max(0, nowSec * 1000 - entry.startedAt)
|
|
210
236
|
: undefined, live: entry.live, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
|
|
237
|
+
case 'service':
|
|
238
|
+
return frame(serviceGlyph(entry, glyphs), serviceCardColor(entry, theme), _jsx(ServiceCard, { serviceId: entry.serviceId, command: entry.command, status: entry.status, url: entry.url, port: entry.port, exitCode: entry.exitCode, startedAt: entry.startedAt, readyAt: entry.readyAt, endedAt: entry.endedAt, rows: entry.rows, terminal: entry.terminal, killIncomplete: entry.killIncomplete, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
|
|
211
239
|
case 'notice':
|
|
212
240
|
return frame(noticeSymbol(entry.level, theme), noticeColor(entry.level, theme), _jsx(Text, { color: noticeColor(entry.level, theme), children: entry.text }));
|
|
213
241
|
default:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer and transcript row ceilings (tui-paste-handling section 5.5 / 5.6).
|
|
3
|
+
*
|
|
4
|
+
* Structural, not policy (D-10): the only user-facing key this feature adds is
|
|
5
|
+
* the boolean `paste`. Everything here is a bound that keeps the frame's row
|
|
6
|
+
* arithmetic true, and every one of them is switchable by returning a larger
|
|
7
|
+
* number rather than by a config flag.
|
|
8
|
+
*/
|
|
9
|
+
/** At or below BOTH of these a paste is inserted verbatim rather than collapsed. */
|
|
10
|
+
export declare const PASTE_INLINE_MAX_LINES = 6;
|
|
11
|
+
export declare const PASTE_INLINE_MAX_CHARS = 400;
|
|
12
|
+
/**
|
|
13
|
+
* Rows one submitted user message may RENDER in the transcript, before the
|
|
14
|
+
* `... +N more lines` tail (G7).
|
|
15
|
+
*
|
|
16
|
+
* `ui/layout/virtual-window.ts` imports this rather than duplicating the number,
|
|
17
|
+
* unlike `DIFF_COLLAPSED_LINES` / `TOOL_LIVE_TAIL_ROWS` beside it: I-13 makes the
|
|
18
|
+
* render cap and the height estimate THE SAME NUMBER, and there is no
|
|
19
|
+
* import-cycle reason here for them to be two.
|
|
20
|
+
*/
|
|
21
|
+
export declare const USER_ENTRY_MAX_ROWS = 40;
|
|
22
|
+
/**
|
|
23
|
+
* Ceiling on the composer's DRAFT rows at a given terminal height.
|
|
24
|
+
*
|
|
25
|
+
* Non-decreasing in `terminalRows`. Returning `Infinity` reduces the whole
|
|
26
|
+
* height-bound layer to the rung below it (section 12, S4), which is what makes
|
|
27
|
+
* that stage revertible on its own.
|
|
28
|
+
*/
|
|
29
|
+
export declare function draftMaxRows(terminalRows: number): number;
|
|
30
|
+
/** The terminal height at which `draftMaxRows` steps from 3 to 6. Pinned by test. */
|
|
31
|
+
export declare const DRAFT_MAX_ROWS_HINT_TIER = 20;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer and transcript row ceilings (tui-paste-handling section 5.5 / 5.6).
|
|
3
|
+
*
|
|
4
|
+
* Structural, not policy (D-10): the only user-facing key this feature adds is
|
|
5
|
+
* the boolean `paste`. Everything here is a bound that keeps the frame's row
|
|
6
|
+
* arithmetic true, and every one of them is switchable by returning a larger
|
|
7
|
+
* number rather than by a config flag.
|
|
8
|
+
*/
|
|
9
|
+
/** At or below BOTH of these a paste is inserted verbatim rather than collapsed. */
|
|
10
|
+
export const PASTE_INLINE_MAX_LINES = 6;
|
|
11
|
+
export const PASTE_INLINE_MAX_CHARS = 400;
|
|
12
|
+
/**
|
|
13
|
+
* Rows one submitted user message may RENDER in the transcript, before the
|
|
14
|
+
* `... +N more lines` tail (G7).
|
|
15
|
+
*
|
|
16
|
+
* `ui/layout/virtual-window.ts` imports this rather than duplicating the number,
|
|
17
|
+
* unlike `DIFF_COLLAPSED_LINES` / `TOOL_LIVE_TAIL_ROWS` beside it: I-13 makes the
|
|
18
|
+
* render cap and the height estimate THE SAME NUMBER, and there is no
|
|
19
|
+
* import-cycle reason here for them to be two.
|
|
20
|
+
*/
|
|
21
|
+
export const USER_ENTRY_MAX_ROWS = 40;
|
|
22
|
+
/**
|
|
23
|
+
* The terminal height at which the composer's hint row starts to fit.
|
|
24
|
+
*
|
|
25
|
+
* DELIBERATELY A SECOND COPY of `ui/layout/budget.ts::HINT_MIN_ROWS` rather than
|
|
26
|
+
* an import: `budget.ts` calls `draftMaxRows` to clamp its composer term, so an
|
|
27
|
+
* import in this direction would close an ESM cycle whose only protection is
|
|
28
|
+
* that neither side reads the other at module scope -- true today, and a
|
|
29
|
+
* confusing TDZ crash the first time someone hoists a use. `budget.test.ts`
|
|
30
|
+
* asserts the two numbers are equal, so they cannot drift in silence.
|
|
31
|
+
*/
|
|
32
|
+
const DRAFT_TIER_MIN_ROWS = 20;
|
|
33
|
+
/**
|
|
34
|
+
* Ceiling on the composer's DRAFT rows at a given terminal height.
|
|
35
|
+
*
|
|
36
|
+
* Non-decreasing in `terminalRows`. Returning `Infinity` reduces the whole
|
|
37
|
+
* height-bound layer to the rung below it (section 12, S4), which is what makes
|
|
38
|
+
* that stage revertible on its own.
|
|
39
|
+
*/
|
|
40
|
+
export function draftMaxRows(terminalRows) {
|
|
41
|
+
if (!Number.isFinite(terminalRows))
|
|
42
|
+
return 3;
|
|
43
|
+
if (terminalRows >= 30)
|
|
44
|
+
return 10;
|
|
45
|
+
if (terminalRows >= DRAFT_TIER_MIN_ROWS)
|
|
46
|
+
return 6;
|
|
47
|
+
return 3;
|
|
48
|
+
}
|
|
49
|
+
/** The terminal height at which `draftMaxRows` steps from 3 to 6. Pinned by test. */
|
|
50
|
+
export const DRAFT_MAX_ROWS_HINT_TIER = DRAFT_TIER_MIN_ROWS;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer layout (tui-paste-handling section 5.5) -- pure. No Ink, no React.
|
|
3
|
+
*
|
|
4
|
+
* THE ONE PLACE THAT MAPS A BUFFER INDEX TO A SCREEN CELL (I-9). The composer
|
|
5
|
+
* used to hand its buffer to Ink one `<Text>` per logical line and let Yoga wrap
|
|
6
|
+
* it, which is fine until the draft is taller than the frame: `AppShell` gives
|
|
7
|
+
* the bottom chrome `flexShrink={0}`, so a tall composer does not grow the frame
|
|
8
|
+
* -- Yoga takes the rows out of the transcript and `overflow: hidden` clips them
|
|
9
|
+
* away, while `layout/budget.ts` keeps telling five consumers the composer is
|
|
10
|
+
* three rows tall. Four hundred presses of `Shift+Enter` do it without pasting
|
|
11
|
+
* anything.
|
|
12
|
+
*
|
|
13
|
+
* So the wrap happens HERE, the row count is a number this module returns, and
|
|
14
|
+
* `budget.ts` is told that number (I-8).
|
|
15
|
+
*
|
|
16
|
+
* HARD WRAP BY DISPLAY COLUMN, NEVER `layout/wrap-rows.ts::wrapToRows` (D-15).
|
|
17
|
+
* That function is a PROSE wrapper: it splits on `/\s+/` and rejoins with single
|
|
18
|
+
* spaces, so it destroys indentation and makes the character-index-to-screen-cell
|
|
19
|
+
* mapping the caret depends on impossible to state, let alone compute.
|
|
20
|
+
*/
|
|
21
|
+
export interface ComposerSegment {
|
|
22
|
+
readonly text: string;
|
|
23
|
+
readonly kind: 'text' | 'token';
|
|
24
|
+
}
|
|
25
|
+
export interface ComposerRow {
|
|
26
|
+
readonly segments: ComposerSegment[];
|
|
27
|
+
}
|
|
28
|
+
export interface ComposerLayout {
|
|
29
|
+
/** At most `maxRows`, and always containing the caret when `active`. */
|
|
30
|
+
readonly rows: ComposerRow[];
|
|
31
|
+
/** Index into `rows`, or -1 when the composer is not focused. */
|
|
32
|
+
readonly cursorRow: number;
|
|
33
|
+
/** Display column within that row. */
|
|
34
|
+
readonly cursorCol: number;
|
|
35
|
+
/** Rows the buffer occupies BEFORE the window was applied. */
|
|
36
|
+
readonly totalRows: number;
|
|
37
|
+
readonly hiddenAbove: number;
|
|
38
|
+
readonly hiddenBelow: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ComposerLayoutInput {
|
|
41
|
+
readonly buffer: string;
|
|
42
|
+
readonly cursor: number;
|
|
43
|
+
readonly cols: number;
|
|
44
|
+
readonly maxRows: number;
|
|
45
|
+
readonly active: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** Total display width of a run, using the same table the layout used. */
|
|
48
|
+
export declare function displayWidth(text: string): number;
|
|
49
|
+
/**
|
|
50
|
+
* Wrap `buffer` into rows, locate the caret, and select a window of at most
|
|
51
|
+
* `maxRows` rows that contains it.
|
|
52
|
+
*
|
|
53
|
+
* The window is pinned to the BOTTOM -- where the user is typing -- and pulled up
|
|
54
|
+
* only far enough to keep the caret visible. That is deterministic, which matters
|
|
55
|
+
* because this function holds no state between calls: a scroll offset would have
|
|
56
|
+
* to live in the reducer and would then need its own invalidation rules.
|
|
57
|
+
*/
|
|
58
|
+
export declare function layoutComposer(input: ComposerLayoutInput): ComposerLayout;
|
|
59
|
+
export interface RowSplit {
|
|
60
|
+
readonly before: ComposerSegment[];
|
|
61
|
+
/** The one cell the caret sits on, or `null` when it sits past the last one. */
|
|
62
|
+
readonly at: ComposerSegment | null;
|
|
63
|
+
readonly after: ComposerSegment[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Split one row at a display column so the renderer can draw an inverse caret on
|
|
67
|
+
* exactly one cell.
|
|
68
|
+
*
|
|
69
|
+
* Lives here rather than in the component because it walks the SAME width table
|
|
70
|
+
* `layoutComposer` walked (I-9): a second, subtly different measurement is how
|
|
71
|
+
* the caret ends up drawn on a cell that does not hold the character it is on.
|
|
72
|
+
*/
|
|
73
|
+
export declare function splitRowAtColumn(row: ComposerRow, column: number): RowSplit;
|