@aragon-agent/cli 0.6.8 → 0.6.9
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 +38 -37
- package/dist/agent/controller.d.ts +1 -1
- package/dist/agent/reducer.d.ts +7 -113
- package/dist/agent/reducer.js +0 -62
- package/dist/cli.js +48 -149
- 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/logging/install.d.ts +2 -2
- package/dist/logging/install.js +2 -2
- 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 +66 -128
- 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 +43 -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 +4 -7
- 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 +9 -199
- 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/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/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 +1 -0
- package/dist/ui/run-status-row.d.ts +61 -0
- package/dist/ui/run-status-row.js +70 -0
- 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 +1 -57
- package/dist/ui/use-startup-notices.js +3 -54
- 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,33 +1,8 @@
|
|
|
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';
|
|
@@ -45,98 +20,6 @@ import { MeasuredEntry } from './layout/MeasuredEntry.js';
|
|
|
45
20
|
import { useViewportGeometry } from './layout/viewport-geometry.js';
|
|
46
21
|
import { estimateEntryRows, heightKey, selectWindow, VIRTUAL_LIMITS, } from './layout/virtual-window.js';
|
|
47
22
|
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
23
|
/**
|
|
141
24
|
* The value `EntryView` should receive for `nowSec` on THIS entry.
|
|
142
25
|
*
|
|
@@ -168,7 +51,7 @@ function noticeSymbol(level, theme) {
|
|
|
168
51
|
return theme.symbols.warn;
|
|
169
52
|
return theme.symbols.info;
|
|
170
53
|
}
|
|
171
|
-
function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density,
|
|
54
|
+
function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, theme, caps, nowSec, }) {
|
|
172
55
|
const glyphs = pickGlyphs(caps);
|
|
173
56
|
const separation = separationRows(prev, entry, density);
|
|
174
57
|
const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
|
|
@@ -176,32 +59,14 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
176
59
|
case 'user':
|
|
177
60
|
return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme, caps: caps }));
|
|
178
61
|
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
62
|
const animate = entry.streaming && !reducedMotion && caps.unicode;
|
|
198
63
|
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
64
|
// `EntryViewImpl` hands each renderer EXPLICIT SCALAR PROPS, so
|
|
200
65
|
// nothing on the entry reaches a component by itself (P1-6b).
|
|
201
|
-
thinkingMs: entry.thinkingMs,
|
|
66
|
+
thinkingMs: entry.thinkingMs, theme: theme, caps: caps }));
|
|
202
67
|
}
|
|
203
68
|
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,
|
|
69
|
+
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
70
|
// `entry.live` AND NOT `entry.live ?? []` (P2-4): `ToolCard`'s default
|
|
206
71
|
// comparator sees a fresh `[]` as a changed prop, which is why that
|
|
207
72
|
// file already keeps a shared `EMPTY_LINES`.
|
|
@@ -263,10 +128,8 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
|
|
|
263
128
|
a.thinkingVisible === b.thinkingVisible &&
|
|
264
129
|
a.reducedMotion === b.reducedMotion &&
|
|
265
130
|
a.density === b.density &&
|
|
266
|
-
a.mode === b.mode &&
|
|
267
131
|
a.theme === b.theme &&
|
|
268
132
|
a.caps === b.caps &&
|
|
269
|
-
a.liveClampRows === b.liveClampRows &&
|
|
270
133
|
/*
|
|
271
134
|
* ELEVENTH TERM, AND OMITTING IT IS THE TRAP I-L2-1 DESCRIBES ONE COMMENT UP
|
|
272
135
|
* (P1-3). This comparator is a CLOSED LIST: a prop absent from it changes
|
|
@@ -279,27 +142,7 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
|
|
|
279
142
|
*/
|
|
280
143
|
a.nowSec === b.nowSec);
|
|
281
144
|
});
|
|
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, }) {
|
|
145
|
+
export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }) {
|
|
303
146
|
const size = Math.max(1, Math.floor(windowSize));
|
|
304
147
|
const visible = entries.length > size ? entries.slice(-size) : entries;
|
|
305
148
|
const collapsed = entries.length - visible.length;
|
|
@@ -352,39 +195,6 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
|
|
|
352
195
|
}
|
|
353
196
|
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
197
|
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,
|
|
198
|
+
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
199
|
}), selection.trailingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.trailingRows }))] }));
|
|
357
200
|
}
|
|
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
|
-
}
|
|
@@ -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);
|
|
@@ -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).
|
|
@@ -80,7 +80,7 @@ export function toolEntryGlyph(name, caps) {
|
|
|
80
80
|
return toolGlyph(pickGlyphs(caps), name);
|
|
81
81
|
}
|
|
82
82
|
function ToolCardImpl(props) {
|
|
83
|
-
const { name, argsRaw, args, status, preview, patch, durationMs, isError, expanded, reducedMotion,
|
|
83
|
+
const { name, argsRaw, args, status, preview, patch, durationMs, isError, expanded, reducedMotion, live, lastOutputAt, nowSec, theme, caps, } = props;
|
|
84
84
|
const glyphs = pickGlyphs(caps);
|
|
85
85
|
const color = statusColor(status, theme);
|
|
86
86
|
const previewText = preview ?? '';
|
|
@@ -88,12 +88,7 @@ function ToolCardImpl(props) {
|
|
|
88
88
|
// is immutable once written, and this used to reallocate up to 200 strings per
|
|
89
89
|
// card per frame (R3).
|
|
90
90
|
const previewLines = previewText.length > 0 ? splitLinesCached(previewText) : EMPTY_LINES;
|
|
91
|
-
|
|
92
|
-
// The inline clamp only lowers the ceiling — it never changes which end is
|
|
93
|
-
// kept, because `Ctrl+O` on a 5 000-line bash result is precisely how an
|
|
94
|
-
// inline session reaches `ink.js:121` (I-L5-1) and the fix must not also
|
|
95
|
-
// reshuffle what a full-screen user has been reading for four releases.
|
|
96
|
-
const ceiling = Math.min(expanded ? previewLines.length : COLLAPSED_LINES, liveClampRows ?? Number.POSITIVE_INFINITY);
|
|
91
|
+
const ceiling = expanded ? previewLines.length : COLLAPSED_LINES;
|
|
97
92
|
const visibleLines = ceiling >= previewLines.length ? previewLines : previewLines.slice(0, ceiling);
|
|
98
93
|
const hidden = previewLines.length - visibleLines.length;
|
|
99
94
|
const settled = status === 'done' || status === 'error';
|
|
@@ -110,11 +105,7 @@ function ToolCardImpl(props) {
|
|
|
110
105
|
// NOW" (D-25). Head-first while live would freeze on the first eight lines a
|
|
111
106
|
// build ever printed and then never change again.
|
|
112
107
|
const liveRows = live ?? EMPTY_LINES;
|
|
113
|
-
const liveCeiling =
|
|
114
|
-
// Inline only: `liveClampRows` already subtracts one row per live entry to
|
|
115
|
-
// pay for a marker (`Transcript.tsx:534-538`), which is what this card's
|
|
116
|
-
// unconditional footer spends (AC-36 / P2-7).
|
|
117
|
-
liveClampRows ?? Number.POSITIVE_INFINITY);
|
|
108
|
+
const liveCeiling = COLLAPSED_LINES;
|
|
118
109
|
const visibleLive = liveRows.length > liveCeiling ? liveRows.slice(-Math.max(1, liveCeiling)) : liveRows;
|
|
119
110
|
const showLive = !settled && visibleLive.length > 0;
|
|
120
111
|
// THE OUTER GUARD WIDENS WITH THE BRANCH (P2-2). It was `previewLines.length >
|
|
@@ -153,7 +144,7 @@ function ToolCardImpl(props) {
|
|
|
153
144
|
// "constant row count" the exactness its arithmetic claims. A
|
|
154
145
|
// space is also what the terminal itself shows for a blank line.
|
|
155
146
|
// eslint-disable-next-line react/no-array-index-key
|
|
156
|
-
_jsx(Text, { wrap: "truncate", color: theme.muted, children: row.length > 0 ? row : ' ' }, i))), _jsx(Text, { color: theme.muted, children: liveFooter })] })) : showPatch ? (_jsx(DiffView, { patch: patch, expanded: expanded,
|
|
147
|
+
_jsx(Text, { wrap: "truncate", color: theme.muted, children: row.length > 0 ? row : ' ' }, i))), _jsx(Text, { color: theme.muted, children: liveFooter })] })) : showPatch ? (_jsx(DiffView, { patch: patch, expanded: expanded, theme: theme })) : (_jsxs(_Fragment, { children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] })) }))] }));
|
|
157
148
|
}
|
|
158
149
|
/**
|
|
159
150
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
* trailing `CSI K` removes only the residue a longer previous line left, and
|
|
26
26
|
* the trailing SGR reset before it stops a BCE terminal painting that residue
|
|
27
27
|
* in whatever background colour the line ended inside.
|
|
28
|
+
* `CSI K` IS EMITTED ONLY WHEN THE LINE IS NARROWER THAN THE TERMINAL. A row
|
|
29
|
+
* that already fills every column (the unified scrollbar paints the last one)
|
|
30
|
+
* has no residue to clear, and on terminals that keep the cursor ON the last
|
|
31
|
+
* column after writing it (a pending-wrap flag rather than a virtual column
|
|
32
|
+
* N+1) the EL would erase the cell that was just written. `cols` unknown keeps
|
|
33
|
+
* the old always-EL behaviour (`paintLine`).
|
|
28
34
|
* - I-5 · Every emitted batch ends parked on row `H+1`. Ink's own
|
|
29
35
|
* `previousLineCount` is computed from the string IT produced and is
|
|
30
36
|
* unaffected by what we actually wrote, so a later pass-through's
|
|
@@ -56,6 +62,13 @@ export interface FrameDifferOptions {
|
|
|
56
62
|
* on resize. `undefined` ⇒ stand down and pass through.
|
|
57
63
|
*/
|
|
58
64
|
rows: () => number | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Live terminal width, read on every paint and never cached (like `rows`).
|
|
67
|
+
* Absent or non-finite => every changed row ends in `CSI K` (the original
|
|
68
|
+
* behaviour); finite => rows whose display width is >= `cols` skip the EL so
|
|
69
|
+
* the last column (the scrollbar) is never erased (I-4).
|
|
70
|
+
*/
|
|
71
|
+
cols?: () => number | undefined;
|
|
59
72
|
/** Raised once, on the 0→1 edge of `fallbacks` (§5.6). */
|
|
60
73
|
onFirstFallback?: () => void;
|
|
61
74
|
/**
|
package/dist/ui/frame-differ.js
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
* trailing `CSI K` removes only the residue a longer previous line left, and
|
|
26
26
|
* the trailing SGR reset before it stops a BCE terminal painting that residue
|
|
27
27
|
* in whatever background colour the line ended inside.
|
|
28
|
+
* `CSI K` IS EMITTED ONLY WHEN THE LINE IS NARROWER THAN THE TERMINAL. A row
|
|
29
|
+
* that already fills every column (the unified scrollbar paints the last one)
|
|
30
|
+
* has no residue to clear, and on terminals that keep the cursor ON the last
|
|
31
|
+
* column after writing it (a pending-wrap flag rather than a virtual column
|
|
32
|
+
* N+1) the EL would erase the cell that was just written. `cols` unknown keeps
|
|
33
|
+
* the old always-EL behaviour (`paintLine`).
|
|
28
34
|
* - I-5 · Every emitted batch ends parked on row `H+1`. Ink's own
|
|
29
35
|
* `previousLineCount` is computed from the string IT produced and is
|
|
30
36
|
* unaffected by what we actually wrote, so a later pass-through's
|
|
@@ -37,6 +43,8 @@
|
|
|
37
43
|
* the guard being absent is a permanently misaligned screen while the failure
|
|
38
44
|
* mode of it firing spuriously is one extra full repaint.
|
|
39
45
|
*/
|
|
46
|
+
import stringWidth from 'string-width';
|
|
47
|
+
import stripAnsi from 'strip-ansi';
|
|
40
48
|
import { MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
41
49
|
import { matchInkErasePrefix } from './frame-parser.js';
|
|
42
50
|
// ---------------------------------------------------------------------------
|
|
@@ -59,6 +67,23 @@ const SYNC_END = `${CSI}?2026l`;
|
|
|
59
67
|
function cursorTo(row) {
|
|
60
68
|
return `${CSI}${row};1H`;
|
|
61
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* One addressed row: `CUP · SGR0 · line · SGR0 [· CSI K]` (I-4).
|
|
72
|
+
*
|
|
73
|
+
* The trailing `CSI K` clears residue a longer previous line left behind, so it is
|
|
74
|
+
* only meaningful when the line is narrower than the terminal. A line that already
|
|
75
|
+
* fills `cols` columns ends with the cursor in the pending-wrap state, where some
|
|
76
|
+
* terminals run EL from the LAST column and erase the cell just written; those
|
|
77
|
+
* rows get no EL. Width is measured exactly as Ink measures it (`string-width` over
|
|
78
|
+
* the ANSI-stripped text) and only for rows that changed, so it costs at most one
|
|
79
|
+
* measurement per changed viewport row. `cols` unknown => the original always-EL.
|
|
80
|
+
*/
|
|
81
|
+
function paintLine(row, line, cols) {
|
|
82
|
+
const head = cursorTo(row) + SGR_RESET + line + SGR_RESET;
|
|
83
|
+
if (cols !== undefined && stringWidth(stripAnsi(line)) >= cols)
|
|
84
|
+
return head;
|
|
85
|
+
return head + ERASE_TO_EOL;
|
|
86
|
+
}
|
|
62
87
|
/**
|
|
63
88
|
* Rebuild `ansiEscapes.eraseLines(count)` byte for byte.
|
|
64
89
|
*
|
|
@@ -211,13 +236,18 @@ export function createFrameDiffer(options) {
|
|
|
211
236
|
cursorTo(lines.length + 1);
|
|
212
237
|
return emit(wrapSync(payload), lines, raw, lines.length, true);
|
|
213
238
|
};
|
|
239
|
+
/** Terminal width for this paint, or `undefined` when unknown (I-4: always EL). */
|
|
240
|
+
const liveCols = () => {
|
|
241
|
+
const cols = options.cols?.();
|
|
242
|
+
return cols !== undefined && Number.isFinite(cols) ? cols : undefined;
|
|
243
|
+
};
|
|
214
244
|
const diffRepaint = (lines, raw, previous) => {
|
|
215
245
|
const out = [];
|
|
246
|
+
const cols = liveCols();
|
|
216
247
|
for (let i = 0; i < lines.length; i += 1) {
|
|
217
248
|
// I-4: paint, THEN clear the tail. Never `ERASE_LINE` first.
|
|
218
|
-
if (lines[i] !== previous[i])
|
|
219
|
-
out.push(
|
|
220
|
-
}
|
|
249
|
+
if (lines[i] !== previous[i])
|
|
250
|
+
out.push(paintLine(i + 1, lines[i], cols));
|
|
221
251
|
}
|
|
222
252
|
// DEFENSIVE ONLY, not a live path (P2-4). `log-update.js:13-15` returns early
|
|
223
253
|
// on an identical frame, so a chunk that reaches here differs by at least one
|
|
@@ -251,10 +281,10 @@ export function createFrameDiffer(options) {
|
|
|
251
281
|
return '';
|
|
252
282
|
const painted = applyDecorate(rawPrev);
|
|
253
283
|
const out = [];
|
|
284
|
+
const cols = liveCols();
|
|
254
285
|
for (let i = 0; i < painted.length; i += 1) {
|
|
255
|
-
if (painted[i] !== prev[i])
|
|
256
|
-
out.push(
|
|
257
|
-
}
|
|
286
|
+
if (painted[i] !== prev[i])
|
|
287
|
+
out.push(paintLine(i + 1, painted[i], cols));
|
|
258
288
|
}
|
|
259
289
|
prev = painted;
|
|
260
290
|
if (out.length === 0)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const render: (node: import("react").ReactNode, options?: NodeJS.WriteStream | import("ink").RenderOptions) => import("ink").Instance;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
/** Ink 5 caches CI detection at import time and otherwise paints only on exit. */
|
|
3
|
+
async function loadInk() {
|
|
4
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
5
|
+
return import('ink');
|
|
6
|
+
const originalCi = process.env.CI;
|
|
7
|
+
try {
|
|
8
|
+
// A real interactive terminal always needs live frames and resize events.
|
|
9
|
+
// `0` also overrides CI_* / CONTINUOUS_INTEGRATION in Ink's detector.
|
|
10
|
+
process.env.CI = '0';
|
|
11
|
+
return await import('ink');
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
// Commands, subprocesses and the updater must see the original environment.
|
|
15
|
+
if (originalCi === undefined)
|
|
16
|
+
delete process.env.CI;
|
|
17
|
+
else
|
|
18
|
+
process.env.CI = originalCi;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const { render } = await loadInk();
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
/** Stable fullscreen slots preserve the editor during overlays and small resizes. */
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { type RenderMode } from './frame.js';
|
|
4
3
|
export interface AppShellProps {
|
|
5
|
-
mode: RenderMode;
|
|
6
4
|
rows: number;
|
|
7
5
|
cols: number;
|
|
8
6
|
header: React.ReactNode;
|
|
9
7
|
viewport: React.ReactNode;
|
|
10
|
-
/** Legacy inline footer; fullscreen assembles these slots inside its viewport. */
|
|
11
|
-
team?: React.ReactNode;
|
|
12
|
-
/** Kept source-compatible; fullscreen rail is owned by ScrollViewport. */
|
|
13
|
-
rail?: React.ReactNode;
|
|
14
|
-
/** Inline-only plan strip. */
|
|
15
|
-
strip?: React.ReactNode;
|
|
16
8
|
toast: React.ReactNode;
|
|
17
|
-
composer: React.ReactNode;
|
|
18
9
|
status: React.ReactNode;
|
|
19
10
|
inactive?: boolean;
|
|
20
11
|
placeholder?: React.ReactNode;
|
|
21
12
|
}
|
|
22
|
-
export declare function AppShell({
|
|
13
|
+
export declare function AppShell({ rows, cols, header, viewport, toast, status, inactive, placeholder, }: AppShellProps): React.ReactElement;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box } from 'ink';
|
|
3
3
|
import { frameHeight } from './frame.js';
|
|
4
|
-
export function AppShell({
|
|
5
|
-
if (mode === 'inline') {
|
|
6
|
-
return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, strip, team, toast, composer, status] }));
|
|
7
|
-
}
|
|
4
|
+
export function AppShell({ rows, cols, header, viewport, toast, status, inactive = false, placeholder, }) {
|
|
8
5
|
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsxs(Box, { display: inactive ? 'none' : 'flex', flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: header }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), _jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: toast }), _jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: status })] }), _jsx(Box, { display: inactive ? 'flex' : 'none', height: frameHeight(rows), overflow: "hidden", children: placeholder })] }));
|
|
9
6
|
}
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OverlayFrame (spec §4.4) — the shared shell for all four overlays.
|
|
3
|
-
*
|
|
4
|
-
* TWO CONTENT MODES, and the distinction is load-bearing:
|
|
5
|
-
*
|
|
6
|
-
* - mode A (`rows`): the caller hands over an array of elements that are each
|
|
7
|
-
* exactly one row and `wrap="truncate"`. The frame slices them and OWNS the
|
|
8
|
-
* scroll keys. Used by Help and Settings.
|
|
9
|
-
*
|
|
10
|
-
* - mode B (`children`): the content manages itself. The frame draws the title,
|
|
11
|
-
* the border and the footer, and touches NOTHING else — no slicing, no
|
|
12
|
-
* arrow-key handling, no position indicator.
|
|
13
|
-
*
|
|
14
|
-
* Mode B exists because of `ink-select-input`: it already windows itself via
|
|
15
|
-
* `limit` and registers its own `useInput` with `isFocused` defaulting to true.
|
|
16
|
-
* Ink dispatches a key to EVERY mounted input handler, so a frame that also
|
|
17
|
-
* claimed the arrow keys would move the model picker's selection twice per press
|
|
18
|
-
* — breaking a feature that already works. Slicing it by element would be worse
|
|
19
|
-
* still, since it is a single component rather than a list of rows.
|
|
20
|
-
*
|
|
21
|
-
* `maxRows` may be `Infinity` (the inline render path, which has no fixed frame
|
|
22
|
-
* and therefore no height to fit): the frame then renders everything and shows
|
|
23
|
-
* no position indicator.
|
|
24
|
-
*/
|
|
25
1
|
import React from 'react';
|
|
26
2
|
import type { Theme } from '../theme.js';
|
|
27
3
|
import type { TermCapabilities } from '../capabilities.js';
|
|
@@ -29,7 +5,7 @@ interface OverlayFrameBase {
|
|
|
29
5
|
title: string;
|
|
30
6
|
/** Footer text. Written by the CALLER so each overlay names its own keys. */
|
|
31
7
|
hint: string;
|
|
32
|
-
/** Viewport rows the overlay may occupy
|
|
8
|
+
/** Viewport rows the overlay may occupy. */
|
|
33
9
|
maxRows: number;
|
|
34
10
|
cols?: number;
|
|
35
11
|
theme: Theme;
|