@aragon-agent/cli 0.6.28 → 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 -1975
- package/README.md +81 -14
- package/dist/agent/project-guidance-prompt.js +20 -20
- package/dist/agent/reducer.js +47 -2
- 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/input/limits.d.ts +20 -0
- package/dist/input/limits.js +20 -0
- package/dist/input/stdin-filter.js +68 -5
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +155 -67
- package/dist/ui/AutocompletePopup.js +1 -1
- package/dist/ui/Header.js +7 -2
- package/dist/ui/Markdown.js +30 -3
- package/dist/ui/PromptInput.js +41 -3
- package/dist/ui/ScrollbackShell.d.ts +28 -0
- package/dist/ui/ScrollbackShell.js +10 -0
- package/dist/ui/Transcript.js +7 -0
- package/dist/ui/frame-differ.d.ts +12 -0
- package/dist/ui/layout/Gutter.js +1 -1
- package/dist/ui/layout/OverlayFrame.d.ts +15 -0
- package/dist/ui/layout/OverlayFrame.js +18 -0
- package/dist/ui/layout/ScrollIndicator.js +10 -3
- package/dist/ui/layout/budget.d.ts +15 -0
- package/dist/ui/layout/budget.js +15 -0
- package/dist/ui/layout/header-hints.d.ts +9 -0
- package/dist/ui/layout/header-hints.js +14 -1
- package/dist/ui/layout/status-layout.js +63 -2
- package/dist/ui/markdown-blocks.d.ts +26 -5
- package/dist/ui/markdown-blocks.js +110 -33
- 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/dist/ui/use-wheel-routing.d.ts +10 -7
- package/dist/ui/use-wheel-routing.js +11 -6
- 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';
|
|
@@ -46,14 +49,14 @@ import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
|
46
49
|
import { useHeightStore } from './use-height-store.js';
|
|
47
50
|
import { useRenderGovernor } from './use-render-governor.js';
|
|
48
51
|
import { readScrollbarPerf, setPerfResetHook, setPerfSnapshotProvider, } from '../commands/perf.js';
|
|
49
|
-
import { buildFrameBudget } from './layout/budget.js';
|
|
50
|
-
import {
|
|
52
|
+
import { buildFrameBudget, POPUP_CAPACITY_SHARE } from './layout/budget.js';
|
|
53
|
+
import { useWheelRouting } from './use-wheel-routing.js';
|
|
51
54
|
import { INDEX_KEY_FRAME } from '../input/limits.js';
|
|
52
55
|
import { INDEX_SKILL_NAME, buildIndexConfirmSummary, hasProjectIndex, indexBuildInvocationArgs, } from '../commands/index-build.js';
|
|
53
56
|
import { installConsoleBridge } from './console-bridge.js';
|
|
54
57
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
55
58
|
import { publishSessionView } from './session-bridge.js';
|
|
56
|
-
import { OverlayFrame } from './layout/OverlayFrame.js';
|
|
59
|
+
import { OverlayFrame, overlayPageSize } from './layout/OverlayFrame.js';
|
|
57
60
|
import { helpRows } from './overlays/HelpOverlay.js';
|
|
58
61
|
import { ModelPicker } from './overlays/ModelPicker.js';
|
|
59
62
|
import { createModelProbe } from '../agent/model-probe.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();
|
|
@@ -148,7 +155,24 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
148
155
|
const [statusExpanded, setStatusExpanded] = useState(false);
|
|
149
156
|
const dimensions = buildFrameBudget({ ...size, draftRows, popupRows, statusExpanded });
|
|
150
157
|
const { rows, cols } = dimensions;
|
|
151
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Rows one PgUp/PgDn - and one Shift+wheel notch - turns inside a controlled
|
|
160
|
+
* overlay (defect 11).
|
|
161
|
+
*
|
|
162
|
+
* ONE VALUE FOR BOTH BRANCHES, derived from the overlay's real window: a
|
|
163
|
+
* constant 8 was 42% of `/help`'s 19-row window, so a shortcut list needed
|
|
164
|
+
* twelve keystrokes and overlapped itself by eleven rows on every turn. The
|
|
165
|
+
* wheel branch gets it through `getOverlayPage`, so the two can never disagree.
|
|
166
|
+
*
|
|
167
|
+
* MEASURED FROM THIS BUDGET, NOT `frameBudget` BELOW. The two agree on every
|
|
168
|
+
* frame except the one that opens an overlay with the completion menu still
|
|
169
|
+
* reported (`frameBudget` zeroes `popupRows` there), and that exception only
|
|
170
|
+
* makes the real window LARGER - so this page can under-turn for one frame but
|
|
171
|
+
* can never step past the window and skip content. The unsafe direction is the
|
|
172
|
+
* one that matters, and it is unreachable.
|
|
173
|
+
*/
|
|
174
|
+
const overlayPage = overlayPageSize(dimensions.viewportRows + (native ? 1 : 0), cols);
|
|
175
|
+
const tooSmall = dimensions.inactive || (native && guardTripped);
|
|
152
176
|
// --- Render governor (tui-render-performance L4). ------------------------
|
|
153
177
|
//
|
|
154
178
|
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
@@ -1037,6 +1061,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1037
1061
|
nodeVersion: vtInputWarning.nodeVersion,
|
|
1038
1062
|
onSeen: () => setVtInputNoticeVersion(VT_INPUT_NOTICE_VERSION),
|
|
1039
1063
|
}
|
|
1064
|
+
: undefined, native
|
|
1065
|
+
? { onSeen: () => setScrollModeNoticeVersion(SCROLL_MODE_NOTICE_VERSION) }
|
|
1040
1066
|
: undefined);
|
|
1041
1067
|
// --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
|
|
1042
1068
|
useEffect(() => {
|
|
@@ -1474,6 +1500,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1474
1500
|
...(sessionBridge?.branch ? { branch: sessionBridge.branch } : {}),
|
|
1475
1501
|
dispatch: (action) => {
|
|
1476
1502
|
if (action.type === 'resetConversation' || action.type === 'restoreEntries') {
|
|
1503
|
+
scrollbackViewRef.current?.notifyReset(action.type === 'resetConversation' ? 'clear' : 'restore');
|
|
1477
1504
|
metrics.current = createRunMetrics();
|
|
1478
1505
|
manualMetrics.current = createRunMetrics();
|
|
1479
1506
|
setScrollResetKey((n) => n + 1);
|
|
@@ -1524,6 +1551,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1524
1551
|
}
|
|
1525
1552
|
: {}),
|
|
1526
1553
|
...(terminal?.writeForeign ? { writeForeign: terminal.writeForeign } : {}),
|
|
1554
|
+
...(native ? { nativeScroll: true } : {}),
|
|
1527
1555
|
});
|
|
1528
1556
|
/**
|
|
1529
1557
|
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
@@ -1764,6 +1792,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1764
1792
|
isPointerCaptured: () => terminal?.scrollbar?.controller?.isCaptured() ?? false,
|
|
1765
1793
|
isActive: () => !tooSmall,
|
|
1766
1794
|
getOverlay: () => stateRef.current.overlay,
|
|
1795
|
+
getOverlayPage: () => overlayPage,
|
|
1767
1796
|
onScroll: scrollBy,
|
|
1768
1797
|
onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
|
|
1769
1798
|
});
|
|
@@ -1975,12 +2004,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1975
2004
|
const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan' || overlay === 'queue';
|
|
1976
2005
|
if (key.pageUp) {
|
|
1977
2006
|
if (controlled)
|
|
1978
|
-
setOverlayScroll((n) => Math.max(0, n -
|
|
2007
|
+
setOverlayScroll((n) => Math.max(0, n - overlayPage));
|
|
1979
2008
|
return;
|
|
1980
2009
|
}
|
|
1981
2010
|
if (key.pageDown) {
|
|
1982
2011
|
if (controlled)
|
|
1983
|
-
setOverlayScroll((n) => n +
|
|
2012
|
+
setOverlayScroll((n) => n + overlayPage);
|
|
1984
2013
|
return;
|
|
1985
2014
|
}
|
|
1986
2015
|
// `settings` keeps Up/Down for field movement — a high-frequency action
|
|
@@ -2024,7 +2053,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2024
2053
|
setRedrawNonce((n) => n + 1);
|
|
2025
2054
|
return;
|
|
2026
2055
|
}
|
|
2027
|
-
|
|
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) {
|
|
2028
2059
|
if (key.pageUp) {
|
|
2029
2060
|
scrollBy('pageUp');
|
|
2030
2061
|
return;
|
|
@@ -2045,10 +2076,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2045
2076
|
if (key.ctrl && (input === 't' || input === 'T')) {
|
|
2046
2077
|
const revealing = !stateRef.current.thinkingVisible;
|
|
2047
2078
|
dispatch({ type: 'toggleThinking' });
|
|
2079
|
+
// Printed history never changes, so say what the toggle actually reaches.
|
|
2080
|
+
const scope = native ? ' Applies to new messages.' : '';
|
|
2048
2081
|
if (!revealing)
|
|
2049
|
-
toast('info',
|
|
2082
|
+
toast('info', `Thinking hidden.${scope}`);
|
|
2050
2083
|
else
|
|
2051
|
-
toast('info',
|
|
2084
|
+
toast('info', `Thinking shown.${scope}`);
|
|
2052
2085
|
return;
|
|
2053
2086
|
}
|
|
2054
2087
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
@@ -2072,7 +2105,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2072
2105
|
e.kind === 'team' ||
|
|
2073
2106
|
e.kind === 'compaction' ||
|
|
2074
2107
|
(e.kind === 'service' && !e.terminal));
|
|
2075
|
-
|
|
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)
|
|
2076
2115
|
dispatch({ type: 'toggleExpand', id: last.id });
|
|
2077
2116
|
// WIDENED FROM `No tool output to expand.`: with three expandable kinds the
|
|
2078
2117
|
// old wording would be wrong in two of the three empty cases.
|
|
@@ -2225,7 +2264,6 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2225
2264
|
const frameBudget = buildFrameBudget({ rows, cols, draftRows,
|
|
2226
2265
|
popupRows: overlay ? 0 : popupRows, statusExpanded });
|
|
2227
2266
|
const viewportBudget = frameBudget.viewportRows;
|
|
2228
|
-
const overlayMaxRows = viewportBudget;
|
|
2229
2267
|
const composerBaseRows = 0;
|
|
2230
2268
|
const baseRail = buildTodoRailLayout({
|
|
2231
2269
|
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
@@ -2245,9 +2283,49 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2245
2283
|
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
2246
2284
|
teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows: 0, composerBaseRows,
|
|
2247
2285
|
});
|
|
2248
|
-
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;
|
|
2249
2289
|
const transcriptEntries = useMemo(() => selectTranscriptEntries(state.entries, state.pendingSteering), [state.entries, state.pendingSteering]);
|
|
2250
|
-
|
|
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: {
|
|
2251
2329
|
provider: cfg.provider,
|
|
2252
2330
|
model: cfg.model,
|
|
2253
2331
|
baseUrl: cfg.baseUrl ?? '',
|
|
@@ -2281,15 +2359,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2281
2359
|
// the save handler cannot disagree about what `(inherit)` means.
|
|
2282
2360
|
...fastSettingsFrom(cfg.fast),
|
|
2283
2361
|
...compactionSettingsFrom(cfg.compaction),
|
|
2284
|
-
}, 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 }),
|
|
2285
2363
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
2286
2364
|
// while the screen is open shows the new value on the next paint.
|
|
2287
|
-
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:
|
|
2288
|
-
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 }));
|
|
2289
|
-
// The wordmark + getting-started card now live INSIDE the viewport, as its
|
|
2290
|
-
// first block of content, so they scroll away instead of being yanked out of
|
|
2291
|
-
// the header the moment the first message lands.
|
|
2292
|
-
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;
|
|
2293
2366
|
const density = cfg.density;
|
|
2294
2367
|
const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
|
|
2295
2368
|
// The fixed status bar remains visible underneath every overlay and toast.
|
|
@@ -2308,8 +2381,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2308
2381
|
heightsMeasured: heights.stats().measured,
|
|
2309
2382
|
heightsEstimated: heights.stats().estimated,
|
|
2310
2383
|
cols: contentCols,
|
|
2311
|
-
mode: 'fullscreen',
|
|
2312
|
-
viewportRows: viewportBudget,
|
|
2384
|
+
mode: native ? 'native' : 'fullscreen',
|
|
2385
|
+
viewportRows: nativeBudget ? nativeBudget.liveRows : viewportBudget,
|
|
2313
2386
|
offset: scrolledLines,
|
|
2314
2387
|
// A getter: the bridge's `frameReady` and geometry change outside React renders.
|
|
2315
2388
|
get scrollbar() {
|
|
@@ -2319,7 +2392,17 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2319
2392
|
},
|
|
2320
2393
|
};
|
|
2321
2394
|
// Completion shares the fixed editor slot and its frame budget.
|
|
2322
|
-
|
|
2395
|
+
//
|
|
2396
|
+
// THE FRAME BUDGET, NOT A LITERAL 6 (defect 10), BUT NOT ALL OF IT EITHER.
|
|
2397
|
+
// `popupMaxHeight` is the capacity `buildFrameBudget` already computed, and a
|
|
2398
|
+
// second, smaller constant here is what made 57 commands browsable six at a time
|
|
2399
|
+
// on a 30-row terminal. Handing over the WHOLE capacity is wrong in the other
|
|
2400
|
+
// direction: `popupCapacity` reserves exactly one message row, so at 20 rows it
|
|
2401
|
+
// grants 13, the transcript collapses to a single row and the TODO rail
|
|
2402
|
+
// disappears - measured, `todo-app-layout.test.tsx` catches it. Half the granted
|
|
2403
|
+
// capacity keeps the menu growing with the terminal (6 rows at 20, 11 at 30, 16
|
|
2404
|
+
// at 40) while the conversation and the rail keep theirs.
|
|
2405
|
+
const popupMaxRows = Math.floor(frameBudget.popupMaxHeight / POPUP_CAPACITY_SHARE);
|
|
2323
2406
|
// WHICH KEY THE HINT ROW NAMES (shift-tab-mode-toggle-still-dead-on-windows,
|
|
2324
2407
|
// C3-1). Derived from `vtInputWarning` rather than from a probe of its own,
|
|
2325
2408
|
// because that prop is already the single answer to "can this console deliver
|
|
@@ -2348,7 +2431,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2348
2431
|
level: latestToast.level === 'success' ? 'info' : latestToast.level } : undefined,
|
|
2349
2432
|
});
|
|
2350
2433
|
const statusInput = {
|
|
2351
|
-
columns:
|
|
2434
|
+
columns: viewCols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
|
|
2352
2435
|
context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
|
|
2353
2436
|
elapsedMs, tokPerSec, speedKnown, activeTool: state.activeTool, runOutcome: state.runOutcome,
|
|
2354
2437
|
waitingForConfirmation: !!humanRequest || !!confirmState,
|
|
@@ -2364,7 +2447,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2364
2447
|
...feedbackProjection,
|
|
2365
2448
|
};
|
|
2366
2449
|
const details = planStatusDetail({ status: statusInput, model: cfg.model, provider: cfg.provider,
|
|
2367
|
-
hints: { interactionPhase: interactionPhase.current, cols, completion, overlay,
|
|
2450
|
+
hints: { interactionPhase: interactionPhase.current, cols: viewCols, completion, overlay,
|
|
2368
2451
|
selectionPending: selectionController?.hasPendingSelection() ?? false,
|
|
2369
2452
|
copyInFlight: copyState.busy, copyCleanupPending: copyState.cleanupPending,
|
|
2370
2453
|
hasDraft: hasDraftRef.current, services: liveServices,
|
|
@@ -2375,43 +2458,48 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2375
2458
|
const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2376
2459
|
const rail = showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2377
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 })] }));
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
}
|
|
2416
|
-
|
|
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 }));
|
|
2417
2505
|
}
|
|
@@ -12,6 +12,6 @@ export function AutocompletePopup({ items, selected, theme, caps, maxRows, layou
|
|
|
12
12
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: theme.border, paddingX: 1, children: [visible.map((item, i) => {
|
|
13
13
|
const idx = windowStart + i;
|
|
14
14
|
const active = idx === selected;
|
|
15
|
-
return (_jsxs(Text, { inverse: active, wrap: "truncate", children: [_jsx(Text, { color: active ? undefined : theme.primary, children: item.label.replace(/\s+/g, ' ') }), item.hint ? _jsxs(Text, { color: theme.muted, children: [" ", item.hint.replace(/\s+/g, ' ')] }) : null] }, idx));
|
|
15
|
+
return (_jsxs(Text, { inverse: active, wrap: "truncate", children: [_jsx(Text, { color: active ? theme.accent : theme.muted, children: active ? `${glyphs.caret} ` : ' ' }), _jsx(Text, { color: active ? undefined : theme.primary, children: item.label.replace(/\s+/g, ' ') }), item.hint ? _jsxs(Text, { color: theme.muted, children: [" ", item.hint.replace(/\s+/g, ' ')] }) : null] }, idx));
|
|
16
16
|
}), showMore && _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [" +", moreBelow, " more"] })] }));
|
|
17
17
|
}
|
package/dist/ui/Header.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { Logo } from './Logo.js';
|
|
5
|
-
import { planHeaderHints } from './layout/header-hints.js';
|
|
5
|
+
import { planHeaderHints, HEADER_HINT_LIMITS } from './layout/header-hints.js';
|
|
6
6
|
export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, statusExpanded = false, scrolledLines = 0, overlayOpen = false }) {
|
|
7
7
|
// The right-hand hints are planned, not assembled (tui-branch-terminal-shortcut
|
|
8
8
|
// 3.9 / D-11). `plan.text` is exactly `plan.cells` wide and already carries the
|
|
@@ -10,5 +10,10 @@ export function Header({ cwd, provider, model, hasKey, variant, theme, caps, col
|
|
|
10
10
|
// hint by guessing one cell short - and the ladder that decides which hints
|
|
11
11
|
// survive a narrow terminal lives in a pure function with its own tests.
|
|
12
12
|
const plan = planHeaderHints({ columns, statusExpanded, overlayOpen, scrolledLines });
|
|
13
|
-
|
|
13
|
+
// THE TWO HALVES ARE SEPARATED BY CELLS, NOT BY LUCK (defect 12). The left half
|
|
14
|
+
// is `wrap="truncate"`, which draws its ellipsis in the LAST cell it owns; with
|
|
15
|
+
// the right half starting in the very next cell a narrow terminal read
|
|
16
|
+
// `aragon...Details off` as one string. The blank column comes out of the left
|
|
17
|
+
// half's width, so the row still spans exactly `columns`.
|
|
18
|
+
return _jsxs(Box, { flexDirection: "row", height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: Math.max(0, columns - plan.cells - HEADER_HINT_LIMITS.separatorCells), 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: HEADER_HINT_LIMITS.separatorCells, flexShrink: 0 }), _jsx(Box, { width: plan.cells, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: plan.text }) })] });
|
|
14
19
|
}
|
package/dist/ui/Markdown.js
CHANGED
|
@@ -16,22 +16,49 @@ function renderBlock(block, key, theme, glyphs) {
|
|
|
16
16
|
case 'code':
|
|
17
17
|
return (_jsx(CodeBlock, { code: block.code, lang: block.lang, theme: theme, glyphs: glyphs }, `b${key}`));
|
|
18
18
|
case 'table': {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Glyphs are injected rather than hardcoded: this module is inside the
|
|
20
|
+
// ASCII-only scan, and the pure parser defaults to ASCII `|` / `-`.
|
|
21
|
+
const formatted = formatTable(block.rows, block.align, {
|
|
22
|
+
separator: glyphs.railVertical,
|
|
23
|
+
rule: glyphs.hrule,
|
|
24
|
+
});
|
|
25
|
+
return (_jsx(Box, { flexDirection: "column", children: formatted.map((row, r) => (_jsx(Text, { wrap: "truncate", bold: r === 0, color: tableRowColor(r, theme), children: row }, r))) }, `b${key}`));
|
|
21
26
|
}
|
|
22
27
|
default:
|
|
23
28
|
return _jsx(InlineLine, { line: block.text, theme: theme, glyphs: glyphs }, `b${key}`);
|
|
24
29
|
}
|
|
25
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Table row 0 is the header (bold + accent); row 1 is the divider drawn under it.
|
|
33
|
+
*
|
|
34
|
+
* The divider must stay distinguishable at colour level 0, where bold is dropped
|
|
35
|
+
* to nothing, so it is MUTED and never bold: in `--no-color` the `----` run is
|
|
36
|
+
* itself the signal, and in a coloured tier the weight difference separates the
|
|
37
|
+
* header from the rule beneath it.
|
|
38
|
+
*/
|
|
39
|
+
function tableRowColor(index, theme) {
|
|
40
|
+
if (index === 0)
|
|
41
|
+
return theme.accent;
|
|
42
|
+
if (index === 1)
|
|
43
|
+
return theme.muted;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
26
46
|
/**
|
|
27
47
|
* Fenced code. The round border is gone (§4.3): nested inside a tool card it
|
|
28
48
|
* produced a box inside a box and squeezed an 80-column terminal down to 74
|
|
29
49
|
* usable columns. A left rail plus the language tag conveys the same "this is
|
|
30
50
|
* code" boundary for 1 column and 0 rows.
|
|
51
|
+
*
|
|
52
|
+
* The language tag is a muted TITLE, not a body row: it used to carry a chip
|
|
53
|
+
* background and one padding space either side of the text, so it rendered as
|
|
54
|
+
* ` | ts` above code rows that start at `| export ...` — a body line that
|
|
55
|
+
* happened to be the language. It keeps its own row on purpose: the height
|
|
56
|
+
* estimator (`virtual-window.estimateEntryRows`) counts RAW markdown lines and
|
|
57
|
+
* has no code-block branch, so the fence line must still occupy exactly 1 row.
|
|
31
58
|
*/
|
|
32
59
|
function CodeBlock({ code, lang, theme, glyphs, }) {
|
|
33
60
|
const rendered = highlightCached(code, lang);
|
|
34
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [lang.length > 0 && (_jsx(Text, {
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [lang.length > 0 && (_jsx(Text, { bold: true, color: theme.muted, children: lang })), _jsx(Text, { children: rendered })] }));
|
|
35
62
|
}
|
|
36
63
|
/** Six heading levels used to render identically; now they carry three weights. */
|
|
37
64
|
function headingStyle(level, theme) {
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -234,6 +234,31 @@ export function fileTokenAt(buffer, cursor) {
|
|
|
234
234
|
end += 1;
|
|
235
235
|
return { start, end, query: buffer.slice(start + 1, end) };
|
|
236
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* How many `@file` candidates reach the popup.
|
|
239
|
+
*
|
|
240
|
+
* The popup has its own row budget; this is the list it windows. Twenty ranked
|
|
241
|
+
* paths are enough to spot the one you meant, and it bounds the array that every
|
|
242
|
+
* draft edit re-renders.
|
|
243
|
+
*/
|
|
244
|
+
const FILE_MATCH_LIMIT = 20;
|
|
245
|
+
/**
|
|
246
|
+
* Segments in a path - the first ranking key for an EMPTY `@` query (defect 10).
|
|
247
|
+
*
|
|
248
|
+
* A bare `@` matches every file in the tree, and `tinyGlob` returns them in
|
|
249
|
+
* whatever order the filesystem walked, so opening the popup without a rank gave
|
|
250
|
+
* a correct-but-arbitrary twenty. Shallowest first, then shortest, then
|
|
251
|
+
* alphabetical: the paths a user is reaching for (`src/`, a root config) come
|
|
252
|
+
* before `packages/cli/src/ui/layout/deep/thing.ts`.
|
|
253
|
+
*/
|
|
254
|
+
function pathDepth(path) {
|
|
255
|
+
let depth = 1;
|
|
256
|
+
for (const char of path) {
|
|
257
|
+
if (char === '/' || char === '\\')
|
|
258
|
+
depth += 1;
|
|
259
|
+
}
|
|
260
|
+
return depth;
|
|
261
|
+
}
|
|
237
262
|
/** A leading C0 control byte or ESC sequence should never be inserted raw. */
|
|
238
263
|
function isControlSeq(input) {
|
|
239
264
|
if (input.length === 0)
|
|
@@ -253,7 +278,11 @@ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, st
|
|
|
253
278
|
const slashSug = dismissed ? null : slashSuggestions(buffer, commands);
|
|
254
279
|
const fileTok = fileTokenAt(buffer, cursor);
|
|
255
280
|
const fileQuery = fileTok ? fileTok.query : null;
|
|
256
|
-
|
|
281
|
+
// A BARE `@` OPENS THE POPUP (defect 10). The `query.length >= 1` gate here and
|
|
282
|
+
// the `< 1` early return in the scan effect below were the whole reason a lone
|
|
283
|
+
// `@` showed nothing while `README.md` promises "@ opens a file-path completion
|
|
284
|
+
// popup". An empty query is a legitimate one: it lists the shallowest paths.
|
|
285
|
+
const fileActive = !dismissed && !!fileTok && fileMatches.length > 0;
|
|
257
286
|
const popupKind = slashSug && slashSug.length > 0 ? 'slash' : fileActive ? 'file' : null;
|
|
258
287
|
const popupItems = popupKind === 'slash'
|
|
259
288
|
? slashSug
|
|
@@ -263,7 +292,7 @@ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, st
|
|
|
263
292
|
const clampedSel = popupItems.length > 0 ? Math.min(sel, popupItems.length - 1) : 0;
|
|
264
293
|
// --- Debounced `@file` scan. ---------------------------------------------
|
|
265
294
|
useEffect(() => {
|
|
266
|
-
if (fileQuery === null
|
|
295
|
+
if (fileQuery === null) {
|
|
267
296
|
setFileMatches([]);
|
|
268
297
|
return;
|
|
269
298
|
}
|
|
@@ -279,7 +308,16 @@ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, st
|
|
|
279
308
|
if (cancelled)
|
|
280
309
|
return;
|
|
281
310
|
const q = fileQuery.toLowerCase();
|
|
282
|
-
|
|
311
|
+
// RANKED, NOT WALK ORDER (defect 10). An empty query matches every path
|
|
312
|
+
// in the tree, so without a sort the popup for a bare `@` was
|
|
313
|
+
// correct-but-arbitrary - the first twenty the filesystem happened to
|
|
314
|
+
// hand back.
|
|
315
|
+
const ranked = matches
|
|
316
|
+
.filter((m) => m.toLowerCase().includes(q))
|
|
317
|
+
.sort((a, b) => pathDepth(a) - pathDepth(b)
|
|
318
|
+
|| a.length - b.length || (a < b ? -1 : a > b ? 1 : 0))
|
|
319
|
+
.slice(0, FILE_MATCH_LIMIT);
|
|
320
|
+
setFileMatches(ranked);
|
|
283
321
|
})
|
|
284
322
|
.catch(() => {
|
|
285
323
|
if (!cancelled)
|
|
@@ -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
|
+
}
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -65,6 +65,13 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
65
65
|
// place by `turnStart` (5.2.3).
|
|
66
66
|
return frame(glyphs.queued, theme.muted, _jsx(QueuedEntry, { text: entry.text, theme: theme, caps: caps }));
|
|
67
67
|
case 'assistant': {
|
|
68
|
+
// Render-layer backstop for the reducer prune (defect 5) - the reducer is the
|
|
69
|
+
// source of truth, and this only catches a settled, entirely empty bubble
|
|
70
|
+
// that reached here some other way. Returned BEFORE `frame(...)`, so neither
|
|
71
|
+
// the glyph row nor the separation row is drawn.
|
|
72
|
+
if (!entry.streaming && entry.text.trim() === '' && (entry.thinking ?? '').trim() === '') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
68
75
|
const animate = entry.streaming && !reducedMotion && caps.unicode;
|
|
69
76
|
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,
|
|
70
77
|
// `EntryViewImpl` hands each renderer EXPLICIT SCALAR PROPS, so
|