@aragon-agent/cli 0.6.14 → 0.6.16
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 +14 -0
- package/README.md +295 -238
- package/dist/agent/controller.d.ts +37 -1
- package/dist/agent/controller.js +195 -45
- package/dist/agent/plan-prompt.d.ts +1 -1
- package/dist/agent/plan-prompt.js +4 -3
- package/dist/agent/project-guidance-prompt.d.ts +4 -0
- package/dist/agent/project-guidance-prompt.js +104 -0
- package/dist/agent/reducer.d.ts +2 -0
- package/dist/agent/reducer.js +4 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/cli.js +49 -2
- package/dist/commands/builtins.js +26 -28
- package/dist/compaction/child.d.ts +9 -1
- package/dist/compaction/child.js +39 -13
- package/dist/compaction/command.js +20 -10
- package/dist/compaction/compactor.d.ts +33 -204
- package/dist/compaction/compactor.js +311 -726
- package/dist/compaction/context-command.js +14 -3
- package/dist/compaction/digest.d.ts +15 -51
- package/dist/compaction/digest.js +113 -148
- package/dist/compaction/limits.d.ts +19 -142
- package/dist/compaction/limits.js +20 -147
- package/dist/compaction/memory-identity.d.ts +16 -0
- package/dist/compaction/memory-identity.js +63 -0
- package/dist/compaction/memory-input.d.ts +25 -0
- package/dist/compaction/memory-input.js +106 -0
- package/dist/compaction/memory-render.d.ts +4 -0
- package/dist/compaction/memory-render.js +47 -0
- package/dist/compaction/memory-validation.d.ts +21 -0
- package/dist/compaction/memory-validation.js +268 -0
- package/dist/compaction/memory.d.ts +79 -0
- package/dist/compaction/memory.js +118 -0
- package/dist/compaction/meter.d.ts +14 -10
- package/dist/compaction/meter.js +53 -19
- package/dist/compaction/operation.d.ts +18 -0
- package/dist/compaction/operation.js +68 -0
- package/dist/compaction/pressure.d.ts +5 -20
- package/dist/compaction/pressure.js +13 -23
- package/dist/compaction/summarize-call.d.ts +3 -16
- package/dist/compaction/summarize-call.js +20 -20
- package/dist/compaction/summary-prompt.d.ts +29 -66
- package/dist/compaction/summary-prompt.js +132 -178
- package/dist/compaction/types.d.ts +27 -4
- package/dist/compaction/wiring.d.ts +10 -25
- package/dist/compaction/wiring.js +56 -66
- package/dist/config/app-paths.d.ts +10 -0
- package/dist/config/app-paths.js +12 -0
- package/dist/config/env.d.ts +2 -1
- package/dist/config/env.js +13 -1
- package/dist/config/load.d.ts +9 -0
- package/dist/config/load.js +14 -0
- package/dist/config/model-windows.d.ts +91 -0
- package/dist/config/model-windows.js +181 -0
- package/dist/config/schema.d.ts +18 -1
- package/dist/config/schema.js +2 -1
- package/dist/exec/events.d.ts +8 -0
- package/dist/exec/index.js +13 -6
- package/dist/exec/runner.d.ts +8 -0
- package/dist/exec/runner.js +26 -2
- package/dist/input/csiu-keys.d.ts +66 -0
- package/dist/input/csiu-keys.js +170 -0
- package/dist/input/keyboard-enhancement.d.ts +42 -0
- package/dist/input/keyboard-enhancement.js +65 -0
- package/dist/input/stdin-filter.d.ts +9 -0
- package/dist/input/stdin-filter.js +45 -9
- package/dist/input/win32-input-mode.d.ts +61 -0
- package/dist/input/win32-input-mode.js +251 -0
- package/dist/session/persist.d.ts +3 -0
- package/dist/session/persist.js +9 -0
- package/dist/session/store.js +2 -0
- package/dist/session/validate-session.js +34 -0
- package/dist/team/subagent.js +8 -0
- package/dist/ui/App.js +218 -116
- package/dist/ui/BottomStatusRow.d.ts +5 -10
- package/dist/ui/BottomStatusRow.js +7 -11
- package/dist/ui/Composer.d.ts +4 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +6 -2
- package/dist/ui/Header.js +7 -6
- package/dist/ui/PromptInput.d.ts +3 -1
- package/dist/ui/PromptInput.js +32 -41
- package/dist/ui/StatusBar.d.ts +6 -0
- package/dist/ui/StatusBar.js +8 -7
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +3 -1
- package/dist/ui/entries/CompactionCard.js +7 -5
- package/dist/ui/frame-differ.js +27 -0
- package/dist/ui/frame-observer.js +18 -5
- package/dist/ui/interaction-copy.d.ts +73 -73
- package/dist/ui/interaction-copy.js +30 -38
- package/dist/ui/interaction-hints.d.ts +8 -1
- package/dist/ui/interaction-hints.js +56 -69
- package/dist/ui/layout/AppShell.d.ts +6 -2
- package/dist/ui/layout/AppShell.js +2 -2
- package/dist/ui/layout/OverlayFrame.d.ts +2 -0
- package/dist/ui/layout/OverlayFrame.js +23 -14
- package/dist/ui/layout/ScrollViewport.d.ts +3 -1
- package/dist/ui/layout/ScrollViewport.js +10 -2
- package/dist/ui/layout/budget.d.ts +25 -26
- package/dist/ui/layout/budget.js +29 -22
- package/dist/ui/layout/document-layout.d.ts +2 -2
- package/dist/ui/layout/document-layout.js +2 -3
- package/dist/ui/layout/queue-layout.js +4 -4
- package/dist/ui/layout/status-detail-layout.d.ts +9 -0
- package/dist/ui/layout/status-detail-layout.js +56 -0
- package/dist/ui/layout/status-layout.d.ts +19 -2
- package/dist/ui/layout/status-layout.js +95 -84
- package/dist/ui/layout/wrap-rows.d.ts +2 -0
- package/dist/ui/layout/wrap-rows.js +21 -7
- package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
- package/dist/ui/overlays/ConfirmDialog.js +22 -3
- package/dist/ui/overlays/HelpOverlay.js +9 -2
- package/dist/ui/overlays/ModelPicker.d.ts +5 -8
- package/dist/ui/overlays/ModelPicker.js +44 -10
- package/dist/ui/overlays/ModelProfileEditor.js +2 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
- package/dist/ui/overlays/ModelProfilePicker.js +4 -2
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
- package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
- package/dist/ui/overlays/QuestionOverlay.js +45 -32
- package/dist/ui/overlays/QueueOverlay.js +2 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
- package/dist/ui/overlays/SettingsScreen.js +10 -3
- package/dist/ui/run-metrics.d.ts +23 -0
- package/dist/ui/run-metrics.js +27 -0
- package/dist/ui/status-feedback.d.ts +33 -0
- package/dist/ui/status-feedback.js +36 -0
- package/dist/ui/stdout-frame-writer.js +28 -0
- package/package.json +2 -2
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Renders an already allocated detail row; it never chooses visibility or animation. */
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import type { Theme } from './theme.js';
|
|
4
|
-
import type {
|
|
5
|
-
import { type ActionHintInput } from './interaction-hints.js';
|
|
4
|
+
import type { StatusLinePlan } from './layout/status-layout.js';
|
|
6
5
|
export interface BottomStatusRowProps {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** Legacy assembly props are accepted but never mount an activity spinner. */
|
|
10
|
-
activity?: React.ReactNode;
|
|
11
|
-
activityGlyph?: React.ReactElement | null;
|
|
12
|
-
update?: React.ReactNode;
|
|
6
|
+
plan: StatusLinePlan;
|
|
7
|
+
columns: number;
|
|
13
8
|
theme: Theme;
|
|
14
9
|
}
|
|
15
|
-
export declare function BottomStatusRow(
|
|
10
|
+
export declare function BottomStatusRow({ plan, columns, theme }: BottomStatusRowProps): React.ReactElement;
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
interactionPhase: props.activity ? 'running' : 'idle' };
|
|
9
|
-
const clauses = buildActionClauses({ ...hints, toast: hints.toast ?? toast?.text,
|
|
10
|
-
updateAvailable: hints.updateAvailable ?? Boolean(props.update) });
|
|
11
|
-
return _jsx(Box, { height: 1, width: hints.cols, flexShrink: 0, children: _jsxs(Text, { color: toast?.level === 'error' ? props.theme.noticeError : props.theme.muted, children: [' ', clauses.join(' | ')] }) });
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Renders an already allocated detail row; it never chooses visibility or animation. */
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
export function BottomStatusRow({ plan, columns, theme }) {
|
|
6
|
+
const tones = { normal: theme.primary, muted: theme.muted, warning: theme.noticeWarn, error: theme.noticeError };
|
|
7
|
+
return _jsxs(Box, { height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: " " }) }), plan.fields.map((field, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: theme.muted, children: plan.separator }) }), _jsx(Box, { width: field.cells, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: tones[field.tone], children: field.text }) })] }, field.id))] });
|
|
12
8
|
}
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface ComposerProps {
|
|
|
20
20
|
/** Compatibility prop; activity is now rendered by the global StatusBar. */
|
|
21
21
|
runRow?: RunRowProps | null;
|
|
22
22
|
cols?: number;
|
|
23
|
+
terminalRows?: number;
|
|
24
|
+
statusExpanded?: boolean;
|
|
23
25
|
deleteDisambiguated?: boolean;
|
|
24
26
|
onCompletionContextChange?: (context: 'none' | 'slash' | 'file') => void;
|
|
25
27
|
cursorVisible?: boolean;
|
|
@@ -47,7 +49,7 @@ export interface ComposerProps {
|
|
|
47
49
|
popupMaxRows?: number;
|
|
48
50
|
popupMaxHeight?: number;
|
|
49
51
|
onPopupRowsChange?: (rows: number) => void;
|
|
50
|
-
/**
|
|
52
|
+
/** Compatibility prop; history position is displayed by Header. */
|
|
51
53
|
scrolledLines?: number;
|
|
52
54
|
/** Compatibility prop; the global status and action rows show live services. */
|
|
53
55
|
services?: number;
|
|
@@ -86,5 +88,5 @@ declare function hintText(opts: {
|
|
|
86
88
|
}): string;
|
|
87
89
|
/** Compatibility export for existing hint contract tests. */
|
|
88
90
|
export declare const hintTextForTest: typeof hintText;
|
|
89
|
-
export declare function Composer({ deleteDisambiguated, onCompletionContextChange, cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange,
|
|
91
|
+
export declare function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
90
92
|
export {};
|
package/dist/ui/Composer.js
CHANGED
|
@@ -36,7 +36,7 @@ function hintText(opts) {
|
|
|
36
36
|
}
|
|
37
37
|
/** Compatibility export for existing hint contract tests. */
|
|
38
38
|
export const hintTextForTest = hintText;
|
|
39
|
-
export function Composer({ deleteDisambiguated, onCompletionContextChange, cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange,
|
|
39
|
+
export function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
|
|
40
40
|
const [hasDraft, setHasDraft] = useState(false);
|
|
41
41
|
const onDraftChange = useCallback((next) => {
|
|
42
42
|
setHasDraft(next.hasDraft);
|
|
@@ -54,5 +54,5 @@ export function Composer({ deleteDisambiguated, onCompletionContextChange, cols,
|
|
|
54
54
|
: hasDraft
|
|
55
55
|
? theme.focusBorder ?? theme.primary
|
|
56
56
|
: theme.idleBorder ?? theme.border;
|
|
57
|
-
return (_jsx(Box, { ref: measureRef, flexDirection: "column", flexShrink: 0, children: _jsx(PromptInput, { deleteDisambiguated: deleteDisambiguated, onCompletionContextChange: onCompletionContextChange, cols: cols, cursorVisible: cursorVisible, onInteraction: onInteraction, isActive: isActive, reducedMotion: reducedMotion, onEscape: onEscape, onEscapeDismiss: onEscapeDismiss, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, borderColor: borderColor, onDraftChange: onDraftChange, onNotice: onNotice
|
|
57
|
+
return (_jsx(Box, { ref: measureRef, width: cols, flexDirection: "column", flexShrink: 0, children: _jsx(PromptInput, { deleteDisambiguated: deleteDisambiguated, onCompletionContextChange: onCompletionContextChange, cols: cols, terminalRows: terminalRows, statusExpanded: statusExpanded, cursorVisible: cursorVisible, onInteraction: onInteraction, isActive: isActive, reducedMotion: reducedMotion, onEscape: onEscape, onEscapeDismiss: onEscapeDismiss, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, borderColor: borderColor, onDraftChange: onDraftChange, onNotice: onNotice }) }));
|
|
58
58
|
}
|
package/dist/ui/Header.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** One-row brand bar
|
|
1
|
+
/** One-row brand bar with a fixed-width history and detail affordance. */
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import type { Theme } from './theme.js';
|
|
4
4
|
import type { TermCapabilities } from './capabilities.js';
|
|
@@ -11,6 +11,10 @@ interface HeaderProps {
|
|
|
11
11
|
variant: HeaderVariant;
|
|
12
12
|
theme: Theme;
|
|
13
13
|
caps: TermCapabilities;
|
|
14
|
+
columns?: number;
|
|
15
|
+
statusExpanded?: boolean;
|
|
16
|
+
scrolledLines?: number;
|
|
17
|
+
overlayOpen?: boolean;
|
|
14
18
|
}
|
|
15
|
-
export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, }: HeaderProps): React.ReactElement;
|
|
19
|
+
export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns, statusExpanded, scrolledLines, overlayOpen }: HeaderProps): React.ReactElement;
|
|
16
20
|
export {};
|
package/dist/ui/Header.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { Logo } from './Logo.js';
|
|
5
|
-
export function Header({ cwd, provider, model, hasKey, variant, theme, caps, }) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, statusExpanded = false, scrolledLines = 0, overlayOpen = false }) {
|
|
6
|
+
const scroll = Number.isFinite(scrolledLines) && scrolledLines > 0
|
|
7
|
+
? (scrolledLines > 9999 ? '^9999+' : '^' + Math.floor(scrolledLines)) + (overlayOpen ? ' ' : ' ') : '';
|
|
8
|
+
const detail = overlayOpen ? (statusExpanded ? 'Details on' : 'Details off')
|
|
9
|
+
: '^G ' + (statusExpanded ? 'less' : 'more');
|
|
10
|
+
return _jsxs(Box, { flexDirection: "row", height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: Math.max(0, columns - 18), flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: variant === 'mini' ? 'mini' : 'bar', theme: theme, caps: caps }), variant !== 'mini' && _jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.accent, children: [' ', provider, ":", model] }), _jsxs(Text, { color: theme.muted, children: [' ', basename(cwd) || cwd] }), _jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [' ', hasKey ? theme.symbols.keyOn : theme.symbols.keyOff] })] })] }) }), _jsx(Box, { width: 18, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: scroll + detail }) })] });
|
|
10
11
|
}
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface CommandOption {
|
|
|
33
33
|
}
|
|
34
34
|
interface PromptInputProps {
|
|
35
35
|
cols?: number;
|
|
36
|
+
terminalRows?: number;
|
|
37
|
+
statusExpanded?: boolean;
|
|
36
38
|
cursorVisible?: boolean;
|
|
37
39
|
onInteraction?: () => void;
|
|
38
40
|
isActive: boolean;
|
|
@@ -157,5 +159,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
157
159
|
end: number;
|
|
158
160
|
query: string;
|
|
159
161
|
} | null;
|
|
160
|
-
export declare function PromptInput({ cols: colsProp, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, onNotice,
|
|
162
|
+
export declare function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, onNotice, }: PromptInputProps): React.ReactElement;
|
|
161
163
|
export {};
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -21,6 +21,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
21
21
|
*/
|
|
22
22
|
import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
23
23
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
24
|
+
import { popupCapacity } from './layout/budget.js';
|
|
24
25
|
import { glob as tinyGlob } from 'tinyglobby';
|
|
25
26
|
import { pickGlyphs } from './glyphs.js';
|
|
26
27
|
import { PromptCaret } from './PromptCaret.js';
|
|
@@ -243,7 +244,7 @@ function isControlSeq(input) {
|
|
|
243
244
|
// ---------------------------------------------------------------------------
|
|
244
245
|
// Component
|
|
245
246
|
// ---------------------------------------------------------------------------
|
|
246
|
-
export function PromptInput({ cols: colsProp, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated = false, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, borderColor, onDraftChange, onNotice,
|
|
247
|
+
export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded = false, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated = false, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, borderColor, onDraftChange, onNotice, }) {
|
|
247
248
|
const { stdout } = useStdout();
|
|
248
249
|
const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
|
|
249
250
|
const { buffer, cursor, historyIndex, dismissed, sel } = editor;
|
|
@@ -260,30 +261,6 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
|
|
|
260
261
|
? fileMatches.map((p) => ({ label: p }))
|
|
261
262
|
: [];
|
|
262
263
|
const clampedSel = popupItems.length > 0 ? Math.min(sel, popupItems.length - 1) : 0;
|
|
263
|
-
const popupLayout = buildAutocompleteLayout({
|
|
264
|
-
itemCount: isActive ? popupItems.length : 0,
|
|
265
|
-
selected: clampedSel,
|
|
266
|
-
maxRows: popupMaxRows,
|
|
267
|
-
maxHeight: popupMaxHeight,
|
|
268
|
-
});
|
|
269
|
-
const popupVisible = isActive && popupLayout.rowCount > 0;
|
|
270
|
-
// Report on every mount, including zero. Cleanup belongs to a separate
|
|
271
|
-
// effect so changing height does not publish an intermediate zero budget.
|
|
272
|
-
useLayoutEffect(() => {
|
|
273
|
-
onPopupRowsChange?.(popupLayout.rowCount);
|
|
274
|
-
}, [onPopupRowsChange, popupLayout.rowCount]);
|
|
275
|
-
useLayoutEffect(() => () => onPopupRowsChange?.(0), [onPopupRowsChange]);
|
|
276
|
-
const completion = popupVisible ? popupKind ?? 'none' : 'none';
|
|
277
|
-
const completionCallback = useRef(onCompletionContextChange);
|
|
278
|
-
completionCallback.current = onCompletionContextChange;
|
|
279
|
-
const lastCompletion = useRef();
|
|
280
|
-
useLayoutEffect(() => {
|
|
281
|
-
if (!onCompletionContextChange || lastCompletion.current === completion)
|
|
282
|
-
return;
|
|
283
|
-
lastCompletion.current = completion;
|
|
284
|
-
onCompletionContextChange?.(completion);
|
|
285
|
-
}, [onCompletionContextChange, completion]);
|
|
286
|
-
useLayoutEffect(() => () => completionCallback.current?.('none'), []);
|
|
287
264
|
// --- Debounced `@file` scan. ---------------------------------------------
|
|
288
265
|
useEffect(() => {
|
|
289
266
|
if (fileQuery === null || fileQuery.length < 1) {
|
|
@@ -322,13 +299,12 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
|
|
|
322
299
|
// the trap `BottomStatusRow.tsx:5-27` documents.
|
|
323
300
|
const glyphs = pickGlyphs(caps);
|
|
324
301
|
const cols = colsProp ?? stdout?.columns ?? 80;
|
|
325
|
-
const terminalRows = stdout?.rows ?? 24;
|
|
326
|
-
const chip = scrollChip(scrolledLines, cols, glyphs);
|
|
302
|
+
const terminalRows = terminalRowsProp ?? stdout?.rows ?? 24;
|
|
327
303
|
// Err NARROW. Wrapping wider than the box Ink gives us would make Ink wrap a
|
|
328
304
|
// second time, and then `layout.rows.length` is a lie — which is the one thing
|
|
329
305
|
// this module may not be.
|
|
330
306
|
const chromeCols = 2 /* border */ + 2 /* paddingX */ + 2 /* marker */;
|
|
331
|
-
const baseCols = Math.max(8, cols - chromeCols
|
|
307
|
+
const baseCols = Math.max(8, cols - chromeCols);
|
|
332
308
|
const measure = (wrapCols) => layoutComposer({
|
|
333
309
|
buffer,
|
|
334
310
|
cursor,
|
|
@@ -356,6 +332,31 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
|
|
|
356
332
|
const preferredColumnWidth = useRef();
|
|
357
333
|
useLayoutEffect(() => { preferredColumnWidth.current = undefined; }, [usableCols]);
|
|
358
334
|
const draftRowCount = Math.max(1, layout.rows.length);
|
|
335
|
+
const menuCapacity = Math.min(popupMaxHeight ?? Infinity, popupCapacity(terminalRows, draftRowCount, statusExpanded));
|
|
336
|
+
const popupLayout = buildAutocompleteLayout({
|
|
337
|
+
itemCount: isActive ? popupItems.length : 0,
|
|
338
|
+
selected: clampedSel,
|
|
339
|
+
maxRows: popupMaxRows,
|
|
340
|
+
maxHeight: menuCapacity,
|
|
341
|
+
});
|
|
342
|
+
const popupVisible = isActive && popupLayout.rowCount > 0;
|
|
343
|
+
// Report on every mount, including zero. Cleanup belongs to a separate
|
|
344
|
+
// effect so changing height does not publish an intermediate zero budget.
|
|
345
|
+
useLayoutEffect(() => {
|
|
346
|
+
onPopupRowsChange?.(popupLayout.rowCount);
|
|
347
|
+
}, [onPopupRowsChange, popupLayout.rowCount]);
|
|
348
|
+
useLayoutEffect(() => () => onPopupRowsChange?.(0), [onPopupRowsChange]);
|
|
349
|
+
const completion = popupVisible ? popupKind ?? 'none' : 'none';
|
|
350
|
+
const completionCallback = useRef(onCompletionContextChange);
|
|
351
|
+
completionCallback.current = onCompletionContextChange;
|
|
352
|
+
const lastCompletion = useRef();
|
|
353
|
+
useLayoutEffect(() => {
|
|
354
|
+
if (!onCompletionContextChange || lastCompletion.current === completion)
|
|
355
|
+
return;
|
|
356
|
+
lastCompletion.current = completion;
|
|
357
|
+
onCompletionContextChange?.(completion);
|
|
358
|
+
}, [onCompletionContextChange, completion]);
|
|
359
|
+
useLayoutEffect(() => () => completionCallback.current?.('none'), []);
|
|
359
360
|
const caretResetKey = useMemo(() => ({}), [buffer, cursor, isActive, usableCols]);
|
|
360
361
|
// --- Draft presence and height, reported only on a transition. -----------
|
|
361
362
|
// Notifying on every keystroke would re-render the whole App tree per key. The
|
|
@@ -363,7 +364,7 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
|
|
|
363
364
|
// (R-6), so widening this callback does not widen how often it fires.
|
|
364
365
|
const hasDraft = buffer.length > 0;
|
|
365
366
|
const lastReportedDraft = useRef(null);
|
|
366
|
-
|
|
367
|
+
useLayoutEffect(() => {
|
|
367
368
|
if (!onDraftChange)
|
|
368
369
|
return;
|
|
369
370
|
const last = lastReportedDraft.current;
|
|
@@ -385,7 +386,7 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
|
|
|
385
386
|
const selectedIndex = Math.min(state.sel, (suggestions?.length ?? 0) - 1);
|
|
386
387
|
const suggestionLayout = buildAutocompleteLayout({
|
|
387
388
|
itemCount: isActive ? suggestions?.length ?? 0 : 0,
|
|
388
|
-
selected: selectedIndex, maxRows: popupMaxRows, maxHeight:
|
|
389
|
+
selected: selectedIndex, maxRows: popupMaxRows, maxHeight: menuCapacity,
|
|
389
390
|
});
|
|
390
391
|
const selected = suggestionLayout.rowCount > 0 ? suggestions?.[selectedIndex] : undefined;
|
|
391
392
|
const text = selected?.label ?? expandPastes(state.buffer, state.pastes);
|
|
@@ -605,16 +606,6 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
|
|
|
605
606
|
// The overflow indicator rides the LAST rendered row rather than
|
|
606
607
|
// taking one of its own: an extra row would put the composer's
|
|
607
608
|
// real height one above the `draftRows` it just reported (I-8).
|
|
608
|
-
_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { children: node }) }), _jsx(Box, { flexShrink: 0, width: overflow.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: overflow.text }) })] }, i)) : (_jsx(Text, { children: node }, i)))) })
|
|
609
|
-
// `alignSelf="flex-start"` pins the chip to the FIRST row of a multi-line
|
|
610
|
-
// draft. Yoga's default `stretch` sizes this box to the full height of the
|
|
611
|
-
// editor; it renders on the top row either way today, and relying on that
|
|
612
|
-
// is how a later `justifyContent` edit moves it without failing anything.
|
|
613
|
-
//
|
|
614
|
-
// `width={chip.cells}` is a FIXED cell, not the chip's natural width
|
|
615
|
-
// (P2-4). The chip is the only thing on this row whose width changes while
|
|
616
|
-
// the user is typing — `↓ 9` becomes `↓ 10` becomes `↓ 100` — and every
|
|
617
|
-
// change would otherwise re-wrap a draft that fills the line.
|
|
618
|
-
_jsx(Box, { flexShrink: 0, alignSelf: "flex-start", marginLeft: 1, width: chip.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: chip.text }) }))] }));
|
|
609
|
+
_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { children: node }) }), _jsx(Box, { flexShrink: 0, width: overflow.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: overflow.text }) })] }, i)) : (_jsx(Text, { children: node }, i)))) })] }));
|
|
619
610
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupVisible && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows, layout: popupLayout })), (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: borderColor, paddingX: 1, children: inputRow }))] }));
|
|
620
611
|
}
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -7,7 +7,13 @@ import type { ContextUsageSnapshot } from '../compaction/types.js';
|
|
|
7
7
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
8
8
|
import type { PendingSteering } from '../agent/queued-messages.js';
|
|
9
9
|
import { type RunPhase } from './layout/status-layout.js';
|
|
10
|
+
import type { EscapeAction, StatusFeedback } from './status-feedback.js';
|
|
10
11
|
export interface StatusBarProps {
|
|
12
|
+
columns: number;
|
|
13
|
+
speedKnown: boolean;
|
|
14
|
+
feedback?: StatusFeedback;
|
|
15
|
+
escapeAction?: EscapeAction;
|
|
16
|
+
interruptPhase?: 'ready' | 'armed' | 'stopping';
|
|
11
17
|
runPhase?: RunPhase;
|
|
12
18
|
activeTool?: {
|
|
13
19
|
name: string;
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/** A single row of complete, measured status fields. */
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { Box, Text
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
5
|
import { pickGlyphs } from './glyphs.js';
|
|
6
6
|
import { liveSpinner } from './ActivityLine.js';
|
|
7
|
-
import {
|
|
7
|
+
import { planPrimaryStatusFields } from './layout/status-layout.js';
|
|
8
8
|
/** An invisible leading byte change forces Ink to repaint on Ctrl+L. */
|
|
9
9
|
const redrawChar = (nonce) => nonce % 2 ? String.fromCharCode(0x00a0) : ' ';
|
|
10
10
|
export function StatusBar(props) {
|
|
11
|
-
const
|
|
12
|
-
const cols = stdout?.columns ?? 80;
|
|
11
|
+
const cols = props.columns;
|
|
13
12
|
const phase = props.runPhase ?? (props.status === 'running' ? 'waiting' : 'idle');
|
|
14
13
|
const active = phase !== 'idle' || !!props.compactionActive?.inFlight;
|
|
15
14
|
const glyph = pickGlyphs(props.caps).spinnerStill;
|
|
16
15
|
const spinner = active ? liveSpinner(props.reducedMotion ?? false, props.caps) : null;
|
|
17
|
-
const
|
|
16
|
+
const plan = planPrimaryStatusFields({
|
|
18
17
|
columns: cols, phase, pendingCount: props.pendingSteering?.length ?? 0,
|
|
19
18
|
context: props.context, usageTotal: props.usageTotal,
|
|
20
19
|
thinkingLevel: props.thinkingLevel, elapsedMs: props.elapsedMs, tokPerSec: props.tokPerSec,
|
|
21
|
-
activeTool: props.activeTool, runOutcome: props.runOutcome, spinner:
|
|
20
|
+
activeTool: props.activeTool, runOutcome: props.runOutcome, spinner: glyph,
|
|
21
|
+
speedKnown: props.speedKnown, feedback: props.feedback, escapeAction: props.escapeAction,
|
|
22
|
+
interruptPhase: props.interruptPhase,
|
|
22
23
|
waitingForConfirmation: props.waitingForConfirmation, stopping: props.stopping,
|
|
23
24
|
compacting: props.compactionActive?.inFlight, retryActive: props.retryActive,
|
|
24
25
|
mode: props.agentMode, pendingMode: props.pendingAgentMode, servicesActive: props.servicesActive,
|
|
@@ -27,7 +28,7 @@ export function StatusBar(props) {
|
|
|
27
28
|
});
|
|
28
29
|
const tones = { normal: props.theme.primary, muted: props.theme.muted,
|
|
29
30
|
warning: props.theme.noticeWarn, error: props.theme.noticeError };
|
|
30
|
-
return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width:
|
|
31
|
+
return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), plan.fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: props.theme.muted, children: plan.separator }) }), _jsx(Box, { width: item.cells, flexShrink: 0, children: _jsx(Text, { color: tones[item.tone], wrap: "truncate", children: item.id === 'phase' && active
|
|
31
32
|
? _jsxs(_Fragment, { children: [spinner ?? glyph, item.text.slice(glyph.length)] })
|
|
32
33
|
: item.text }) })] }, item.id))] });
|
|
33
34
|
}
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -98,7 +98,7 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
98
98
|
case 'fast':
|
|
99
99
|
return frame(entry.status === 'failed' ? glyphs.toolError : glyphs.teamAgent, fastCardColor(entry.status, theme), _jsx(FastCard, { reviewIndex: entry.reviewIndex, model: entry.model, status: entry.status, text: entry.text, detail: entry.detail, turn: entry.turn, durationMs: entry.durationMs, live: entry.live, reducedMotion: reducedMotion, theme: theme, caps: caps }));
|
|
100
100
|
case 'compaction':
|
|
101
|
-
return frame(entry.applied || entry.live ? glyphs.compaction : glyphs.toolError, compactionCardColor(entry, theme), _jsx(CompactionCard, { index: entry.index, trigger: entry.trigger, mode: entry.mode, applied: entry.applied, reason: entry.reason, messagesBefore: entry.messagesBefore, messagesAfter: entry.messagesAfter, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter, summary: entry.summary, model: entry.model, durationMs: entry.durationMs, tailRelief: entry.tailRelief,
|
|
101
|
+
return frame(entry.applied || entry.live ? glyphs.compaction : glyphs.toolError, compactionCardColor(entry, theme), _jsx(CompactionCard, { index: entry.index, decision: entry.decision, memoryVersion: entry.memoryVersion, trigger: entry.trigger, mode: entry.mode, applied: entry.applied, reason: entry.reason, messagesBefore: entry.messagesBefore, messagesAfter: entry.messagesAfter, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter, summary: entry.summary, model: entry.model, durationMs: entry.durationMs, tailRelief: entry.tailRelief,
|
|
102
102
|
// ONLY WHILE LIVE, and only when the clock has ticked at least once:
|
|
103
103
|
// `nowSec` is `undefined` until then, and a settled card must keep
|
|
104
104
|
// rendering `durationMs` rather than a figure that would drift.
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
import React from 'react';
|
|
23
23
|
import type { Theme } from '../theme.js';
|
|
24
24
|
import type { TermCapabilities } from '../capabilities.js';
|
|
25
|
-
import type { CompactionMode, CompactionUiTrigger } from '../../compaction/types.js';
|
|
25
|
+
import type { CompactionDecision, CompactionMode, CompactionUiTrigger } from '../../compaction/types.js';
|
|
26
26
|
export interface CompactionCardProps {
|
|
27
|
+
decision?: CompactionDecision;
|
|
28
|
+
memoryVersion?: 2;
|
|
27
29
|
index: number;
|
|
28
30
|
trigger: CompactionUiTrigger;
|
|
29
31
|
mode: CompactionMode;
|
|
@@ -59,7 +59,7 @@ function headline(props, arrow, dot) {
|
|
|
59
59
|
? `compacting context #${index}${dot}${props.model}${elapsed}`
|
|
60
60
|
: `compacting context #${index}${elapsed}`;
|
|
61
61
|
}
|
|
62
|
-
if (!props.applied) {
|
|
62
|
+
if (!props.applied || props.tokensAfter >= props.tokensBefore) {
|
|
63
63
|
const why = props.reason ? `: ${props.reason}` : '';
|
|
64
64
|
return `context not compacted #${index}${why}`;
|
|
65
65
|
}
|
|
@@ -90,8 +90,8 @@ function detailLine(props) {
|
|
|
90
90
|
return '';
|
|
91
91
|
return `summarizing with ${props.model} - esc to cancel`;
|
|
92
92
|
}
|
|
93
|
-
if (!props.applied)
|
|
94
|
-
return
|
|
93
|
+
if (!props.applied || props.tokensAfter >= props.tokensBefore)
|
|
94
|
+
return 'History preserved.';
|
|
95
95
|
const dropped = Math.max(0, props.messagesBefore - props.messagesAfter);
|
|
96
96
|
if (props.mode === 'relieved') {
|
|
97
97
|
const n = props.tailRelief?.messages ?? 0;
|
|
@@ -138,7 +138,8 @@ function CompactionCardImpl(props) {
|
|
|
138
138
|
const head = headline(props, glyphs.arrowRight, dot);
|
|
139
139
|
const detail = detailLine(props);
|
|
140
140
|
const relief = reliefLine(props);
|
|
141
|
-
const
|
|
141
|
+
const showMemory = props.memoryVersion !== 2 || expanded === true;
|
|
142
|
+
const rows = summary && showMemory ? summaryRows(summary, expanded === true) : [];
|
|
142
143
|
const totalRows = summary ? summary.split('\n').filter((l) => l.trim().length > 0).length : 0;
|
|
143
144
|
const hidden = Math.max(0, totalRows - rows.length);
|
|
144
145
|
// COLOUR TRACKS `live`, MOTION TRACKS `animate` — one headline, two separate
|
|
@@ -147,7 +148,8 @@ function CompactionCardImpl(props) {
|
|
|
147
148
|
// every run, and folding the two together would grey out every live card for
|
|
148
149
|
// the whole run, losing the status colour along with the motion.
|
|
149
150
|
const animate = live && reducedMotion !== true && caps.unicode;
|
|
150
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "
|
|
151
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.accent, bold: true, children: props.trigger === 'manual' ? 'Manual compaction' : props.trigger === 'overflow'
|
|
152
|
+
? 'Overflow: threshold reached' : 'Automatic: threshold reached' }), _jsxs(Text, { color: live ? color : theme.muted, wrap: caps.unicode ? 'truncate' : 'wrap', children: [animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), props.memoryVersion === 2 && (_jsxs(Text, { color: theme.muted, children: ["Task memory v2", expanded ? '' : ' (ctrl+o to expand)'] })), detail.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: caps.unicode ? 'truncate' : 'wrap', children: detail }) })), relief.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.noticeWarn, children: relief }) })), rows.length > 0 && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [rows.map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))), hidden > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " +", hidden, " more lines (ctrl+o to expand)"] })), hidden === 0 && expanded === true && totalRows > COMPACTION_LIMITS.cardTextRows && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " ctrl+o to collapse"] }))] }))] }));
|
|
151
153
|
}
|
|
152
154
|
/**
|
|
153
155
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
package/dist/ui/frame-differ.js
CHANGED
|
@@ -64,6 +64,18 @@ const ERASE_DOWN = `${CSI}J`;
|
|
|
64
64
|
/** DEC private mode 2026 — begin/end synchronized update (F2, §3.3). */
|
|
65
65
|
const SYNC_BEGIN = `${CSI}?2026h`;
|
|
66
66
|
const SYNC_END = `${CSI}?2026l`;
|
|
67
|
+
/**
|
|
68
|
+
* `ansiEscapes.clearTerminal` — Ink's tall-frame write (`ink.js:121`).
|
|
69
|
+
*
|
|
70
|
+
* When `outputHeight >= stdout.rows` Ink abandons log-update and writes
|
|
71
|
+
* `clearTerminal + frame` in one chunk: no erase-lines prefix, no trailing
|
|
72
|
+
* `\n`. That happens on ANY height shrink, because Ink re-renders
|
|
73
|
+
* synchronously from its own `'resize'` handler while the React tree still
|
|
74
|
+
* holds the pre-resize height (`useTerminalSize` debounces 50 ms). Without
|
|
75
|
+
* recognition the chunk lands in the prefix-less branch and counts as a
|
|
76
|
+
* foreign write — the same §5.6 notice, one ordinary window-drag later.
|
|
77
|
+
*/
|
|
78
|
+
const CLEAR_TERMINAL = `${CSI}2J${CSI}3J${CSI}H`;
|
|
67
79
|
function cursorTo(row) {
|
|
68
80
|
return `${CSI}${row};1H`;
|
|
69
81
|
}
|
|
@@ -294,6 +306,21 @@ export function createFrameDiffer(options) {
|
|
|
294
306
|
return wrapSync(out.join(''));
|
|
295
307
|
};
|
|
296
308
|
const transform = (chunk) => {
|
|
309
|
+
// A+ · Ink's tall-frame write (see CLEAR_TERMINAL above): a shrink makes
|
|
310
|
+
// `outputHeight >= stdout.rows` while the tree still holds the old height,
|
|
311
|
+
// so Ink bypasses log-update and writes clearTerminal + frame in one chunk.
|
|
312
|
+
// The chunk paints the whole frame by itself, so it needs no parsing: pass
|
|
313
|
+
// it through, drop the cache (the screen it described is gone), and do NOT
|
|
314
|
+
// count it — this is Ink's own write at a moment absolute addressing cannot
|
|
315
|
+
// cover, not something behind Ink's back. The next log-update frame lands
|
|
316
|
+
// on an empty cache and is a full repaint, exactly what a cleared screen
|
|
317
|
+
// needs. Checked BEFORE the seed bookkeeping — and it must be, because the
|
|
318
|
+
// tall-frame write bypasses `this.log`: log-update's own
|
|
319
|
+
// `previousLineCount` stays 0, so the first log-update frame after it still
|
|
320
|
+
// carries no erase prefix. THAT write is the session's genuine seed frame
|
|
321
|
+
// (P1-1), and a control write arriving first must not spend its exemption.
|
|
322
|
+
if (chunk.startsWith(CLEAR_TERMINAL))
|
|
323
|
+
return passThrough(false);
|
|
297
324
|
const firstChunk = !seenAnyChunk;
|
|
298
325
|
seenAnyChunk = true;
|
|
299
326
|
const rows = usableRows();
|
|
@@ -9,16 +9,25 @@ function summarizeFrame(chunk, rows, cols) {
|
|
|
9
9
|
const unicode = pickGlyphs({ unicode: true, colorLevel: 0 });
|
|
10
10
|
const cells = new Set(['|', '#', unicode.scrollTrack, unicode.scrollThumb]);
|
|
11
11
|
const plain = lines.map((line) => stripAnsi(line));
|
|
12
|
-
|
|
13
|
-
if (plain.some((line) => stringWidth(line) > cols) ||
|
|
14
|
-
!track.every((line) => stringWidth(line) === cols && cells.has(line.slice(-1))))
|
|
12
|
+
if (plain.some((line) => stringWidth(line) > cols))
|
|
15
13
|
return null;
|
|
16
|
-
|
|
14
|
+
let trackRows = 0;
|
|
15
|
+
// Fixed composer/status slots have no last-column rail. Discover the actual
|
|
16
|
+
// message track, then acknowledge it only against the published Yoga geometry.
|
|
17
|
+
for (const line of plain.slice(1)) {
|
|
18
|
+
if (stringWidth(line) !== cols || !cells.has(line.slice(-1)))
|
|
19
|
+
break;
|
|
20
|
+
trackRows++;
|
|
21
|
+
}
|
|
22
|
+
if (trackRows < 1)
|
|
23
|
+
return null;
|
|
24
|
+
return { columns: cols, rows, trackTop: 2, trackRows };
|
|
17
25
|
}
|
|
18
26
|
/** Observe before differ transformation, but acknowledge only after downstream write returns. */
|
|
19
27
|
export function createFrameObserver(options) {
|
|
20
28
|
const { stdout, terminal, scrollbar } = options;
|
|
21
29
|
let summary = null;
|
|
30
|
+
let summaryRevision;
|
|
22
31
|
let disposed = false;
|
|
23
32
|
let resizeTimer = null;
|
|
24
33
|
const invalidate = () => { summary = null; scrollbar.invalidate(); };
|
|
@@ -37,12 +46,16 @@ export function createFrameObserver(options) {
|
|
|
37
46
|
};
|
|
38
47
|
const match = () => {
|
|
39
48
|
const g = scrollbar.geometry;
|
|
49
|
+
if (summary && g && summaryRevision === undefined)
|
|
50
|
+
summaryRevision = g.revision;
|
|
40
51
|
scrollbar.frameReady = !!summary && !!g && summary.columns === terminal.columns &&
|
|
41
52
|
summary.rows === terminal.rows && g.trackCol === summary.columns &&
|
|
42
|
-
g.trackTop === summary.trackTop && g.trackRows === summary.trackRows
|
|
53
|
+
g.trackTop === summary.trackTop && g.trackRows === summary.trackRows &&
|
|
54
|
+
g.revision === summaryRevision;
|
|
43
55
|
};
|
|
44
56
|
const observe = (chunk) => {
|
|
45
57
|
summary = summarizeFrame(chunk, terminal.rows, terminal.columns);
|
|
58
|
+
summaryRevision = scrollbar.geometry?.revision;
|
|
46
59
|
if (!summary) {
|
|
47
60
|
invalidate();
|
|
48
61
|
return;
|