@aragon-agent/cli 0.6.29 → 0.6.31
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 +2123 -2103
- package/README.md +81 -14
- package/dist/agent/project-guidance-prompt.js +20 -20
- package/dist/cli.js +169 -56
- package/dist/commands/builtins.js +4 -0
- package/dist/commands/perf.d.ts +25 -1
- package/dist/commands/perf.js +26 -2
- package/dist/commands/registry.d.ts +6 -0
- package/dist/config/env.js +4 -0
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +12 -1
- package/dist/config/schema.d.ts +27 -0
- package/dist/config/schema.js +9 -0
- package/dist/config/ui-state.d.ts +9 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +122 -62
- package/dist/ui/ScrollbackShell.d.ts +28 -0
- package/dist/ui/ScrollbackShell.js +10 -0
- package/dist/ui/frame-differ.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
- package/dist/ui/overlays/HelpOverlay.js +21 -3
- package/dist/ui/screen.d.ts +16 -0
- package/dist/ui/screen.js +32 -0
- package/dist/ui/scrollback/budget.d.ts +43 -0
- package/dist/ui/scrollback/budget.js +44 -0
- package/dist/ui/scrollback/drain.d.ts +32 -0
- package/dist/ui/scrollback/drain.js +64 -0
- package/dist/ui/scrollback/exit-summary.d.ts +23 -0
- package/dist/ui/scrollback/exit-summary.js +25 -0
- package/dist/ui/scrollback/limits.d.ts +25 -0
- package/dist/ui/scrollback/limits.js +25 -0
- package/dist/ui/scrollback/live-preview.d.ts +26 -0
- package/dist/ui/scrollback/live-preview.js +106 -0
- package/dist/ui/scrollback/renderer.d.ts +38 -0
- package/dist/ui/scrollback/renderer.js +158 -0
- package/dist/ui/scrollback/scrollback-writer.d.ts +74 -0
- package/dist/ui/scrollback/scrollback-writer.js +263 -0
- package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
- package/dist/ui/scrollback/settled-boundary.js +106 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +92 -0
- package/dist/ui/scrollback/use-scrollback-view.js +263 -0
- package/dist/ui/scrollback/writer-shapes.d.ts +61 -0
- package/dist/ui/scrollback/writer-shapes.js +114 -0
- package/dist/ui/stdout-frame-writer.js +12 -2
- package/dist/ui/terminal-plan.d.ts +52 -0
- package/dist/ui/terminal-plan.js +49 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +21 -7
- package/dist/ui/use-startup-notices.d.ts +12 -1
- package/dist/ui/use-startup-notices.js +28 -2
- package/package.json +2 -2
- package/skills/project-indexer/SKILL.md +163 -163
- package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
- package/skills/project-indexer/templates/cleancode-template.md +94 -94
- package/skills/project-indexer/templates/config-template.md +69 -69
- package/skills/project-indexer/templates/index-template.md +129 -129
- package/skills/skill-creator/SKILL.md +96 -96
package/dist/ui/App.js
CHANGED
|
@@ -16,13 +16,13 @@ import { computeCost } from '../agent/usage.js';
|
|
|
16
16
|
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
17
17
|
import { updatePersistedConfig } from '../config/store.js';
|
|
18
18
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
19
|
-
import { bumpSubmitCount, setMouseNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
|
|
19
|
+
import { bumpSubmitCount, setMouseNoticeVersion, setScrollModeNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
|
|
20
20
|
import { getLogger } from '../logging/logger.js';
|
|
21
21
|
import { isTerminalStatus } from '../proc/types.js';
|
|
22
22
|
import { getSecrets } from '../logging/secret-registry.js';
|
|
23
23
|
import { redactText } from '../logging/redact.js';
|
|
24
24
|
import { resolveTuiCapabilities } from './capabilities.js';
|
|
25
|
-
import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
|
|
25
|
+
import { useStartupNotices, MOUSE_NOTICE_VERSION, SCROLL_MODE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
|
|
26
26
|
import { getTheme } from './theme.js';
|
|
27
27
|
import { pickGlyphs } from './glyphs.js';
|
|
28
28
|
import { Header } from './Header.js';
|
|
@@ -38,6 +38,9 @@ import { BottomStatusRow } from './BottomStatusRow.js';
|
|
|
38
38
|
// Only the updater's pure visibility predicate enters the status detail plan.
|
|
39
39
|
import { shouldRenderUpdateLine } from '../update/types.js';
|
|
40
40
|
import { AppShell } from './layout/AppShell.js';
|
|
41
|
+
import { ScrollbackShell } from './ScrollbackShell.js';
|
|
42
|
+
import { useScrollbackView } from './scrollback/use-scrollback-view.js';
|
|
43
|
+
import { buildExitSummary } from './scrollback/exit-summary.js';
|
|
41
44
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
42
45
|
import { buildTodoRailLayout } from './layout/todo-layout.js';
|
|
43
46
|
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
@@ -132,7 +135,11 @@ function seedViewState(seed) {
|
|
|
132
135
|
? viewReducer(base, { type: 'restoreEntries', entries: seed.entries })
|
|
133
136
|
: base;
|
|
134
137
|
}
|
|
135
|
-
export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, initialEntries, session, sessionBridge, sessionNotice, }) {
|
|
138
|
+
export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, scrollMode = 'app', scrollback, initialEntries, session, sessionBridge, sessionNotice, }) {
|
|
139
|
+
const native = scrollMode === 'native' && scrollback !== undefined;
|
|
140
|
+
/** Set when the native writer swallowed repeated tall frames; cleared by a resize. */
|
|
141
|
+
const [guardTripped, setGuardTripped] = useState(false);
|
|
142
|
+
const scrollbackViewRef = useRef(null);
|
|
136
143
|
const { exit } = useApp();
|
|
137
144
|
const { stdout } = useStdout();
|
|
138
145
|
const size = useTerminalSize();
|
|
@@ -164,8 +171,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
164
171
|
* can never step past the window and skip content. The unsafe direction is the
|
|
165
172
|
* one that matters, and it is unreachable.
|
|
166
173
|
*/
|
|
167
|
-
const overlayPage = overlayPageSize(dimensions.viewportRows, cols);
|
|
168
|
-
const tooSmall = dimensions.inactive;
|
|
174
|
+
const overlayPage = overlayPageSize(dimensions.viewportRows + (native ? 1 : 0), cols);
|
|
175
|
+
const tooSmall = dimensions.inactive || (native && guardTripped);
|
|
169
176
|
// --- Render governor (tui-render-performance L4). ------------------------
|
|
170
177
|
//
|
|
171
178
|
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
@@ -1054,6 +1061,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1054
1061
|
nodeVersion: vtInputWarning.nodeVersion,
|
|
1055
1062
|
onSeen: () => setVtInputNoticeVersion(VT_INPUT_NOTICE_VERSION),
|
|
1056
1063
|
}
|
|
1064
|
+
: undefined, native
|
|
1065
|
+
? { onSeen: () => setScrollModeNoticeVersion(SCROLL_MODE_NOTICE_VERSION) }
|
|
1057
1066
|
: undefined);
|
|
1058
1067
|
// --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
|
|
1059
1068
|
useEffect(() => {
|
|
@@ -1491,6 +1500,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1491
1500
|
...(sessionBridge?.branch ? { branch: sessionBridge.branch } : {}),
|
|
1492
1501
|
dispatch: (action) => {
|
|
1493
1502
|
if (action.type === 'resetConversation' || action.type === 'restoreEntries') {
|
|
1503
|
+
scrollbackViewRef.current?.notifyReset(action.type === 'resetConversation' ? 'clear' : 'restore');
|
|
1494
1504
|
metrics.current = createRunMetrics();
|
|
1495
1505
|
manualMetrics.current = createRunMetrics();
|
|
1496
1506
|
setScrollResetKey((n) => n + 1);
|
|
@@ -1541,6 +1551,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1541
1551
|
}
|
|
1542
1552
|
: {}),
|
|
1543
1553
|
...(terminal?.writeForeign ? { writeForeign: terminal.writeForeign } : {}),
|
|
1554
|
+
...(native ? { nativeScroll: true } : {}),
|
|
1544
1555
|
});
|
|
1545
1556
|
/**
|
|
1546
1557
|
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
@@ -2042,7 +2053,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2042
2053
|
setRedrawNonce((n) => n + 1);
|
|
2043
2054
|
return;
|
|
2044
2055
|
}
|
|
2045
|
-
|
|
2056
|
+
// Native mode has no scrollable viewport: the terminal owns scrolling, so these
|
|
2057
|
+
// keys are left alone. An open overlay is application content and keeps them.
|
|
2058
|
+
if (!native || stateRef.current.overlay) {
|
|
2046
2059
|
if (key.pageUp) {
|
|
2047
2060
|
scrollBy('pageUp');
|
|
2048
2061
|
return;
|
|
@@ -2063,10 +2076,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2063
2076
|
if (key.ctrl && (input === 't' || input === 'T')) {
|
|
2064
2077
|
const revealing = !stateRef.current.thinkingVisible;
|
|
2065
2078
|
dispatch({ type: 'toggleThinking' });
|
|
2079
|
+
// Printed history never changes, so say what the toggle actually reaches.
|
|
2080
|
+
const scope = native ? ' Applies to new messages.' : '';
|
|
2066
2081
|
if (!revealing)
|
|
2067
|
-
toast('info',
|
|
2082
|
+
toast('info', `Thinking hidden.${scope}`);
|
|
2068
2083
|
else
|
|
2069
|
-
toast('info',
|
|
2084
|
+
toast('info', `Thinking shown.${scope}`);
|
|
2070
2085
|
return;
|
|
2071
2086
|
}
|
|
2072
2087
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
@@ -2090,7 +2105,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2090
2105
|
e.kind === 'team' ||
|
|
2091
2106
|
e.kind === 'compaction' ||
|
|
2092
2107
|
(e.kind === 'service' && !e.terminal));
|
|
2093
|
-
|
|
2108
|
+
// Native mode cannot rewrite what is already printed, so it PRINTS an expanded
|
|
2109
|
+
// copy as a new block instead of toggling the old one (spec 4.7).
|
|
2110
|
+
if (last && native) {
|
|
2111
|
+
if (!scrollbackViewRef.current?.reprintExpanded(last.id))
|
|
2112
|
+
toast('info', 'Nothing to expand.');
|
|
2113
|
+
}
|
|
2114
|
+
else if (last)
|
|
2094
2115
|
dispatch({ type: 'toggleExpand', id: last.id });
|
|
2095
2116
|
// WIDENED FROM `No tool output to expand.`: with three expandable kinds the
|
|
2096
2117
|
// old wording would be wrong in two of the three empty cases.
|
|
@@ -2243,7 +2264,6 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2243
2264
|
const frameBudget = buildFrameBudget({ rows, cols, draftRows,
|
|
2244
2265
|
popupRows: overlay ? 0 : popupRows, statusExpanded });
|
|
2245
2266
|
const viewportBudget = frameBudget.viewportRows;
|
|
2246
|
-
const overlayMaxRows = viewportBudget;
|
|
2247
2267
|
const composerBaseRows = 0;
|
|
2248
2268
|
const baseRail = buildTodoRailLayout({
|
|
2249
2269
|
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
@@ -2263,9 +2283,49 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2263
2283
|
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
2264
2284
|
teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows: 0, composerBaseRows,
|
|
2265
2285
|
});
|
|
2266
|
-
const { visible:
|
|
2286
|
+
const { visible: railVisible, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
2287
|
+
// No fixed frame means no side rail in native mode (N4); the status chip carries it.
|
|
2288
|
+
const showRail = railVisible && !native;
|
|
2267
2289
|
const transcriptEntries = useMemo(() => selectTranscriptEntries(state.entries, state.pendingSteering), [state.entries, state.pendingSteering]);
|
|
2268
|
-
|
|
2290
|
+
// --- Native scroll (tui-native-terminal-scroll). -----------------------------
|
|
2291
|
+
//
|
|
2292
|
+
// ALWAYS CALLED, INERT WHEN `enabled` IS FALSE: hooks cannot be conditional, and
|
|
2293
|
+
// an inert hook is what keeps `scrollMode: 'app'` byte-identical (I-S9).
|
|
2294
|
+
const viewCols = native ? Math.max(1, cols - 1) : cols;
|
|
2295
|
+
const header = (_jsx(Header, { columns: viewCols, statusExpanded: statusExpanded, scrolledLines: scrolledLines, overlayOpen: overlay !== null, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
|
|
2296
|
+
// The wordmark + getting-started card now live INSIDE the viewport, as its
|
|
2297
|
+
// first block of content, so they scroll away instead of being yanked out of
|
|
2298
|
+
// the header the moment the first message lands.
|
|
2299
|
+
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(native ? Math.max(0, rows - 8) : viewportBudget, native ? viewCols : contentCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: native ? Math.max(0, rows - 8) : viewportBudget, theme: theme, caps: caps })) : null;
|
|
2300
|
+
const scrollbackView = useScrollbackView({
|
|
2301
|
+
enabled: native, bridge: scrollback, entries: state.entries, runActive: running,
|
|
2302
|
+
theme, caps, density: cfg.density, thinkingVisible: state.thinkingVisible,
|
|
2303
|
+
reducedMotion, rows, cols, headerNode: header, openerNode: opener,
|
|
2304
|
+
overlayOpen: overlay !== null, draftRows, popupRows: overlay ? 0 : popupRows,
|
|
2305
|
+
statusExpanded, forceInactive: native && guardTripped,
|
|
2306
|
+
});
|
|
2307
|
+
scrollbackViewRef.current = scrollbackView;
|
|
2308
|
+
// The two hooks `cli.tsx` calls from OUTSIDE React: the exit summary (every exit
|
|
2309
|
+
// path, some of which run after this tree is gone, so it reads refs, never state)
|
|
2310
|
+
// and the writer's tall-frame guard. A resize clears the degraded state.
|
|
2311
|
+
useLayoutEffect(() => {
|
|
2312
|
+
if (!native || !scrollback)
|
|
2313
|
+
return undefined;
|
|
2314
|
+
scrollback.getSummaryRows = () => {
|
|
2315
|
+
const live = controller.getConfig();
|
|
2316
|
+
return buildExitSummary({
|
|
2317
|
+
entries: stateRef.current.entries, usageTotal: stateRef.current.usageTotal,
|
|
2318
|
+
provider: live.provider, model: live.model, elapsedMs: Date.now() - startedAt.current,
|
|
2319
|
+
...(identityRef.current ? { sessionId: identityRef.current.id } : {}), glyphs,
|
|
2320
|
+
});
|
|
2321
|
+
};
|
|
2322
|
+
scrollback.onGuardTrip = () => setGuardTripped(true);
|
|
2323
|
+
return () => { scrollback.onGuardTrip = null; };
|
|
2324
|
+
}, [native, scrollback, controller, glyphs]);
|
|
2325
|
+
useEffect(() => { setGuardTripped(false); }, [rows, cols]);
|
|
2326
|
+
const nativeBudget = native ? scrollbackView.budget : null;
|
|
2327
|
+
const overlayMaxRows = nativeBudget ? nativeBudget.overlayRows : viewportBudget;
|
|
2328
|
+
const overlayNode = overlay === 'queue' ? (_jsx(QueueOverlay, { pending: state.pendingSteering, cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: viewCols, rows: helpRows(theme, caps, native), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { isActive: !tooSmall, registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
|
|
2269
2329
|
provider: cfg.provider,
|
|
2270
2330
|
model: cfg.model,
|
|
2271
2331
|
baseUrl: cfg.baseUrl ?? '',
|
|
@@ -2299,15 +2359,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2299
2359
|
// the save handler cannot disagree about what `(inherit)` means.
|
|
2300
2360
|
...fastSettingsFrom(cfg.fast),
|
|
2301
2361
|
...compactionSettingsFrom(cfg.compaction),
|
|
2302
|
-
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols:
|
|
2362
|
+
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, draft: settingsDraft, probe: probe, onProfileSave: handleProfileSave, onReload: () => controller.createModelSettingsDraft(), onClose: () => dispatch({ type: 'setOverlay', overlay: null }),
|
|
2303
2363
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
2304
2364
|
// while the screen is open shows the new value on the next paint.
|
|
2305
|
-
retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { isActive: !tooSmall, state: confirmState, maxRows: overlayMaxRows, cols:
|
|
2306
|
-
const header = (_jsx(Header, { columns: cols, statusExpanded: statusExpanded, scrolledLines: scrolledLines, overlayOpen: overlay !== null, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
|
|
2307
|
-
// The wordmark + getting-started card now live INSIDE the viewport, as its
|
|
2308
|
-
// first block of content, so they scroll away instead of being yanked out of
|
|
2309
|
-
// the header the moment the first message lands.
|
|
2310
|
-
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: viewportBudget, theme: theme, caps: caps })) : null;
|
|
2365
|
+
retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { isActive: !tooSmall, state: confirmState, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { isActive: !tooSmall, questions: humanRequest.questions, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { isActive: !tooSmall, plan: humanRequest.plan, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
|
|
2311
2366
|
const density = cfg.density;
|
|
2312
2367
|
const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
|
|
2313
2368
|
// The fixed status bar remains visible underneath every overlay and toast.
|
|
@@ -2326,8 +2381,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2326
2381
|
heightsMeasured: heights.stats().measured,
|
|
2327
2382
|
heightsEstimated: heights.stats().estimated,
|
|
2328
2383
|
cols: contentCols,
|
|
2329
|
-
mode: 'fullscreen',
|
|
2330
|
-
viewportRows: viewportBudget,
|
|
2384
|
+
mode: native ? 'native' : 'fullscreen',
|
|
2385
|
+
viewportRows: nativeBudget ? nativeBudget.liveRows : viewportBudget,
|
|
2331
2386
|
offset: scrolledLines,
|
|
2332
2387
|
// A getter: the bridge's `frameReady` and geometry change outside React renders.
|
|
2333
2388
|
get scrollbar() {
|
|
@@ -2376,7 +2431,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2376
2431
|
level: latestToast.level === 'success' ? 'info' : latestToast.level } : undefined,
|
|
2377
2432
|
});
|
|
2378
2433
|
const statusInput = {
|
|
2379
|
-
columns:
|
|
2434
|
+
columns: viewCols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
|
|
2380
2435
|
context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
|
|
2381
2436
|
elapsedMs, tokPerSec, speedKnown, activeTool: state.activeTool, runOutcome: state.runOutcome,
|
|
2382
2437
|
waitingForConfirmation: !!humanRequest || !!confirmState,
|
|
@@ -2392,7 +2447,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2392
2447
|
...feedbackProjection,
|
|
2393
2448
|
};
|
|
2394
2449
|
const details = planStatusDetail({ status: statusInput, model: cfg.model, provider: cfg.provider,
|
|
2395
|
-
hints: { interactionPhase: interactionPhase.current, cols, completion, overlay,
|
|
2450
|
+
hints: { interactionPhase: interactionPhase.current, cols: viewCols, completion, overlay,
|
|
2396
2451
|
selectionPending: selectionController?.hasPendingSelection() ?? false,
|
|
2397
2452
|
copyInFlight: copyState.busy, copyCleanupPending: copyState.cleanupPending,
|
|
2398
2453
|
hasDraft: hasDraftRef.current, services: liveServices,
|
|
@@ -2403,43 +2458,48 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2403
2458
|
const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2404
2459
|
const rail = showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2405
2460
|
const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: overlayNode, footer: _jsxs(_Fragment, { children: [team, _jsx(QueuePanel, { layout: queueLayout, theme: theme })] }), rail: rail, scrollbar: terminal?.scrollbar, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, onGeometryChange: onGeometryChange, geometryKey: `${rows}:${cols}:${viewportBudget}:${frameBudget.composerSlotRows}:${frameBudget.statusRows}:${contentCols}:${overlay}:${tooSmall}`, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: transcriptEntries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }));
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2461
|
+
const detailsNode = _jsx(BottomStatusRow, { plan: details, columns: viewCols, theme: theme });
|
|
2462
|
+
const statusNode = (_jsx(StatusBar, { columns: viewCols, speedKnown: speedKnown, ...feedbackProjection, interruptPhase: interruptGesture.current.phase, pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, runOutcome: state.runOutcome, reducedMotion: reducedMotion, ...(sessionId ? { sessionId } : {}), waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
|
|
2463
|
+
? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2464
|
+
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2465
|
+
// projection of "many sources of truth" into the props layer, and a
|
|
2466
|
+
// caller could update three of them; a single reading cannot be
|
|
2467
|
+
// partially stale. The window and its `known` flag now travel INSIDE
|
|
2468
|
+
// it, resolved by the meter - which is what makes the user's
|
|
2469
|
+
// `contextWindow` override reach the bar.
|
|
2470
|
+
context: state.context, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
|
|
2471
|
+
? {
|
|
2472
|
+
teamActive: {
|
|
2473
|
+
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
2474
|
+
r.phase !== 'failed' && r.phase !== 'aborted').length,
|
|
2475
|
+
total: state.team.runs.length,
|
|
2476
|
+
},
|
|
2477
|
+
}
|
|
2478
|
+
: {}), ...(state.todos && !showRail
|
|
2479
|
+
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
2480
|
+
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
|
|
2481
|
+
// Manual compaction starts before its next snapshot is published.
|
|
2482
|
+
compactionActive: { inFlight: statusInput.compacting ?? false }, ...(controller.isCompactionEnabled()
|
|
2483
|
+
? {
|
|
2484
|
+
gaugeMarks: {
|
|
2485
|
+
warn: cfg.compaction.warnThreshold * 100,
|
|
2486
|
+
high: cfg.compaction.threshold * 100,
|
|
2487
|
+
},
|
|
2488
|
+
}
|
|
2489
|
+
: {}), ...(state.retry
|
|
2490
|
+
? {
|
|
2491
|
+
retryActive: {
|
|
2492
|
+
attempt: state.retry.attempt,
|
|
2493
|
+
max: state.retry.maxRetries,
|
|
2494
|
+
// Recomputed at RENDER from the absolute `resumeAt`, which is
|
|
2495
|
+
// what keeps the chip and the transcript card showing the same
|
|
2496
|
+
// number without either of them owning a second clock.
|
|
2497
|
+
secondsLeft: secondsLeft(state.retry.resumeAt, Date.now()),
|
|
2498
|
+
},
|
|
2499
|
+
}
|
|
2500
|
+
: {}) }));
|
|
2501
|
+
if (nativeBudget) {
|
|
2502
|
+
return (_jsx(ScrollbackShell, { budget: nativeBudget, overlay: overlayNode, liveTailLines: scrollbackView.liveTailLines, composer: composer, status: statusNode, details: detailsNode, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), theme: theme }));
|
|
2503
|
+
}
|
|
2504
|
+
return (_jsx(AppShell, { inactive: tooSmall, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), rows: rows, cols: cols, header: header, viewport: viewport, viewportRows: frameBudget.viewportRows, composer: composer, composerSlotRows: frameBudget.composerSlotRows, statusRows: frameBudget.statusRows, details: detailsNode, status: statusNode }));
|
|
2445
2505
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollbackShell — the bounded LIVE REGION of native-scroll mode
|
|
3
|
+
* (tui-native-terminal-scroll §4.6).
|
|
4
|
+
*
|
|
5
|
+
* The full-screen `AppShell` fills the whole terminal; this shell is only as tall as
|
|
6
|
+
* `budget.liveRows` (the root box height IS that number: giving it the frame height
|
|
7
|
+
* would make the "small region" a whole screen again, and Ink would then repaint a
|
|
8
|
+
* screenful per frame). Everything settled has already been printed into the
|
|
9
|
+
* terminal's own scrollback above it.
|
|
10
|
+
*
|
|
11
|
+
* Top to bottom: [overlay | streaming preview] -> composer -> status (-> details).
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import type { Theme } from './theme.js';
|
|
15
|
+
import type { ScrollbackBudget } from './scrollback/budget.js';
|
|
16
|
+
export interface ScrollbackShellProps {
|
|
17
|
+
budget: ScrollbackBudget;
|
|
18
|
+
/** Shown instead of the preview when an overlay is open. */
|
|
19
|
+
overlay: React.ReactNode | null;
|
|
20
|
+
/** Plain preview rows, bottom-aligned just above the composer. */
|
|
21
|
+
liveTailLines: readonly string[];
|
|
22
|
+
composer: React.ReactNode;
|
|
23
|
+
status: React.ReactNode;
|
|
24
|
+
details?: React.ReactNode;
|
|
25
|
+
placeholder: React.ReactNode;
|
|
26
|
+
theme: Theme;
|
|
27
|
+
}
|
|
28
|
+
export declare function ScrollbackShell({ budget, overlay, liveTailLines, composer, status, details, placeholder, theme, }: ScrollbackShellProps): React.ReactElement;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function ScrollbackShell({ budget, overlay, liveTailLines, composer, status, details, placeholder, theme, }) {
|
|
4
|
+
const width = budget.contentCols;
|
|
5
|
+
if (budget.inactive) {
|
|
6
|
+
return (_jsx(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: budget.liveRows > 0 ? placeholder : null }));
|
|
7
|
+
}
|
|
8
|
+
const tail = liveTailLines.slice(-budget.liveTailRows);
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: [budget.overlayRows > 0 && (_jsx(Box, { flexDirection: "column", height: budget.overlayRows, flexShrink: 0, overflow: "hidden", children: overlay })), budget.liveTailRows > 0 && (_jsx(Box, { flexDirection: "column", height: budget.liveTailRows, flexShrink: 0, justifyContent: "flex-end", overflow: "hidden", children: tail.map((line, index) => (_jsx(Text, { wrap: "truncate", color: theme.muted, children: line }, index))) })), _jsx(Box, { flexDirection: "column", height: budget.composerSlotRows, justifyContent: "flex-end", flexShrink: 0, overflow: "hidden", children: composer }), _jsx(Box, { height: budget.statusRows > 0 ? 1 : 0, flexShrink: 0, overflow: "hidden", children: status }), budget.statusRows === 2 && (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: details }))] }));
|
|
10
|
+
}
|
|
@@ -150,5 +150,17 @@ export interface FrameDiffer {
|
|
|
150
150
|
* appears one frame later instead of never.
|
|
151
151
|
*/
|
|
152
152
|
repaint(): string;
|
|
153
|
+
/**
|
|
154
|
+
* The stream was resized. Optional (tui-native-terminal-scroll §5.1): the
|
|
155
|
+
* full-screen differ does not implement it and is invalidated instead, which is
|
|
156
|
+
* its existing behaviour; the scrollback writer keeps its cache for a reflow
|
|
157
|
+
* estimate.
|
|
158
|
+
*/
|
|
159
|
+
onResize?(): void;
|
|
160
|
+
/**
|
|
161
|
+
* An own write that moves neither a cell nor the cursor (OSC 52 copy). Optional
|
|
162
|
+
* for the same reason: absent, the write invalidates the cache as before.
|
|
163
|
+
*/
|
|
164
|
+
cursorNeutralWrite?(): void;
|
|
153
165
|
}
|
|
154
166
|
export declare function createFrameDiffer(options: FrameDifferOptions): FrameDiffer;
|
|
@@ -14,4 +14,4 @@ import React from 'react';
|
|
|
14
14
|
import type { Theme } from '../theme.js';
|
|
15
15
|
import type { TermCapabilities } from '../capabilities.js';
|
|
16
16
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
17
|
-
export declare function helpRows(theme: Theme, caps: TermCapabilities): React.ReactElement[];
|
|
17
|
+
export declare function helpRows(theme: Theme, caps: TermCapabilities, native?: boolean): React.ReactElement[];
|
|
@@ -2,7 +2,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Text } from 'ink';
|
|
3
3
|
import { pickGlyphs } from '../glyphs.js';
|
|
4
4
|
const KEY_PAD = 24;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Rows that only make sense when the APPLICATION scrolls and selects. In native
|
|
7
|
+
* scroll mode (tui-native-terminal-scroll §4.7) the terminal does both, so they are
|
|
8
|
+
* dropped and one honest row replaces them.
|
|
9
|
+
*/
|
|
10
|
+
const APP_SCROLL_ROWS = new Set([
|
|
11
|
+
'Shift+Up / Shift+Down', 'Wheel', 'Shift+Wheel', 'Drag right edge', '^N',
|
|
12
|
+
'Drag text (left button)', 'Ctrl+C (with a selection)',
|
|
13
|
+
]);
|
|
14
|
+
function keyRows(times, native = false) {
|
|
15
|
+
const all = allKeyRows(times);
|
|
16
|
+
if (!native)
|
|
17
|
+
return all;
|
|
18
|
+
return all
|
|
19
|
+
.filter(([key]) => !APP_SCROLL_ROWS.has(key))
|
|
20
|
+
.map(([key, description]) => key === 'PgUp / PgDn' ? [key, 'Page through an open overlay'] : [key, description])
|
|
21
|
+
.concat([['Scroll', 'Use your terminal (wheel, Shift+PgUp, scrollbar, search)']]);
|
|
22
|
+
}
|
|
23
|
+
function allKeyRows(times) {
|
|
6
24
|
return [
|
|
7
25
|
['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
|
|
8
26
|
['Alt+Enter / Shift+Enter', 'Newline when supported; the host may intercept either binding'],
|
|
@@ -133,13 +151,13 @@ const FAST_ROWS = [
|
|
|
133
151
|
['<fast_review>', 'An automated second opinion from the fast model - advice, not the user'],
|
|
134
152
|
];
|
|
135
153
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
136
|
-
export function helpRows(theme, caps) {
|
|
154
|
+
export function helpRows(theme, caps, native = false) {
|
|
137
155
|
const glyphs = pickGlyphs(caps);
|
|
138
156
|
const rows = [];
|
|
139
157
|
const section = (title) => (_jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: title }, `s-${title}`));
|
|
140
158
|
const pair = (k, d) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.primary, children: k.padEnd(KEY_PAD) }), _jsx(Text, { color: theme.muted, children: d })] }, `r-${k}`));
|
|
141
159
|
rows.push(section('Keybindings'));
|
|
142
|
-
for (const [k, d] of keyRows(glyphs.times))
|
|
160
|
+
for (const [k, d] of keyRows(glyphs.times, native))
|
|
143
161
|
rows.push(pair(k, d));
|
|
144
162
|
rows.push(section('Slash commands'));
|
|
145
163
|
for (const [c, d] of COMMANDS)
|
package/dist/ui/screen.d.ts
CHANGED
|
@@ -71,6 +71,22 @@ export interface AltScreenOptions {
|
|
|
71
71
|
* CI) — writing screen-control sequences into a redirected file corrupts it.
|
|
72
72
|
*/
|
|
73
73
|
export declare function enterAltScreen(stdout: NodeJS.WriteStream | undefined, options?: AltScreenOptions): ScreenHandle;
|
|
74
|
+
export interface NativeScreenOptions {
|
|
75
|
+
/** Same discipline as `AltScreenOptions.bracketedPaste`: a filter that consumes it exists. */
|
|
76
|
+
readonly bracketedPaste?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Native-scroll mode's counterpart of `enterAltScreen` (tui-native-terminal-scroll
|
|
80
|
+
* §4.8 / D6): it deliberately does almost nothing. The main buffer stays in use so
|
|
81
|
+
* the conversation lands in the terminal's own scrollback; there is no alternate
|
|
82
|
+
* screen, no mouse reporting and no DEC 1007 save/restore.
|
|
83
|
+
*
|
|
84
|
+
* The only mode it touches is DEC 2004 (bracketed paste), which changes how input
|
|
85
|
+
* is encoded and has nothing to do with scrolling. Returns the SAME `ScreenHandle`
|
|
86
|
+
* shape, so the four idempotent restore paths in `cli.tsx` need not know which mode
|
|
87
|
+
* is running. A non-TTY stdout is a hard no-op, like `enterAltScreen`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function enterNativeScreen(stdout: NodeJS.WriteStream | undefined, options?: NativeScreenOptions): ScreenHandle;
|
|
74
90
|
/**
|
|
75
91
|
* Replay the session summary into the normal buffer after `restore()`, so the
|
|
76
92
|
* conversation does not evaporate along with the alternate screen.
|
package/dist/ui/screen.js
CHANGED
|
@@ -181,6 +181,38 @@ export function enterAltScreen(stdout, options = { mouse: false }) {
|
|
|
181
181
|
setMouseCapture,
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Native-scroll mode's counterpart of `enterAltScreen` (tui-native-terminal-scroll
|
|
186
|
+
* §4.8 / D6): it deliberately does almost nothing. The main buffer stays in use so
|
|
187
|
+
* the conversation lands in the terminal's own scrollback; there is no alternate
|
|
188
|
+
* screen, no mouse reporting and no DEC 1007 save/restore.
|
|
189
|
+
*
|
|
190
|
+
* The only mode it touches is DEC 2004 (bracketed paste), which changes how input
|
|
191
|
+
* is encoded and has nothing to do with scrolling. Returns the SAME `ScreenHandle`
|
|
192
|
+
* shape, so the four idempotent restore paths in `cli.tsx` need not know which mode
|
|
193
|
+
* is running. A non-TTY stdout is a hard no-op, like `enterAltScreen`.
|
|
194
|
+
*/
|
|
195
|
+
export function enterNativeScreen(stdout, options = {}) {
|
|
196
|
+
if (!stdout || !stdout.isTTY || typeof stdout.write !== 'function')
|
|
197
|
+
return NOOP_HANDLE;
|
|
198
|
+
const bracketedPaste = options.bracketedPaste === true;
|
|
199
|
+
if (bracketedPaste)
|
|
200
|
+
safeWrite(stdout, ENABLE_BRACKETED_PASTE);
|
|
201
|
+
let restored = false;
|
|
202
|
+
return {
|
|
203
|
+
restore() {
|
|
204
|
+
if (restored)
|
|
205
|
+
return;
|
|
206
|
+
restored = true;
|
|
207
|
+
// 2004 is FORCED off rather than saved/restored, for the reason recorded at
|
|
208
|
+
// `DISABLE_BRACKETED_PASTE`. The cursor is shown again because Ink's
|
|
209
|
+
// log-update hides it and a signal kill never gives it back.
|
|
210
|
+
safeWrite(stdout, (bracketedPaste ? DISABLE_BRACKETED_PASTE : '') + SHOW_CURSOR);
|
|
211
|
+
},
|
|
212
|
+
// No mouse reporting in this mode: nothing to hand back or take again.
|
|
213
|
+
setMouseCapture() { },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
184
216
|
/**
|
|
185
217
|
* Replay the session summary into the normal buffer after `restore()`, so the
|
|
186
218
|
* conversation does not evaporate along with the alternate screen.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Height budget of the native-scroll LIVE REGION (tui-native-terminal-scroll §4.6).
|
|
3
|
+
*
|
|
4
|
+
* I-S1 · `liveRows <= max(0, rows - 1)` for every input. Ink writes
|
|
5
|
+
* `clearTerminal` (which contains `ESC[3J`, erasing the user's scrollback) as soon
|
|
6
|
+
* as a frame is as tall as the terminal; a one-row margin is the only thing between
|
|
7
|
+
* a tidy region and data loss, so it is established by construction here and
|
|
8
|
+
* enumerated by a property test rather than trusted.
|
|
9
|
+
*
|
|
10
|
+
* The composer, popup and status rows come from the SAME `buildFrameBudget` the
|
|
11
|
+
* full-screen layout uses, so the two modes cannot disagree about how tall the
|
|
12
|
+
* editor is. What differs is the top: there is no header, so the row it would
|
|
13
|
+
* occupy joins the pool shared by the overlay and the streaming preview.
|
|
14
|
+
*/
|
|
15
|
+
import { type FrameBudgetInput } from '../layout/budget.js';
|
|
16
|
+
export interface ScrollbackBudgetInput extends FrameBudgetInput {
|
|
17
|
+
overlayOpen: boolean;
|
|
18
|
+
/** Preview rows the content would like; the budget grants what fits. */
|
|
19
|
+
liveTailWanted: number;
|
|
20
|
+
/** The writer tripped its tall-frame guard: degrade to the placeholder until a resize. */
|
|
21
|
+
forceInactive?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface ScrollbackBudget {
|
|
24
|
+
rows: number;
|
|
25
|
+
cols: number;
|
|
26
|
+
/** Width everything is rendered and printed at (`cols - 1`, D5). */
|
|
27
|
+
contentCols: number;
|
|
28
|
+
/** Terminal below the 40x12 floor: the region is a one-row placeholder. */
|
|
29
|
+
inactive: boolean;
|
|
30
|
+
statusRows: 0 | 1 | 2;
|
|
31
|
+
composerRows: number;
|
|
32
|
+
composerSlotRows: number;
|
|
33
|
+
composerCols: number;
|
|
34
|
+
popupRows: number;
|
|
35
|
+
popupMaxHeight: number;
|
|
36
|
+
overlayRows: number;
|
|
37
|
+
liveTailRows: number;
|
|
38
|
+
/** Rows above the composer: overlay or preview. */
|
|
39
|
+
topRows: number;
|
|
40
|
+
/** Total live-region height; what Ink renders. */
|
|
41
|
+
liveRows: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function buildScrollbackBudget(input: ScrollbackBudgetInput): ScrollbackBudget;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Height budget of the native-scroll LIVE REGION (tui-native-terminal-scroll §4.6).
|
|
3
|
+
*
|
|
4
|
+
* I-S1 · `liveRows <= max(0, rows - 1)` for every input. Ink writes
|
|
5
|
+
* `clearTerminal` (which contains `ESC[3J`, erasing the user's scrollback) as soon
|
|
6
|
+
* as a frame is as tall as the terminal; a one-row margin is the only thing between
|
|
7
|
+
* a tidy region and data loss, so it is established by construction here and
|
|
8
|
+
* enumerated by a property test rather than trusted.
|
|
9
|
+
*
|
|
10
|
+
* The composer, popup and status rows come from the SAME `buildFrameBudget` the
|
|
11
|
+
* full-screen layout uses, so the two modes cannot disagree about how tall the
|
|
12
|
+
* editor is. What differs is the top: there is no header, so the row it would
|
|
13
|
+
* occupy joins the pool shared by the overlay and the streaming preview.
|
|
14
|
+
*/
|
|
15
|
+
import { buildFrameBudget } from '../layout/budget.js';
|
|
16
|
+
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
17
|
+
export function buildScrollbackBudget(input) {
|
|
18
|
+
const base = buildFrameBudget(input);
|
|
19
|
+
const contentCols = Math.max(1, base.cols - 1);
|
|
20
|
+
if (base.inactive || input.forceInactive === true) {
|
|
21
|
+
return {
|
|
22
|
+
rows: base.rows, cols: base.cols, contentCols, inactive: true, statusRows: 0,
|
|
23
|
+
composerRows: 0, composerSlotRows: 0, composerCols: base.composerCols, popupRows: 0,
|
|
24
|
+
popupMaxHeight: 0, overlayRows: 0, liveTailRows: 0, topRows: 0,
|
|
25
|
+
liveRows: Math.max(0, Math.min(1, base.rows - 1)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// The full-screen frame spends one row on the header; here it joins the pool.
|
|
29
|
+
const remaining = Math.max(0, base.viewportRows + base.headerRows);
|
|
30
|
+
const overlayRows = input.overlayOpen ? remaining : 0;
|
|
31
|
+
const tailCap = Math.min(SCROLLBACK_LIMITS.maxLiveTailRows, Math.floor(base.rows * SCROLLBACK_LIMITS.liveTailRowShare));
|
|
32
|
+
const liveTailRows = input.overlayOpen
|
|
33
|
+
? 0
|
|
34
|
+
: Math.max(0, Math.min(Math.floor(input.liveTailWanted), remaining, tailCap));
|
|
35
|
+
const topRows = overlayRows + liveTailRows;
|
|
36
|
+
const liveRows = topRows + base.composerSlotRows + base.statusRows;
|
|
37
|
+
return {
|
|
38
|
+
rows: base.rows, cols: base.cols, contentCols, inactive: false,
|
|
39
|
+
statusRows: base.statusRows, composerRows: base.composerRows,
|
|
40
|
+
composerSlotRows: base.composerSlotRows, composerCols: base.composerCols,
|
|
41
|
+
popupRows: base.popupRows, popupMaxHeight: base.popupMaxHeight, overlayRows,
|
|
42
|
+
liveTailRows, topRows, liveRows,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static-row feed — the queue between the promotion pipeline (which decides an
|
|
3
|
+
* entry is final) and the scrollback writer (which prints it)
|
|
4
|
+
* (tui-native-terminal-scroll §4.3 "feed").
|
|
5
|
+
*
|
|
6
|
+
* FIFO, bounded. When the writer falls behind and the queue overflows, the OLDEST
|
|
7
|
+
* pending entries are folded into one ASCII marker and counted: a visible, counted
|
|
8
|
+
* omission, never a silent one (I-S2). Entries are taken as whole blocks and never
|
|
9
|
+
* split, so a batch boundary can never land inside one entry's rows.
|
|
10
|
+
*/
|
|
11
|
+
export interface EntryRef {
|
|
12
|
+
id: string;
|
|
13
|
+
kind: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ScrollbackDrain {
|
|
16
|
+
enqueue(rows: readonly string[], meta: EntryRef): void;
|
|
17
|
+
takeAll(): readonly string[];
|
|
18
|
+
/** Whole entries only; always takes at least one when anything is pending. */
|
|
19
|
+
takeUpTo(maxRows: number): readonly string[];
|
|
20
|
+
/** Rows waiting, including a pending overflow marker. */
|
|
21
|
+
pending(): number;
|
|
22
|
+
/** Whole entries waiting (the overflow marker is not an entry). */
|
|
23
|
+
pendingEntries(): number;
|
|
24
|
+
/** Entries folded away by overflow, cumulative. */
|
|
25
|
+
elided(): number;
|
|
26
|
+
onEnqueue: (() => void) | null;
|
|
27
|
+
onDrained: (() => void) | null;
|
|
28
|
+
}
|
|
29
|
+
export declare function overflowMarker(count: number): string;
|
|
30
|
+
export declare function createScrollbackDrain(options?: {
|
|
31
|
+
capacity?: number;
|
|
32
|
+
}): ScrollbackDrain;
|