@aragon-agent/cli 0.6.8 → 0.6.10
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 +27 -0
- package/README.md +89 -44
- package/dist/agent/controller.d.ts +1 -1
- package/dist/agent/reducer.d.ts +31 -113
- package/dist/agent/reducer.js +41 -63
- package/dist/cli.js +53 -155
- package/dist/commands/builtins.d.ts +2 -1
- package/dist/commands/builtins.js +47 -2
- package/dist/commands/perf.d.ts +32 -2
- package/dist/commands/perf.js +32 -2
- package/dist/commands/registry.d.ts +2 -2
- package/dist/config/env.js +0 -130
- package/dist/config/load.d.ts +0 -2
- package/dist/config/load.js +2 -31
- package/dist/config/schema.d.ts +1 -14
- package/dist/config/schema.js +0 -1
- package/dist/config/store.js +2 -0
- package/dist/input/enter-sequences.d.ts +63 -0
- package/dist/input/enter-sequences.js +151 -0
- package/dist/input/limits.d.ts +11 -0
- package/dist/input/limits.js +11 -0
- package/dist/input/stdin-filter.js +19 -1
- package/dist/logging/install.d.ts +2 -2
- package/dist/logging/install.js +2 -2
- package/dist/session/persist.js +20 -0
- package/dist/todo/follow-through.d.ts +0 -4
- package/dist/todo/follow-through.js +0 -4
- package/dist/todo/limits.d.ts +0 -10
- package/dist/todo/limits.js +0 -10
- package/dist/todo/prompt.d.ts +0 -15
- package/dist/ui/ActivityLine.d.ts +26 -34
- package/dist/ui/ActivityLine.js +30 -19
- package/dist/ui/App.d.ts +1 -41
- package/dist/ui/App.js +94 -129
- package/dist/ui/BottomStatusRow.d.ts +1 -3
- package/dist/ui/BottomStatusRow.js +4 -8
- package/dist/ui/Composer.d.ts +28 -1
- package/dist/ui/Composer.js +46 -17
- package/dist/ui/Header.d.ts +2 -16
- package/dist/ui/Header.js +3 -18
- package/dist/ui/Logo.d.ts +1 -17
- package/dist/ui/Logo.js +0 -16
- package/dist/ui/ModeChip.d.ts +7 -18
- package/dist/ui/ModeChip.js +9 -0
- package/dist/ui/PromptInput.d.ts +2 -28
- package/dist/ui/PromptInput.js +38 -9
- package/dist/ui/StatusBar.d.ts +0 -11
- package/dist/ui/ToastStack.d.ts +2 -18
- package/dist/ui/ToastStack.js +4 -11
- package/dist/ui/Transcript.d.ts +5 -71
- package/dist/ui/Transcript.js +15 -199
- package/dist/ui/enter-frames.d.ts +63 -0
- package/dist/ui/enter-frames.js +136 -0
- package/dist/ui/entries/AssistantEntry.d.ts +0 -23
- package/dist/ui/entries/AssistantEntry.js +3 -4
- package/dist/ui/entries/DiffView.d.ts +1 -7
- package/dist/ui/entries/DiffView.js +2 -2
- package/dist/ui/entries/QueuedEntry.d.ts +27 -0
- package/dist/ui/entries/QueuedEntry.js +30 -0
- package/dist/ui/entries/ToolCard.d.ts +0 -6
- package/dist/ui/entries/ToolCard.js +4 -13
- package/dist/ui/frame-differ.d.ts +13 -0
- package/dist/ui/frame-differ.js +36 -6
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/ink-runtime.d.ts +1 -0
- package/dist/ui/ink-runtime.js +21 -0
- package/dist/ui/layout/AppShell.d.ts +1 -10
- package/dist/ui/layout/AppShell.js +2 -5
- package/dist/ui/layout/OverlayFrame.d.ts +1 -25
- package/dist/ui/layout/OverlayFrame.js +0 -24
- package/dist/ui/layout/ScrollViewport.d.ts +3 -0
- package/dist/ui/layout/ScrollViewport.js +17 -5
- package/dist/ui/layout/frame.d.ts +0 -22
- package/dist/ui/layout/frame.js +0 -36
- package/dist/ui/layout/todo-layout.d.ts +1 -3
- package/dist/ui/layout/todo-layout.js +7 -8
- package/dist/ui/layout/viewport-geometry.d.ts +1 -1
- package/dist/ui/layout/viewport-geometry.js +1 -1
- package/dist/ui/live-clamp.d.ts +1 -29
- package/dist/ui/live-clamp.js +1 -29
- package/dist/ui/overlays/HelpOverlay.js +3 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +5 -1
- package/dist/ui/overlays/QuestionOverlay.js +7 -2
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/run-status-row.d.ts +61 -0
- package/dist/ui/run-status-row.js +70 -0
- package/dist/ui/selection/selection-controller.d.ts +23 -7
- package/dist/ui/selection/selection-controller.js +68 -16
- package/dist/ui/transcript-text.d.ts +0 -8
- package/dist/ui/transcript-text.js +0 -8
- package/dist/ui/use-startup-notices.d.ts +6 -58
- package/dist/ui/use-startup-notices.js +10 -57
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/ui/TodoStrip.d.ts +0 -36
- package/dist/ui/TodoStrip.js +0 -32
package/dist/ui/Transcript.js
CHANGED
|
@@ -1,38 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* flicker on long/fast sessions.
|
|
7
|
-
*
|
|
8
|
-
* `computeSettledCount` is pure and exported for `transcript-static.test.ts`.
|
|
9
|
-
* The settled boundary is held MONOTONIC at render time: a previously-settled
|
|
10
|
-
* entry never leaves `<Static>` (which cannot un-print it) even if it later
|
|
11
|
-
* becomes expanded, so the live tail can never duplicate a scrolled-off entry.
|
|
12
|
-
*
|
|
13
|
-
* `EntryView` is shared by both render paths ON PURPOSE (§4.3 / §12). Forking it
|
|
14
|
-
* to freeze inline's v0.2.0 output would mean writing every future entry-layer
|
|
15
|
-
* change twice and watching the two drift; inline is a degradation path, not a
|
|
16
|
-
* parallel product. What inline still guarantees is its GEOMETRY contract — no
|
|
17
|
-
* fixed frame, `<Static>` keeps the settled history, no private ANSI — not its
|
|
18
|
-
* pixel-for-pixel appearance.
|
|
19
|
-
*
|
|
20
|
-
* Two things changed with tui-render-performance:
|
|
21
|
-
* - `EntryView` is `React.memo`'d with an explicit comparator (L2 / R3), which
|
|
22
|
-
* is only worth anything because `mapEntry` already preserves object
|
|
23
|
-
* identity for untouched entries;
|
|
24
|
-
* - `TranscriptList` (the FULL-SCREEN body) mounts only the entries inside the
|
|
25
|
-
* viewport and replaces the rest with two spacer boxes (L3 / R1), and the
|
|
26
|
-
* inline `Transcript` clamps its live region so `ink.js:121` is unreachable
|
|
27
|
-
* (L5 / R6).
|
|
28
|
-
*/
|
|
29
|
-
import React, { useCallback, useRef } from 'react';
|
|
30
|
-
import { Box, Static, Text } from 'ink';
|
|
2
|
+
/** Virtualized full-screen transcript and entry cards. */
|
|
3
|
+
/** Virtualized full-screen transcript and shared entry cards. */
|
|
4
|
+
import React, { useCallback } from 'react';
|
|
5
|
+
import { Box, Text } from 'ink';
|
|
31
6
|
import Spinner from 'ink-spinner';
|
|
32
7
|
import { pickGlyphs, toolGlyph } from './glyphs.js';
|
|
33
8
|
import { separationRows } from './density.js';
|
|
34
9
|
import { EntryFrame } from './entries/EntryFrame.js';
|
|
35
10
|
import { UserEntry } from './entries/UserEntry.js';
|
|
11
|
+
import { QueuedEntry } from './entries/QueuedEntry.js';
|
|
36
12
|
import { AssistantEntry } from './entries/AssistantEntry.js';
|
|
37
13
|
import { ToolCard, statusColor, toolEntryGlyph } from './entries/ToolCard.js';
|
|
38
14
|
import { TeamCard, teamCardColor } from './entries/TeamCard.js';
|
|
@@ -45,98 +21,6 @@ import { MeasuredEntry } from './layout/MeasuredEntry.js';
|
|
|
45
21
|
import { useViewportGeometry } from './layout/viewport-geometry.js';
|
|
46
22
|
import { estimateEntryRows, heightKey, selectWindow, VIRTUAL_LIMITS, } from './layout/virtual-window.js';
|
|
47
23
|
import { advanceTailRows } from './layout/follow-state.js';
|
|
48
|
-
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
49
|
-
/** Keep the last K entries in the live region so a mutating tail stays hot. */
|
|
50
|
-
const LIVE_TAIL = 1;
|
|
51
|
-
/**
|
|
52
|
-
* Rows the inline live region leaves for the composer, the prompt echo and the
|
|
53
|
-
* one-row margin Ink needs before `outputHeight >= stdout.rows` (I-L5-1).
|
|
54
|
-
*/
|
|
55
|
-
const INLINE_LIVE_MARGIN = 4;
|
|
56
|
-
/** Floor on the clamp, so a very short terminal still shows something useful. */
|
|
57
|
-
const INLINE_LIVE_MIN_ROWS = 4;
|
|
58
|
-
/**
|
|
59
|
-
* Largest prefix length P such that `entries[0..P)` are all terminal, none is
|
|
60
|
-
* within the last `LIVE_TAIL` entries, and none is currently expanded. Pure and
|
|
61
|
-
* non-decreasing as entries terminalize (expanding a card can lower it — the
|
|
62
|
-
* component clamps with a monotonic high-water mark to avoid Static duplication).
|
|
63
|
-
*/
|
|
64
|
-
export function computeSettledCount(entries, expandedToolIds) {
|
|
65
|
-
const limit = entries.length - LIVE_TAIL;
|
|
66
|
-
let count = 0;
|
|
67
|
-
for (let i = 0; i < entries.length; i += 1) {
|
|
68
|
-
if (i >= limit)
|
|
69
|
-
break;
|
|
70
|
-
const e = entries[i];
|
|
71
|
-
if (expandedToolIds[e.id])
|
|
72
|
-
break;
|
|
73
|
-
if (e.kind === 'assistant' && e.streaming)
|
|
74
|
-
break;
|
|
75
|
-
if (e.kind === 'tool' && e.status !== 'done' && e.status !== 'error')
|
|
76
|
-
break;
|
|
77
|
-
// A live dispatch is not settled: its `runs` are rewritten on every
|
|
78
|
-
// `teamUpdate`, and `<Static>` cannot un-print what it has already drawn.
|
|
79
|
-
// The boundary is held MONOTONIC above, which is also why a `team` entry
|
|
80
|
-
// that never settles would be re-rendered on every frame for the rest of the
|
|
81
|
-
// session — the failure `session/persist.ts` normalizes away on load (P1-5).
|
|
82
|
-
if (e.kind === 'team' && e.active)
|
|
83
|
-
break;
|
|
84
|
-
// A live todo card is not settled either: it is rewritten on every
|
|
85
|
-
// `todoUpdate` for the whole turn, and `<Static>` cannot un-print what it has
|
|
86
|
-
// already drawn (C-5 / I-6). `session/persist.ts` forces `live: false` on
|
|
87
|
-
// load, which is the other half — a card that never settles is re-rendered
|
|
88
|
-
// on every frame for the rest of the session.
|
|
89
|
-
if (e.kind === 'todo' && e.live)
|
|
90
|
-
break;
|
|
91
|
-
// Nor is a retry card that is still counting down or still in flight: it is
|
|
92
|
-
// rewritten on every `retryScheduled` / `retryAttempt` and once a second by
|
|
93
|
-
// `retryTick`, and `<Static>` cannot un-print what it has already drawn.
|
|
94
|
-
// `session/persist.ts` forces a loaded card to `interrupted`, which is the
|
|
95
|
-
// other half — a card that never settles is re-rendered on every frame for
|
|
96
|
-
// the rest of the session (R-10).
|
|
97
|
-
if (e.kind === 'retry' && (e.phase === 'waiting' || e.phase === 'retrying'))
|
|
98
|
-
break;
|
|
99
|
-
// A live review card is not settled either: it is rewritten once, when the
|
|
100
|
-
// call comes back, and `<Static>` cannot un-print what it has already drawn
|
|
101
|
-
// (C-5). `session/persist.ts` forces `live: false` on load, which is the
|
|
102
|
-
// other half.
|
|
103
|
-
if (e.kind === 'fast' && e.live)
|
|
104
|
-
break;
|
|
105
|
-
// Nor is a compaction card whose call is still open: it is rewritten once,
|
|
106
|
-
// when the engine reports its verdict, and `<Static>` cannot un-print what it
|
|
107
|
-
// has already drawn (C-8). `session/persist.ts` forces `live: false` on load,
|
|
108
|
-
// which is the other half.
|
|
109
|
-
if (e.kind === 'compaction' && e.live)
|
|
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;
|
|
136
|
-
count = i + 1;
|
|
137
|
-
}
|
|
138
|
-
return count;
|
|
139
|
-
}
|
|
140
24
|
/**
|
|
141
25
|
* The value `EntryView` should receive for `nowSec` on THIS entry.
|
|
142
26
|
*
|
|
@@ -168,40 +52,27 @@ function noticeSymbol(level, theme) {
|
|
|
168
52
|
return theme.symbols.warn;
|
|
169
53
|
return theme.symbols.info;
|
|
170
54
|
}
|
|
171
|
-
function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density,
|
|
55
|
+
function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, theme, caps, nowSec, }) {
|
|
172
56
|
const glyphs = pickGlyphs(caps);
|
|
173
57
|
const separation = separationRows(prev, entry, density);
|
|
174
58
|
const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
|
|
175
59
|
switch (entry.kind) {
|
|
176
60
|
case 'user':
|
|
177
61
|
return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme, caps: caps }));
|
|
62
|
+
case 'queued':
|
|
63
|
+
// `theme.muted`: quieter than a user message, louder than a notice --
|
|
64
|
+
// the row exists to be noticed UNTIL it is accepted, then rewritten in
|
|
65
|
+
// place by `turnStart` (5.2.3).
|
|
66
|
+
return frame(glyphs.queued, theme.muted, _jsx(QueuedEntry, { text: entry.text, theme: theme, caps: caps }));
|
|
178
67
|
case 'assistant': {
|
|
179
|
-
// While text streams, the spinner IS the role marker. Braille dots are
|
|
180
|
-
// both an animation and a Unicode-only glyph, so reduced motion and an
|
|
181
|
-
// ASCII terminal fall back to the same static marker (P2-11 / A-10).
|
|
182
|
-
//
|
|
183
|
-
// `reducedMotion` ARRIVES PRE-WIDENED FROM `App`: it is now
|
|
184
|
-
// `cfg.reducedMotion || activityVisible`, so this marker is static for the
|
|
185
|
-
// whole of every run and the activity row above the composer carries the
|
|
186
|
-
// one animation (single-spinner-while-running D-2). Nothing here changes —
|
|
187
|
-
// the expression already meant "do not animate here".
|
|
188
|
-
//
|
|
189
|
-
// THAT IS SAFE FOR `<Static>` BY CONSTRUCTION, not by luck: a settled
|
|
190
|
-
// prefix is printed once and can never be re-printed, so a boolean that
|
|
191
|
-
// changed how an already-printed entry renders would tear the history.
|
|
192
|
-
// `computeSettledCount` below breaks on EVERY condition that makes an
|
|
193
|
-
// entry animate (`:84` streaming, `:85` running tool, `:91` active team,
|
|
194
|
-
// `:104` retry, `:109` live fast), so every animating entry is in the live
|
|
195
|
-
// region and every entry in `<Static>` already has `animate === false` on
|
|
196
|
-
// both sides of the flip. Weakening a break clause breaks that proof.
|
|
197
68
|
const animate = entry.streaming && !reducedMotion && caps.unicode;
|
|
198
69
|
return frame(animate ? _jsx(Spinner, { type: "dots" }) : glyphs.assistant, theme.primary, _jsx(AssistantEntry, { text: entry.text, thinking: entry.thinking, thinkingOpen: entry.thinkingOpen, thinkingVisible: thinkingVisible, streaming: entry.streaming, aborted: entry.aborted,
|
|
199
70
|
// `EntryViewImpl` hands each renderer EXPLICIT SCALAR PROPS, so
|
|
200
71
|
// nothing on the entry reaches a component by itself (P1-6b).
|
|
201
|
-
thinkingMs: entry.thinkingMs,
|
|
72
|
+
thinkingMs: entry.thinkingMs, theme: theme, caps: caps }));
|
|
202
73
|
}
|
|
203
74
|
case 'tool':
|
|
204
|
-
return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, patch: entry.patch, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion,
|
|
75
|
+
return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, patch: entry.patch, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion,
|
|
205
76
|
// `entry.live` AND NOT `entry.live ?? []` (P2-4): `ToolCard`'s default
|
|
206
77
|
// comparator sees a fresh `[]` as a changed prop, which is why that
|
|
207
78
|
// file already keeps a shared `EMPTY_LINES`.
|
|
@@ -263,10 +134,8 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
|
|
|
263
134
|
a.thinkingVisible === b.thinkingVisible &&
|
|
264
135
|
a.reducedMotion === b.reducedMotion &&
|
|
265
136
|
a.density === b.density &&
|
|
266
|
-
a.mode === b.mode &&
|
|
267
137
|
a.theme === b.theme &&
|
|
268
138
|
a.caps === b.caps &&
|
|
269
|
-
a.liveClampRows === b.liveClampRows &&
|
|
270
139
|
/*
|
|
271
140
|
* ELEVENTH TERM, AND OMITTING IT IS THE TRAP I-L2-1 DESCRIBES ONE COMMENT UP
|
|
272
141
|
* (P1-3). This comparator is a CLOSED LIST: a prop absent from it changes
|
|
@@ -279,27 +148,7 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
|
|
|
279
148
|
*/
|
|
280
149
|
a.nowSec === b.nowSec);
|
|
281
150
|
});
|
|
282
|
-
|
|
283
|
-
* Full-screen transcript body — the same entry renderers with NO `<Static>`.
|
|
284
|
-
*
|
|
285
|
-
* `<Static>` prints above the live frame; once the frame is `rows - 1` tall
|
|
286
|
-
* there is exactly one visible line up there, so the mechanism stops being a
|
|
287
|
-
* history view and starts being a leak. Full-screen therefore owns its history
|
|
288
|
-
* inside `ScrollViewport` instead.
|
|
289
|
-
*
|
|
290
|
-
* `computeSettledCount` is deliberately NOT called here: its only job is to keep
|
|
291
|
-
* an entry that already reached `<Static>` from flowing back into the live
|
|
292
|
-
* region, and with no Static there is nothing to protect. The settled high-water
|
|
293
|
-
* mark feeds nothing else — `/save` serializes `entries` in full — so leaving it
|
|
294
|
-
* un-advanced has no side effect.
|
|
295
|
-
*
|
|
296
|
-
* VIRTUALISED (L3 / R1). `windowSize` is now the SCROLL HORIZON — how far back
|
|
297
|
-
* the user can scroll — and no longer the rendering budget, because entries
|
|
298
|
-
* outside the viewport are not mounted at all. What bounds the frame is
|
|
299
|
-
* `selectWindow`, and the two spacers are what keep the scroll maths honest
|
|
300
|
-
* while they are absent.
|
|
301
|
-
*/
|
|
302
|
-
export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }) {
|
|
151
|
+
export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }) {
|
|
303
152
|
const size = Math.max(1, Math.floor(windowSize));
|
|
304
153
|
const visible = entries.length > size ? entries.slice(-size) : entries;
|
|
305
154
|
const collapsed = entries.length - visible.length;
|
|
@@ -352,39 +201,6 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
|
|
|
352
201
|
}
|
|
353
202
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [collapsed > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: [glyphs.ellipsis, " ", collapsed, " earlier ", collapsed === 1 ? 'entry' : 'entries', " collapsed (/save exports the full session)"] })), selection.leadingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.leadingRows })), slice.map((entry, i) => {
|
|
354
203
|
const index = selection.startIndex + i;
|
|
355
|
-
return (_jsx(MeasuredEntry, { heightKey: keyOf(entry), onMeasure: report, children: _jsx(EntryView, { entry: entry, prev: index > 0 ? visible[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density,
|
|
204
|
+
return (_jsx(MeasuredEntry, { heightKey: keyOf(entry), onMeasure: report, children: _jsx(EntryView, { entry: entry, prev: index > 0 ? visible[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, nowSec: nowSecFor(entry, nowSec), theme: theme, caps: caps }) }, entry.id));
|
|
356
205
|
}), selection.trailingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.trailingRows }))] }));
|
|
357
206
|
}
|
|
358
|
-
export function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, }) {
|
|
359
|
-
const highWater = useRef(0);
|
|
360
|
-
const prevLen = useRef(0);
|
|
361
|
-
const { rows } = useTerminalSize();
|
|
362
|
-
const raw = computeSettledCount(entries, expandedToolIds);
|
|
363
|
-
// The transcript shrank (clear / reset / restore) — drop the high-water mark.
|
|
364
|
-
if (entries.length < prevLen.current)
|
|
365
|
-
highWater.current = 0;
|
|
366
|
-
prevLen.current = entries.length;
|
|
367
|
-
const settled = Math.min(entries.length, Math.max(highWater.current, raw));
|
|
368
|
-
highWater.current = settled;
|
|
369
|
-
const settledEntries = entries.slice(0, settled);
|
|
370
|
-
const liveEntries = entries.slice(settled);
|
|
371
|
-
// I-L5-1 — the live region must stay strictly below `stdout.rows`, or Ink
|
|
372
|
-
// takes the `ink.js:118-123` branch and writes `clearTerminal +
|
|
373
|
-
// fullStaticOutput + output` on EVERY subsequent frame for the rest of the
|
|
374
|
-
// session.
|
|
375
|
-
//
|
|
376
|
-
// `LIVE_TAIL` is 1, so the common case is the design's `rows - 4` less the one
|
|
377
|
-
// row the clamp marker costs. THE DIVISION IS NOT DECORATION: the settled
|
|
378
|
-
// boundary is held back by an expanded card, a running tool, a live dispatch
|
|
379
|
-
// or a live todo list, and in every one of those cases the region holds
|
|
380
|
-
// SEVERAL entries at once — a flat per-entry ceiling of `rows - 4` would then
|
|
381
|
-
// overrun by a multiple of itself. `- 1` pays for each entry's marker row.
|
|
382
|
-
//
|
|
383
|
-
// The residual: an entry contributes at least its header row whatever the
|
|
384
|
-
// clamp says, so the bound holds while the live region holds fewer than about
|
|
385
|
-
// `rows / 2` entries. Bounding it unconditionally would mean refusing to draw
|
|
386
|
-
// a live entry at all, which is worse than a tall frame.
|
|
387
|
-
const liveBudget = Math.max(INLINE_LIVE_MIN_ROWS, rows - INLINE_LIVE_MARGIN);
|
|
388
|
-
const liveClampRows = Math.max(1, Math.floor(liveBudget / Math.max(1, liveEntries.length)) - 1);
|
|
389
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: settledEntries, children: (entry, index) => (_jsx(EntryView, { entry: entry, prev: index > 0 ? settledEntries[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, theme: theme, caps: caps }, entry.id)) }), _jsx(Box, { flexDirection: "column", children: liveEntries.map((entry, i) => (_jsx(EntryView, { entry: entry, prev: i > 0 ? liveEntries[i - 1] : entries[settled - 1], expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, theme: theme, caps: caps, liveClampRows: liveClampRows, nowSec: nowSecFor(entry, nowSec) }, entry.id))) })] }));
|
|
390
|
-
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading Enter frames back out of an Ink `input` string
|
|
3
|
+
* (tui-shift-enter-copy-queue 3.5) -- pure.
|
|
4
|
+
*
|
|
5
|
+
* The stdin filter rewrites a modified Enter into `ENTER_NEWLINE_FRAME` inline,
|
|
6
|
+
* in stream order, exactly the way it frames a paste. This module is the only
|
|
7
|
+
* place that takes those frames apart again, and it handles BOTH newline
|
|
8
|
+
* sources the composer can see in one `input` string:
|
|
9
|
+
*
|
|
10
|
+
* - `ENTER_NEWLINE_FRAME` -- Shift+Enter / Alt+Enter / CSI-u Enter, translated
|
|
11
|
+
* by the filter; and
|
|
12
|
+
* - a bare `\n` -- Ctrl+J, which Ink parses to `name: 'enter'` with
|
|
13
|
+
* `input === '\n'` and NO `key.return`, so it reaches this branch as an
|
|
14
|
+
* ordinary (control) character that must still mean "newline".
|
|
15
|
+
*
|
|
16
|
+
* I-12, THE BROADCAST OBLIGATION, APPLIES TO THESE FRAMES TOO. `useInput`
|
|
17
|
+
* re-emits every chunk to every mounted handler, `stripPasteFrames` does not
|
|
18
|
+
* know this frame, and a NUL appended into `SettingsScreen`'s API-key field
|
|
19
|
+
* renders as nothing and silently breaks authentication -- the exact accident
|
|
20
|
+
* class that function's own doc records. `stripEnterFrames` is the equivalent
|
|
21
|
+
* guard for overlays with no paste model of their own.
|
|
22
|
+
*/
|
|
23
|
+
import { type FrameSegment } from './paste-frames.js';
|
|
24
|
+
/** True when this `input` carries at least one Enter frame -- the branch guard. */
|
|
25
|
+
export declare function hasEnterFrame(input: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Split an `input` string into ordered text runs and newline runs.
|
|
28
|
+
*
|
|
29
|
+
* Text-only by construction (paste frames are `mergeWithPasteRuns`'s job), so
|
|
30
|
+
* the paste id never has to exist here -- `PromptInput` allocates ids for paste
|
|
31
|
+
* segments while building the reducer's action, exactly as the paste branch
|
|
32
|
+
* does (P1-5: the reducer stays pure and this stays a function of its input).
|
|
33
|
+
*/
|
|
34
|
+
export declare function splitEnterFrames(input: string): FrameSegment[];
|
|
35
|
+
/**
|
|
36
|
+
* Merge newline runs with the paste frames that share the same `input`.
|
|
37
|
+
*
|
|
38
|
+
* Ink drains its buffer in one size-less `read()`, so the filter's TWO writes
|
|
39
|
+
* (one per frame family) can arrive as ONE chunk carrying both a paste frame
|
|
40
|
+
* and an Enter frame. Neither splitter alone preserves that order --
|
|
41
|
+
* `splitPasteFrames` sanitises `\n` out of its text runs, and this module's
|
|
42
|
+
* spans treat a paste payload as ordinary text -- so the merge walks the
|
|
43
|
+
* ORIGINAL string once: paste frames become paste segments (payload through
|
|
44
|
+
* `sanitisePaste`, as `splitPasteFrames` does), and the spans between them are
|
|
45
|
+
* segmented with the SAME walker `splitEnterFrames` uses, which is what keeps
|
|
46
|
+
* the two views from disagreeing about where a newline sits.
|
|
47
|
+
*
|
|
48
|
+
* Paste segments carry NO id here (P1-5); the caller assigns one per segment
|
|
49
|
+
* only when it builds the reducer's action.
|
|
50
|
+
*/
|
|
51
|
+
export declare function mergeWithPasteRuns(input: string, runs: FrameSegment[]): FrameSegment[];
|
|
52
|
+
/**
|
|
53
|
+
* Remove every Enter frame for a consumer that has no newline model of its
|
|
54
|
+
* own, replacing each frame with `'\n'` (tui-shift-enter-copy-queue 3.5, the
|
|
55
|
+
* I-12 analogue for this frame family).
|
|
56
|
+
*
|
|
57
|
+
* NO OTHER SANITISATION, unlike `stripPasteFrames`: the overlay call sites wrap
|
|
58
|
+
* this INSIDE their existing `stripPasteFrames(...)` call, and the composite
|
|
59
|
+
* keeps that function's byte-level guarantees. Composed the other way round a
|
|
60
|
+
* lone NUL frame would survive as its payload letter; composed this way the
|
|
61
|
+
* frame's newline lands exactly where a bare Ctrl+J lands today.
|
|
62
|
+
*/
|
|
63
|
+
export declare function stripEnterFrames(input: string): string;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading Enter frames back out of an Ink `input` string
|
|
3
|
+
* (tui-shift-enter-copy-queue 3.5) -- pure.
|
|
4
|
+
*
|
|
5
|
+
* The stdin filter rewrites a modified Enter into `ENTER_NEWLINE_FRAME` inline,
|
|
6
|
+
* in stream order, exactly the way it frames a paste. This module is the only
|
|
7
|
+
* place that takes those frames apart again, and it handles BOTH newline
|
|
8
|
+
* sources the composer can see in one `input` string:
|
|
9
|
+
*
|
|
10
|
+
* - `ENTER_NEWLINE_FRAME` -- Shift+Enter / Alt+Enter / CSI-u Enter, translated
|
|
11
|
+
* by the filter; and
|
|
12
|
+
* - a bare `\n` -- Ctrl+J, which Ink parses to `name: 'enter'` with
|
|
13
|
+
* `input === '\n'` and NO `key.return`, so it reaches this branch as an
|
|
14
|
+
* ordinary (control) character that must still mean "newline".
|
|
15
|
+
*
|
|
16
|
+
* I-12, THE BROADCAST OBLIGATION, APPLIES TO THESE FRAMES TOO. `useInput`
|
|
17
|
+
* re-emits every chunk to every mounted handler, `stripPasteFrames` does not
|
|
18
|
+
* know this frame, and a NUL appended into `SettingsScreen`'s API-key field
|
|
19
|
+
* renders as nothing and silently breaks authentication -- the exact accident
|
|
20
|
+
* class that function's own doc records. `stripEnterFrames` is the equivalent
|
|
21
|
+
* guard for overlays with no paste model of their own.
|
|
22
|
+
*/
|
|
23
|
+
import { ENTER_NEWLINE_FRAME, PASTE_CLOSE, PASTE_OPEN } from '../input/limits.js';
|
|
24
|
+
import { sanitisePaste } from '../input/paste-parse.js';
|
|
25
|
+
import { sanitiseTyped } from './paste-frames.js';
|
|
26
|
+
/** True when this `input` carries at least one Enter frame -- the branch guard. */
|
|
27
|
+
export function hasEnterFrame(input) {
|
|
28
|
+
return input.includes(ENTER_NEWLINE_FRAME);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Split one Enter-frame-free span into ordered text / newline segments.
|
|
32
|
+
*
|
|
33
|
+
* Every newline source -- frame or bare `\n` -- yields ONE
|
|
34
|
+
* `{ kind: 'text', text: '\n' }` segment, so the editor's existing `input`
|
|
35
|
+
* action inserts it through the same path typing uses and the draft's row
|
|
36
|
+
* budget applies unchanged. Non-newline text is accumulated and passed through
|
|
37
|
+
* `sanitiseTyped` (I-14): a chunk that carries a frame can also carry the
|
|
38
|
+
* keystrokes that arrived beside it, and a coalesced CR must not be spliced
|
|
39
|
+
* back into the draft raw.
|
|
40
|
+
*/
|
|
41
|
+
function splitEnterSpan(span) {
|
|
42
|
+
const segments = [];
|
|
43
|
+
let text = '';
|
|
44
|
+
const pushText = () => {
|
|
45
|
+
const clean = sanitiseTyped(text);
|
|
46
|
+
if (clean.length > 0)
|
|
47
|
+
segments.push({ kind: 'text', text: clean });
|
|
48
|
+
text = '';
|
|
49
|
+
};
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < span.length) {
|
|
52
|
+
if (span.startsWith(ENTER_NEWLINE_FRAME, i)) {
|
|
53
|
+
pushText();
|
|
54
|
+
segments.push({ kind: 'text', text: '\n' });
|
|
55
|
+
i += ENTER_NEWLINE_FRAME.length;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (span[i] === '\n') {
|
|
59
|
+
pushText();
|
|
60
|
+
segments.push({ kind: 'text', text: '\n' });
|
|
61
|
+
i += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
text += span[i];
|
|
65
|
+
i += 1;
|
|
66
|
+
}
|
|
67
|
+
pushText();
|
|
68
|
+
return segments;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Split an `input` string into ordered text runs and newline runs.
|
|
72
|
+
*
|
|
73
|
+
* Text-only by construction (paste frames are `mergeWithPasteRuns`'s job), so
|
|
74
|
+
* the paste id never has to exist here -- `PromptInput` allocates ids for paste
|
|
75
|
+
* segments while building the reducer's action, exactly as the paste branch
|
|
76
|
+
* does (P1-5: the reducer stays pure and this stays a function of its input).
|
|
77
|
+
*/
|
|
78
|
+
export function splitEnterFrames(input) {
|
|
79
|
+
return splitEnterSpan(input);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Merge newline runs with the paste frames that share the same `input`.
|
|
83
|
+
*
|
|
84
|
+
* Ink drains its buffer in one size-less `read()`, so the filter's TWO writes
|
|
85
|
+
* (one per frame family) can arrive as ONE chunk carrying both a paste frame
|
|
86
|
+
* and an Enter frame. Neither splitter alone preserves that order --
|
|
87
|
+
* `splitPasteFrames` sanitises `\n` out of its text runs, and this module's
|
|
88
|
+
* spans treat a paste payload as ordinary text -- so the merge walks the
|
|
89
|
+
* ORIGINAL string once: paste frames become paste segments (payload through
|
|
90
|
+
* `sanitisePaste`, as `splitPasteFrames` does), and the spans between them are
|
|
91
|
+
* segmented with the SAME walker `splitEnterFrames` uses, which is what keeps
|
|
92
|
+
* the two views from disagreeing about where a newline sits.
|
|
93
|
+
*
|
|
94
|
+
* Paste segments carry NO id here (P1-5); the caller assigns one per segment
|
|
95
|
+
* only when it builds the reducer's action.
|
|
96
|
+
*/
|
|
97
|
+
export function mergeWithPasteRuns(input, runs) {
|
|
98
|
+
if (!input.includes(PASTE_OPEN))
|
|
99
|
+
return runs;
|
|
100
|
+
const merged = [];
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < input.length) {
|
|
103
|
+
const open = input.indexOf(PASTE_OPEN, i);
|
|
104
|
+
if (open === -1) {
|
|
105
|
+
merged.push(...splitEnterSpan(input.slice(i)));
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
merged.push(...splitEnterSpan(input.slice(i, open)));
|
|
109
|
+
const bodyStart = open + PASTE_OPEN.length;
|
|
110
|
+
const close = input.indexOf(PASTE_CLOSE, bodyStart);
|
|
111
|
+
const body = close === -1 ? input.slice(bodyStart) : input.slice(bodyStart, close);
|
|
112
|
+
const payload = sanitisePaste(body);
|
|
113
|
+
if (payload.length > 0)
|
|
114
|
+
merged.push({ kind: 'paste', text: payload });
|
|
115
|
+
if (close === -1)
|
|
116
|
+
break;
|
|
117
|
+
i = close + PASTE_CLOSE.length;
|
|
118
|
+
}
|
|
119
|
+
return merged;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Remove every Enter frame for a consumer that has no newline model of its
|
|
123
|
+
* own, replacing each frame with `'\n'` (tui-shift-enter-copy-queue 3.5, the
|
|
124
|
+
* I-12 analogue for this frame family).
|
|
125
|
+
*
|
|
126
|
+
* NO OTHER SANITISATION, unlike `stripPasteFrames`: the overlay call sites wrap
|
|
127
|
+
* this INSIDE their existing `stripPasteFrames(...)` call, and the composite
|
|
128
|
+
* keeps that function's byte-level guarantees. Composed the other way round a
|
|
129
|
+
* lone NUL frame would survive as its payload letter; composed this way the
|
|
130
|
+
* frame's newline lands exactly where a bare Ctrl+J lands today.
|
|
131
|
+
*/
|
|
132
|
+
export function stripEnterFrames(input) {
|
|
133
|
+
if (!hasEnterFrame(input))
|
|
134
|
+
return input;
|
|
135
|
+
return input.split(ENTER_NEWLINE_FRAME).join('\n');
|
|
136
|
+
}
|
|
@@ -29,29 +29,6 @@ interface AssistantEntryProps {
|
|
|
29
29
|
* `thought for 0s` is a lie where a bare `thought` is merely terse.
|
|
30
30
|
*/
|
|
31
31
|
thinkingMs?: number;
|
|
32
|
-
/**
|
|
33
|
-
* Whether pressing `Ctrl+T` would actually reveal THIS entry's body — i.e.
|
|
34
|
-
* `mode === 'fullscreen'` (D-16 / P0-2).
|
|
35
|
-
*
|
|
36
|
-
* In inline mode a settled entry has already been printed into Ink's
|
|
37
|
-
* `<Static>`, which cannot un-print or re-print (`Transcript.tsx:9-10`), and
|
|
38
|
-
* the settled boundary is held monotonic precisely so an entry never flows back
|
|
39
|
-
* out. With thinking hidden by default the body is never printed there AT ALL,
|
|
40
|
-
* so a row reading `ctrl+t to show` would be an instruction that provably does
|
|
41
|
-
* nothing. The row degrades to `* thought for 12s` instead — still honest that
|
|
42
|
-
* reasoning happened and how long it took, and silent about a key that cannot
|
|
43
|
-
* deliver.
|
|
44
|
-
*/
|
|
45
|
-
revealable?: boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Inline mode only (L5): keep at most this many rows of a LIVE entry, so the
|
|
48
|
-
* non-`<Static>` region cannot reach `stdout.rows`. `undefined` means no
|
|
49
|
-
* clamp on the ANSWER BODY, which is what the full-screen branch passes — so
|
|
50
|
-
* the rendered answer there is byte-identical to a pre-feature build. (The
|
|
51
|
-
* thinking block has its own streaming clamp below and is not covered by that
|
|
52
|
-
* statement.)
|
|
53
|
-
*/
|
|
54
|
-
liveClampRows?: number;
|
|
55
32
|
theme: Theme;
|
|
56
33
|
caps: TermCapabilities;
|
|
57
34
|
}
|
|
@@ -22,7 +22,7 @@ import { splitLinesCached } from '../render-cache.js';
|
|
|
22
22
|
import { clampLiveText } from '../live-clamp.js';
|
|
23
23
|
import { formatDuration } from '../../agent/usage.js';
|
|
24
24
|
function AssistantEntryImpl(props) {
|
|
25
|
-
const { text, thinking, thinkingOpen, thinkingVisible, streaming, aborted, thinkingMs,
|
|
25
|
+
const { text, thinking, thinkingOpen, thinkingVisible, streaming, aborted, thinkingMs, theme, caps, } = props;
|
|
26
26
|
const glyphs = pickGlyphs(caps);
|
|
27
27
|
const hasThinking = !!thinking && thinking.trim().length > 0;
|
|
28
28
|
const hasText = text.trim().length > 0;
|
|
@@ -32,12 +32,11 @@ function AssistantEntryImpl(props) {
|
|
|
32
32
|
// full-screen mode: it is scratch reasoning the user reads the newest end of,
|
|
33
33
|
// and an unbounded one is the single easiest way to push a live entry past the
|
|
34
34
|
// whole viewport.
|
|
35
|
-
const thinkingRows =
|
|
35
|
+
const thinkingRows = streaming ? STREAMING_THINKING_ROWS : undefined;
|
|
36
36
|
const clampedThinking = hasThinking && thinkingRows !== undefined
|
|
37
37
|
? clampLiveText(thinking, thinkingRows)
|
|
38
38
|
: { text: thinking ?? '', hiddenRows: 0 };
|
|
39
|
-
|
|
40
|
-
return (_jsxs(Box, { flexDirection: "column", children: [hasThinking && thinkingVisible && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [glyphs.thinking, " thinking", thinkingOpen ? glyphs.ellipsis : ''] }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [clampedThinking.hiddenRows > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.ellipsis, " ", clampedThinking.hiddenRows, " earlier", ' ', clampedThinking.hiddenRows === 1 ? 'line' : 'lines'] })), splitLinesCached(clampedThinking.text).map((line, i) => (_jsx(Text, { color: theme.thinking, dimColor: true, children: line }, i)))] })] })), hasThinking && !thinkingVisible && !streaming && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.thinking, " thought", thinkingMs !== undefined ? ` for ${formatDuration(thinkingMs)}` : '', revealable ? ` ${glyphs.midDot} ctrl+t to show` : ''] })), (hasText || streaming) && (_jsxs(Box, { flexDirection: "column", children: [body.hiddenRows > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.ellipsis, " ", body.hiddenRows, " earlier", ' ', body.hiddenRows === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), hasText ? (_jsx(Markdown, { text: body.text, theme: theme, caps: caps })) : (_jsx(Text, { color: theme.muted, children: glyphs.ellipsis })), aborted && _jsx(Text, { color: theme.noticeWarn, children: "[aborted]" })] }))] }));
|
|
39
|
+
return (_jsxs(Box, { flexDirection: "column", children: [hasThinking && thinkingVisible && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [glyphs.thinking, " thinking", thinkingOpen ? glyphs.ellipsis : ''] }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [clampedThinking.hiddenRows > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.ellipsis, " ", clampedThinking.hiddenRows, " earlier", ' ', clampedThinking.hiddenRows === 1 ? 'line' : 'lines'] })), splitLinesCached(clampedThinking.text).map((line, i) => (_jsx(Text, { color: theme.thinking, dimColor: true, children: line }, i)))] })] })), hasThinking && !thinkingVisible && !streaming && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.thinking, " thought", thinkingMs !== undefined ? ` for ${formatDuration(thinkingMs)}` : '', ` ${glyphs.midDot} ctrl+t to show`] })), (hasText || streaming) && (_jsxs(Box, { flexDirection: "column", children: [hasText ? (_jsx(Markdown, { text: text, theme: theme, caps: caps })) : (_jsx(Text, { color: theme.muted, children: glyphs.ellipsis })), aborted && _jsx(Text, { color: theme.noticeWarn, children: "[aborted]" })] }))] }));
|
|
41
40
|
}
|
|
42
41
|
/** Rows of live thinking kept on screen while an answer streams. */
|
|
43
42
|
const STREAMING_THINKING_ROWS = 24;
|
|
@@ -32,12 +32,6 @@ export declare const DIFF_COLLAPSED_LINES = 12;
|
|
|
32
32
|
export interface DiffViewProps {
|
|
33
33
|
patch: FilePatch;
|
|
34
34
|
expanded?: boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Inline mode only (L5): ceiling on the rows a LIVE card contributes to the
|
|
37
|
-
* non-`<Static>` region. `undefined` — what the full-screen branch passes —
|
|
38
|
-
* means no clamp.
|
|
39
|
-
*/
|
|
40
|
-
liveClampRows?: number;
|
|
41
35
|
theme: Theme;
|
|
42
36
|
}
|
|
43
37
|
/** One rendered row: a patch line, or the `@@` separator between two hunks. */
|
|
@@ -56,7 +50,7 @@ interface DiffRow {
|
|
|
56
50
|
* disagree.
|
|
57
51
|
*/
|
|
58
52
|
export declare function diffRows(patch: FilePatch): DiffRow[];
|
|
59
|
-
declare function DiffViewImpl({ patch, expanded,
|
|
53
|
+
declare function DiffViewImpl({ patch, expanded, theme, }: DiffViewProps): React.ReactElement;
|
|
60
54
|
/**
|
|
61
55
|
* `React.memo` with the DEFAULT comparator, matching `ToolCard`'s boundary.
|
|
62
56
|
* `patch` qualifies as a prop only because the reducer writes it ONCE at
|
|
@@ -86,9 +86,9 @@ function gutterWidth(rows) {
|
|
|
86
86
|
max = row.num;
|
|
87
87
|
return Math.min(GUTTER_MAX, Math.max(GUTTER_MIN, String(max).length));
|
|
88
88
|
}
|
|
89
|
-
function DiffViewImpl({ patch, expanded,
|
|
89
|
+
function DiffViewImpl({ patch, expanded, theme, }) {
|
|
90
90
|
const rows = diffRows(patch);
|
|
91
|
-
const ceiling =
|
|
91
|
+
const ceiling = expanded ? rows.length : DIFF_COLLAPSED_LINES;
|
|
92
92
|
const visible = ceiling >= rows.length ? rows : rows.slice(0, Math.max(0, ceiling));
|
|
93
93
|
const hidden = rows.length - visible.length;
|
|
94
94
|
const width = gutterWidth(rows);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a queued steering message (tui-shift-enter-copy-queue 5.4).
|
|
3
|
+
*
|
|
4
|
+
* ONE ROW, ALWAYS. `wrap="truncate"` -- the run-status-row width policy, for
|
|
5
|
+
* the height-estimate reason DiffView learned the hard way: a queued message
|
|
6
|
+
* can be arbitrarily long, and an entry whose wrapped height disagrees with its
|
|
7
|
+
* estimate is exactly the one the virtual window never re-measures.
|
|
8
|
+
*
|
|
9
|
+
* The glyph and the rail belong to `EntryFrame`, exactly as for every other
|
|
10
|
+
* entry; this component renders content only.
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import type { Theme } from '../theme.js';
|
|
14
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
15
|
+
declare function QueuedEntryImpl({ text, theme, caps, }: {
|
|
16
|
+
text: string;
|
|
17
|
+
theme: Theme;
|
|
18
|
+
caps: TermCapabilities;
|
|
19
|
+
}): React.ReactElement;
|
|
20
|
+
/**
|
|
21
|
+
* `React.memo` with the DEFAULT comparator, like every sibling entry card: the
|
|
22
|
+
* reducer rebuilds the entry object only when it changes, and a `queued` entry
|
|
23
|
+
* changes exactly twice -- once when appended, once when it is rewritten into a
|
|
24
|
+
* `user` entry and stops being this component's problem.
|
|
25
|
+
*/
|
|
26
|
+
export declare const QueuedEntry: React.MemoExoticComponent<typeof QueuedEntryImpl>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Render a queued steering message (tui-shift-enter-copy-queue 5.4).
|
|
4
|
+
*
|
|
5
|
+
* ONE ROW, ALWAYS. `wrap="truncate"` -- the run-status-row width policy, for
|
|
6
|
+
* the height-estimate reason DiffView learned the hard way: a queued message
|
|
7
|
+
* can be arbitrarily long, and an entry whose wrapped height disagrees with its
|
|
8
|
+
* estimate is exactly the one the virtual window never re-measures.
|
|
9
|
+
*
|
|
10
|
+
* The glyph and the rail belong to `EntryFrame`, exactly as for every other
|
|
11
|
+
* entry; this component renders content only.
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { Text } from 'ink';
|
|
15
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
16
|
+
function QueuedEntryImpl({ text, theme, caps, }) {
|
|
17
|
+
const glyphs = pickGlyphs(caps);
|
|
18
|
+
const lines = text.split('\n');
|
|
19
|
+
const first = lines[0] ?? '';
|
|
20
|
+
const extra = lines.length - 1;
|
|
21
|
+
const suffix = extra > 0 ? ` ${glyphs.ellipsis}+${extra} line${extra === 1 ? '' : 's'}` : '';
|
|
22
|
+
return (_jsx(Text, { wrap: "truncate", color: theme.muted, children: `Queue: ${first}${suffix}` }));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `React.memo` with the DEFAULT comparator, like every sibling entry card: the
|
|
26
|
+
* reducer rebuilds the entry object only when it changes, and a `queued` entry
|
|
27
|
+
* changes exactly twice -- once when appended, once when it is rewritten into a
|
|
28
|
+
* `user` entry and stops being this component's problem.
|
|
29
|
+
*/
|
|
30
|
+
export const QueuedEntry = React.memo(QueuedEntryImpl);
|
|
@@ -37,12 +37,6 @@ interface ToolCardProps {
|
|
|
37
37
|
isError?: boolean;
|
|
38
38
|
expanded?: boolean;
|
|
39
39
|
reducedMotion?: boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Inline mode only (L5): ceiling on the rows a LIVE card contributes to the
|
|
42
|
-
* non-`<Static>` region. `undefined` — what the full-screen branch passes —
|
|
43
|
-
* means no clamp and byte-identical output.
|
|
44
|
-
*/
|
|
45
|
-
liveClampRows?: number;
|
|
46
40
|
/**
|
|
47
41
|
* The sanitised live tail while the tool runs
|
|
48
42
|
* (agent-activity-presentation-live §3.3.4).
|