@aragon-agent/cli 0.6.4 → 0.6.7
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 +37 -0
- package/README.md +108 -11
- package/dist/agent/controller.d.ts +39 -2
- package/dist/agent/controller.js +125 -0
- package/dist/agent/reducer.d.ts +33 -19
- package/dist/agent/reducer.js +62 -71
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +48 -1
- package/dist/commands/builtins.js +9 -0
- package/dist/compaction/compactor.d.ts +60 -24
- package/dist/compaction/compactor.js +89 -61
- package/dist/compaction/context-command.d.ts +33 -0
- package/dist/compaction/context-command.js +166 -0
- package/dist/compaction/meter.d.ts +251 -0
- package/dist/compaction/meter.js +428 -0
- package/dist/compaction/pressure.d.ts +6 -0
- package/dist/compaction/pressure.js +4 -0
- package/dist/compaction/types.d.ts +41 -0
- package/dist/compaction/wiring.d.ts +47 -0
- package/dist/compaction/wiring.js +88 -27
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.js +15 -1
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +45 -1
- package/dist/config/schema.d.ts +44 -0
- package/dist/config/schema.js +36 -0
- package/dist/config/store.js +27 -1
- package/dist/exec/events.d.ts +138 -1
- package/dist/exec/events.js +42 -0
- package/dist/exec/index.js +43 -6
- package/dist/exec/progress-reporter.d.ts +23 -0
- package/dist/exec/progress-reporter.js +88 -0
- package/dist/exec/runner.d.ts +34 -0
- package/dist/exec/runner.js +137 -25
- package/dist/fast/reviewer.d.ts +19 -1
- package/dist/fast/reviewer.js +22 -0
- package/dist/fast/types.d.ts +15 -0
- package/dist/fast/wiring.d.ts +10 -1
- package/dist/fast/wiring.js +18 -1
- package/dist/todo/limits.d.ts +4 -17
- package/dist/todo/limits.js +4 -17
- package/dist/todo/panel-rows.d.ts +8 -6
- package/dist/todo/panel-rows.js +19 -6
- package/dist/ui/App.js +75 -63
- package/dist/ui/AutocompletePopup.d.ts +3 -1
- package/dist/ui/AutocompletePopup.js +9 -11
- package/dist/ui/BottomStatusRow.d.ts +1 -1
- package/dist/ui/Composer.d.ts +3 -1
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +4 -1
- package/dist/ui/PromptInput.js +19 -5
- package/dist/ui/StatusBar.d.ts +21 -15
- package/dist/ui/StatusBar.js +48 -9
- package/dist/ui/TeamPanel.d.ts +3 -1
- package/dist/ui/TeamPanel.js +13 -9
- package/dist/ui/TodoPanel.d.ts +5 -5
- package/dist/ui/TodoPanel.js +56 -24
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- package/dist/ui/exit-snapshot.d.ts +9 -0
- package/dist/ui/layout/ScrollViewport.d.ts +1 -1
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/team-panel.d.ts +18 -0
- package/dist/ui/layout/team-panel.js +22 -0
- package/dist/ui/layout/todo-layout.d.ts +24 -0
- package/dist/ui/layout/todo-layout.js +27 -0
- package/dist/ui/transcript-text.js +6 -2
- package/package.json +2 -2
package/dist/ui/App.js
CHANGED
|
@@ -11,7 +11,6 @@ import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
|
11
11
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
12
12
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
13
13
|
import { computeCost } from '../agent/usage.js';
|
|
14
|
-
import { isApproximate } from '../compaction/pressure.js';
|
|
15
14
|
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
16
15
|
import { updatePersistedConfig } from '../config/store.js';
|
|
17
16
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
@@ -47,8 +46,8 @@ import { shouldRenderUpdateLine } from '../update/types.js';
|
|
|
47
46
|
import { UPDATE_LIMITS } from '../update/limits.js';
|
|
48
47
|
import { AppShell } from './layout/AppShell.js';
|
|
49
48
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
49
|
+
import { buildTodoRailLayout } from './layout/todo-layout.js';
|
|
50
|
+
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
52
51
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
53
52
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
54
53
|
import { useHeightStore } from './use-height-store.js';
|
|
@@ -751,6 +750,27 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
751
750
|
}
|
|
752
751
|
});
|
|
753
752
|
}, [controller]);
|
|
753
|
+
// --- Context occupancy (context-usage-gauge-accuracy §3.3 / I-1). --------
|
|
754
|
+
//
|
|
755
|
+
// A SEVENTH SUBSCRIPTION, AND THE SOLE DISPATCHER OF `contextUsage`. The gauge
|
|
756
|
+
// used to be written from three reducer branches fed by two upstreams, which
|
|
757
|
+
// is what let a stale `snapshot` bounce the bar back to the pre-compaction
|
|
758
|
+
// figure one statement after a compaction correctly dropped it (P0-1). One
|
|
759
|
+
// writer makes that unrepresentable.
|
|
760
|
+
//
|
|
761
|
+
// IT SUBSCRIBES UNCONDITIONALLY. `subscribeContextUsage` is not a compaction
|
|
762
|
+
// forwarder - the meter exists in every session, including one started with
|
|
763
|
+
// `--no-compaction`, which is precisely the session whose gauge previously had
|
|
764
|
+
// one sample per turn and read 0 % after a `/resume`.
|
|
765
|
+
useEffect(() => {
|
|
766
|
+
// SEED ON MOUNT so a resumed session shows a real occupancy on frame 1
|
|
767
|
+
// rather than after its first completed turn. `getContextUsage()` measures
|
|
768
|
+
// on demand, so this works before any event has ever arrived.
|
|
769
|
+
dispatch({ type: 'contextUsage', snapshot: controller.getContextUsage() });
|
|
770
|
+
return controller.subscribeContextUsage((snapshot) => {
|
|
771
|
+
dispatch({ type: 'contextUsage', snapshot });
|
|
772
|
+
});
|
|
773
|
+
}, [controller]);
|
|
754
774
|
// --- Compaction event stream (context-auto-compaction §5.2). -------------
|
|
755
775
|
//
|
|
756
776
|
// A SIXTH SUBSCRIPTION, for the reason the team, todo and fast ones record:
|
|
@@ -787,23 +807,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
787
807
|
model: event.model,
|
|
788
808
|
});
|
|
789
809
|
break;
|
|
790
|
-
case 'compaction_end':
|
|
810
|
+
case 'compaction_end':
|
|
811
|
+
// THE CARD, AND NOTHING ELSE (context-usage-gauge-accuracy §3.3).
|
|
812
|
+
//
|
|
813
|
+
// A `contextTokensEstimated` dispatch used to live here, hand-building
|
|
814
|
+
// the post-compaction occupancy out of `record.tokensAfter` plus the
|
|
815
|
+
// snapshot's `estimateOffset`. It is gone because the gauge now has ONE
|
|
816
|
+
// writer: `ContextMeter` learns about the splice from
|
|
817
|
+
// `CompactionWiring.settlePending` itself and re-measures, so the bar
|
|
818
|
+
// falls in the same frame WITHOUT this branch and without the `snapshot`
|
|
819
|
+
// branch below racing it. Those two dispatches were the second and
|
|
820
|
+
// third writers of one number, and the loser of that race was the
|
|
821
|
+
// correct value (P0-1).
|
|
791
822
|
dispatch({ type: 'compactionEnd', record: event.record });
|
|
792
|
-
// THE GAUGE MUST FALL IMMEDIATELY (§6.2 / AC-5), and it must fall to a
|
|
793
|
-
// number in the SAME UNIT as the one it replaces (P1-11). The value
|
|
794
|
-
// being overwritten came from `occupiedTokens(usage)` — a provider
|
|
795
|
-
// measurement that includes the tool schemas — while
|
|
796
|
-
// `record.tokensAfter` comes from core's `estimatePromptTokens`, which
|
|
797
|
-
// never counts them. Writing the raw estimate makes the bar fall too
|
|
798
|
-
// far and then visibly climb again on the next `turn_end` for no reason
|
|
799
|
-
// the user can see, which is the same is-this-thing-working doubt the
|
|
800
|
-
// immediate drop exists to prevent, one turn later.
|
|
801
|
-
if (event.record.applied) {
|
|
802
|
-
const offset = controller.getCompactionSnapshot().pressure.estimateOffset ?? 0;
|
|
803
|
-
dispatch({ type: 'contextTokensEstimated', tokens: event.record.tokensAfter + offset });
|
|
804
|
-
}
|
|
805
823
|
break;
|
|
806
|
-
}
|
|
807
824
|
case 'usage': {
|
|
808
825
|
// Compaction spend is REAL SPEND at the SUMMARIZER's price (§6.4 /
|
|
809
826
|
// AC-15), which may be neither the lead's nor the fast tier's — so the
|
|
@@ -820,24 +837,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
820
837
|
break;
|
|
821
838
|
}
|
|
822
839
|
case 'snapshot':
|
|
840
|
+
// THE CHIP, THE COUNTS AND THE SUMMARIZER (§3.3). The occupancy that
|
|
841
|
+
// rides along on `snapshot.pressure` is deliberately NOT dispatched
|
|
842
|
+
// here: the meter that produced it publishes it on its own channel, and
|
|
843
|
+
// a second dispatch of the same number is the second writer this
|
|
844
|
+
// feature removed.
|
|
823
845
|
dispatch({ type: 'compactionSnapshot', snapshot: event.snapshot });
|
|
824
|
-
// ONE NUMBER ON SCREEN AND IN THE TRIGGER (R-11 / hardening §3.2.4).
|
|
825
|
-
// The checkpoint's occupancy is a measured base PLUS an estimate of
|
|
826
|
-
// what was appended after it, and without this the gauge would still
|
|
827
|
-
// show the bare base from the last `turn_end` - so a user watching
|
|
828
|
-
// 76 % would see compaction fire and conclude the feature is broken.
|
|
829
|
-
//
|
|
830
|
-
// `contextTokensEstimated` IS REUSED RATHER THAN A SECOND WRITER ADDED:
|
|
831
|
-
// it is the one existing action for "this figure is derived", and it
|
|
832
|
-
// already sets the `~`. Gated on `isApproximate`, which is precisely
|
|
833
|
-
// the case it was built for - a wholly measured figure belongs to
|
|
834
|
-
// `turnEnd`.
|
|
835
|
-
if (isApproximate(event.snapshot.pressure)) {
|
|
836
|
-
dispatch({
|
|
837
|
-
type: 'contextTokensEstimated',
|
|
838
|
-
tokens: event.snapshot.pressure.occupied,
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
846
|
break;
|
|
842
847
|
}
|
|
843
848
|
});
|
|
@@ -1128,6 +1133,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1128
1133
|
// may call an updater more than once, and scheduling a timer from inside one
|
|
1129
1134
|
// would arm it twice.
|
|
1130
1135
|
const [draftRows, setDraftRows] = useState(1);
|
|
1136
|
+
const [popupRows, setPopupRows] = useState(0);
|
|
1137
|
+
const onPopupRowsChange = useCallback((next) => {
|
|
1138
|
+
setPopupRows(previous => previous === next ? previous : next);
|
|
1139
|
+
}, []);
|
|
1131
1140
|
const draftRowsRef = useRef(1);
|
|
1132
1141
|
const shrinkTimer = useRef(null);
|
|
1133
1142
|
const onDraftRows = useCallback((next) => {
|
|
@@ -1821,8 +1830,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1821
1830
|
}
|
|
1822
1831
|
});
|
|
1823
1832
|
// --- Render ------------------------------------------------------------
|
|
1824
|
-
const modelInfo = controller.getModelInfo()
|
|
1825
|
-
|
|
1833
|
+
// `const modelInfo = controller.getModelInfo()` AND ITS `modelKnown` COMPANION
|
|
1834
|
+
// USED TO LIVE HERE (context-usage-gauge-accuracy §4.3). They fed the status
|
|
1835
|
+
// bar's `contextWindow` / `contextWindowKnown` props, which no longer exist:
|
|
1836
|
+
// the denominator and its trustworthiness are resolved inside `ContextMeter`,
|
|
1837
|
+
// which is what lets the user's `contextWindow` override reach the gauge at
|
|
1838
|
+
// all. Nothing else in this render read them.
|
|
1826
1839
|
const overlay = state.overlay;
|
|
1827
1840
|
const hasKey = controller.hasApiKey();
|
|
1828
1841
|
const empty = state.entries.length === 0;
|
|
@@ -1878,22 +1891,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1878
1891
|
// and every test that never types byte-identical to before.
|
|
1879
1892
|
const viewportBudget = computeViewportRows(rows, draftRows);
|
|
1880
1893
|
const overlayMaxRows = fullscreen ? viewportBudget : Number.POSITIVE_INFINITY;
|
|
1881
|
-
//
|
|
1882
|
-
//
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
state.todos !== null && // no list -> no furniture, zero layout cost
|
|
1895
|
-
railWidth > 0 && // too narrow (under 80 columns)
|
|
1896
|
-
railRows >= TODO_LIMITS.panelMinRows; // too short to say anything useful
|
|
1894
|
+
// Renderers consume these same projections. Team capacity does not depend on
|
|
1895
|
+
// popup occupancy, so a menu cannot toggle the team's collapsed state.
|
|
1896
|
+
const teamLayout = buildTeamPanelLayout({
|
|
1897
|
+
snapshot: state.team, terminalRows: rows, availableRows: viewportBudget,
|
|
1898
|
+
});
|
|
1899
|
+
const railLayout = buildTodoRailLayout({
|
|
1900
|
+
mode, cols, panelEnabled: cfg.todo.panel, overlayOpen: overlay !== null,
|
|
1901
|
+
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
1902
|
+
teamRows: teamLayout.rowCount, popupRows,
|
|
1903
|
+
});
|
|
1904
|
+
// Every viewport consumer uses contentCols; header, composer, status and
|
|
1905
|
+
// overlays keep the full terminal width. Overlay visibility is in the gate.
|
|
1906
|
+
const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
1897
1907
|
// The inline plan strip (todo-plan-followthrough §3.7 / W2).
|
|
1898
1908
|
//
|
|
1899
1909
|
// DELIBERATELY NOT `!showRail`, which is true for FOUR different reasons and
|
|
@@ -1906,12 +1916,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1906
1916
|
// needless — this runs in RENDER scope, where `cfg` is re-read every frame.
|
|
1907
1917
|
// The asymmetry with the `agent_end` handler above is the whole of P1-1.
|
|
1908
1918
|
const showStrip = !fullscreen && cfg.todo.panel && state.todos !== null;
|
|
1909
|
-
// Every consumer INSIDE the viewport slot switches to this; the header,
|
|
1910
|
-
// composer, status bar, toasts and every overlay keep the full frame width
|
|
1911
|
-
// (C-7). Overlays keep it because `showRail` already includes `overlay ===
|
|
1912
|
-
// null`: splitting the screen with a status panel behind a modal is both worse
|
|
1913
|
-
// design and a second source of width-contract bugs in six components.
|
|
1914
|
-
const contentCols = cols - (showRail ? railWidth : 0);
|
|
1915
1919
|
const overlayNode = overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: cols, rows: helpRows(theme, caps), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { initial: {
|
|
1916
1920
|
provider: cfg.provider,
|
|
1917
1921
|
model: cfg.model,
|
|
@@ -2031,8 +2035,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2031
2035
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
2032
2036
|
// only decides which one the user is taught.
|
|
2033
2037
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
2034
|
-
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, scrolledLines: scrolledLines, onDraftRows: onDraftRows, onNotice: notify, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
2035
|
-
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(BottomStatusRow, { mode: mode, toasts: state.toasts,
|
|
2038
|
+
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: railLayout.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, scrolledLines: scrolledLines, onDraftRows: onDraftRows, onNotice: notify, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
2039
|
+
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: fullscreen ? teamLayout : undefined, rows: rows, cols: cols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(BottomStatusRow, { mode: mode, toasts: state.toasts,
|
|
2036
2040
|
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
2037
2041
|
// the full-screen branch replaces the whole viewport with the overlay,
|
|
2038
2042
|
// and a working line under a settings screen is noise attached to a
|
|
@@ -2070,7 +2074,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2070
2074
|
// branch replaces the whole viewport with the overlay, and an update
|
|
2071
2075
|
// notice under a settings screen is noise attached to a surface it is
|
|
2072
2076
|
// not visible on.
|
|
2073
|
-
update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2077
|
+
update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2078
|
+
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2079
|
+
// projection of "many sources of truth" into the props layer, and a
|
|
2080
|
+
// caller could update three of them; a single reading cannot be
|
|
2081
|
+
// partially stale. The window and its `known` flag now travel INSIDE
|
|
2082
|
+
// it, resolved by the meter - which is what makes the user's
|
|
2083
|
+
// `contextWindow` override reach the bar.
|
|
2084
|
+
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
|
|
2074
2085
|
? {
|
|
2075
2086
|
teamActive: {
|
|
2076
2087
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -2081,8 +2092,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2081
2092
|
: {}), ...(state.todos && !showRail
|
|
2082
2093
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
2083
2094
|
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.compaction?.live
|
|
2095
|
+
? { compactionActive: { inFlight: state.compaction.inFlight } }
|
|
2096
|
+
: {}), ...(controller.isCompactionEnabled()
|
|
2084
2097
|
? {
|
|
2085
|
-
compactionActive: { inFlight: state.compaction.inFlight },
|
|
2086
2098
|
gaugeMarks: {
|
|
2087
2099
|
warn: cfg.compaction.warnThreshold * 100,
|
|
2088
2100
|
high: cfg.compaction.threshold * 100,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import type { Theme } from './theme.js';
|
|
8
8
|
import type { TermCapabilities } from './capabilities.js';
|
|
9
|
+
import { type AutocompleteLayout } from './layout/autocomplete.js';
|
|
9
10
|
export interface Suggestion {
|
|
10
11
|
label: string;
|
|
11
12
|
hint?: string;
|
|
@@ -16,6 +17,7 @@ interface AutocompletePopupProps {
|
|
|
16
17
|
theme: Theme;
|
|
17
18
|
caps: TermCapabilities;
|
|
18
19
|
maxRows?: number;
|
|
20
|
+
layout?: AutocompleteLayout;
|
|
19
21
|
}
|
|
20
|
-
export declare function AutocompletePopup({ items, selected, theme, caps, maxRows, }: AutocompletePopupProps): React.ReactElement | null;
|
|
22
|
+
export declare function AutocompletePopup({ items, selected, theme, caps, maxRows, layout, }: AutocompletePopupProps): React.ReactElement | null;
|
|
21
23
|
export {};
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { pickGlyphs } from './glyphs.js';
|
|
4
|
-
|
|
5
|
-
export function AutocompletePopup({ items, selected, theme, caps, maxRows
|
|
6
|
-
|
|
4
|
+
import { buildAutocompleteLayout } from './layout/autocomplete.js';
|
|
5
|
+
export function AutocompletePopup({ items, selected, theme, caps, maxRows, layout, }) {
|
|
6
|
+
const projection = layout ?? buildAutocompleteLayout({ itemCount: items.length, selected, maxRows });
|
|
7
|
+
if (projection.rowCount === 0)
|
|
7
8
|
return null;
|
|
8
9
|
const glyphs = pickGlyphs(caps);
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const visible = items.slice(windowStart, windowStart + maxRows);
|
|
13
|
-
const moreBelow = items.length - (windowStart + visible.length);
|
|
14
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: glyphs.boxStyle, borderColor: theme.border, paddingX: 1, children: [visible.map((item, i) => {
|
|
10
|
+
const { start: windowStart, count, moreBelow, showMore } = projection;
|
|
11
|
+
const visible = items.slice(windowStart, windowStart + count);
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: theme.border, paddingX: 1, children: [visible.map((item, i) => {
|
|
15
13
|
const idx = windowStart + i;
|
|
16
14
|
const active = idx === selected;
|
|
17
|
-
return (_jsxs(Text, { inverse: active, children: [_jsx(Text, { color: active ? undefined : theme.primary, children: item.label }), item.hint ? _jsxs(Text, { color: theme.muted, children: [" ", item.hint] }) : null] },
|
|
18
|
-
}),
|
|
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));
|
|
16
|
+
}), showMore && _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [" +", moreBelow, " more"] })] }));
|
|
19
17
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* box is its only `flexShrink={1}` child — so an extra chrome row does not make
|
|
12
12
|
* the frame taller, Yoga takes the row out of the TRANSCRIPT. Meanwhile
|
|
13
13
|
* `viewportRows()` keeps returning the old number to `ScrollViewport`,
|
|
14
|
-
* `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `
|
|
14
|
+
* `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `buildTodoRailLayout`.
|
|
15
15
|
* The transcript would draw one row shorter than every consumer believes, ONLY
|
|
16
16
|
* WHILE A RUN IS IN FLIGHT, so the layout would shift on submit and shift back on
|
|
17
17
|
* `agent_end`.
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface ComposerProps {
|
|
|
49
49
|
*/
|
|
50
50
|
modeToggleKey?: string;
|
|
51
51
|
popupMaxRows?: number;
|
|
52
|
+
popupMaxHeight?: number;
|
|
53
|
+
onPopupRowsChange?: (rows: number) => void;
|
|
52
54
|
/**
|
|
53
55
|
* Rows of newer output below the viewport, forwarded to the input row's chip
|
|
54
56
|
* (tui-selection-and-scroll-follow §4.2 / G7).
|
|
@@ -117,5 +119,5 @@ declare function hintText(opts: {
|
|
|
117
119
|
* than fishing the string out of a rendered frame.
|
|
118
120
|
*/
|
|
119
121
|
export declare const hintTextForTest: typeof hintText;
|
|
120
|
-
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
122
|
+
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
121
123
|
export {};
|
package/dist/ui/Composer.js
CHANGED
|
@@ -90,7 +90,7 @@ function hintText(opts) {
|
|
|
90
90
|
* than fishing the string out of a rendered frame.
|
|
91
91
|
*/
|
|
92
92
|
export const hintTextForTest = hintText;
|
|
93
|
-
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services = 0, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
|
|
93
|
+
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services = 0, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
|
|
94
94
|
const [hasDraft, setHasDraft] = useState(false);
|
|
95
95
|
const onDraftChange = useCallback((next) => {
|
|
96
96
|
setHasDraft(next.hasDraft);
|
|
@@ -110,7 +110,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
|
|
|
110
110
|
: theme.idleBorder ?? theme.border;
|
|
111
111
|
const glyphs = pickGlyphs(caps);
|
|
112
112
|
const visible = showHint && hintsEnabled;
|
|
113
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
|
|
114
114
|
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
115
115
|
// costs zero rows on a 24-row terminal.
|
|
116
116
|
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText({
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -48,6 +48,9 @@ interface PromptInputProps {
|
|
|
48
48
|
* `App` derives this from `budget.ts` so the popup shrinks instead.
|
|
49
49
|
*/
|
|
50
50
|
popupMaxRows?: number;
|
|
51
|
+
/** Total menu rows, including borders and the overflow hint. */
|
|
52
|
+
popupMaxHeight?: number;
|
|
53
|
+
onPopupRowsChange?: (rows: number) => void;
|
|
51
54
|
onSubmit: (text: string) => void;
|
|
52
55
|
onHelp?: () => void;
|
|
53
56
|
/**
|
|
@@ -173,5 +176,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
173
176
|
end: number;
|
|
174
177
|
query: string;
|
|
175
178
|
} | null;
|
|
176
|
-
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
|
|
179
|
+
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
|
|
177
180
|
export {};
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -19,12 +19,13 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
19
19
|
* stays a plain `useState` because it is written from the debounced glob effect
|
|
20
20
|
* rather than from a key.
|
|
21
21
|
*/
|
|
22
|
-
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
22
|
+
import { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
|
|
23
23
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
24
24
|
import { glob as tinyGlob } from 'tinyglobby';
|
|
25
25
|
import { pickGlyphs } from './glyphs.js';
|
|
26
26
|
import { recognize } from '../input/keymap.js';
|
|
27
27
|
import { AutocompletePopup } from './AutocompletePopup.js';
|
|
28
|
+
import { buildAutocompleteLayout } from './layout/autocomplete.js';
|
|
28
29
|
import { editorReducer, INITIAL_EDITOR_STATE, } from './editor-reducer.js';
|
|
29
30
|
import { hasPasteFrame, splitPasteFrames } from './paste-frames.js';
|
|
30
31
|
import { allocatePasteId, expandPastes, expandRangeOverTokens } from './paste-tokens.js';
|
|
@@ -246,7 +247,7 @@ function isControlSeq(input) {
|
|
|
246
247
|
// ---------------------------------------------------------------------------
|
|
247
248
|
// Component
|
|
248
249
|
// ---------------------------------------------------------------------------
|
|
249
|
-
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines = 0, }) {
|
|
250
|
+
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines = 0, }) {
|
|
250
251
|
const { stdout } = useStdout();
|
|
251
252
|
const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
|
|
252
253
|
const { buffer, cursor, historyIndex, dismissed, sel } = editor;
|
|
@@ -263,6 +264,19 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
263
264
|
? fileMatches.map((p) => ({ label: p }))
|
|
264
265
|
: [];
|
|
265
266
|
const clampedSel = popupItems.length > 0 ? Math.min(sel, popupItems.length - 1) : 0;
|
|
267
|
+
const popupLayout = buildAutocompleteLayout({
|
|
268
|
+
itemCount: isActive ? popupItems.length : 0,
|
|
269
|
+
selected: clampedSel,
|
|
270
|
+
maxRows: popupMaxRows,
|
|
271
|
+
maxHeight: popupMaxHeight,
|
|
272
|
+
});
|
|
273
|
+
const popupVisible = isActive && popupLayout.rowCount > 0;
|
|
274
|
+
// Report on every mount, including zero. Cleanup belongs to a separate
|
|
275
|
+
// effect so changing height does not publish an intermediate zero budget.
|
|
276
|
+
useLayoutEffect(() => {
|
|
277
|
+
onPopupRowsChange?.(popupLayout.rowCount);
|
|
278
|
+
}, [onPopupRowsChange, popupLayout.rowCount]);
|
|
279
|
+
useLayoutEffect(() => () => onPopupRowsChange?.(0), [onPopupRowsChange]);
|
|
266
280
|
// --- Debounced `@file` scan. ---------------------------------------------
|
|
267
281
|
useEffect(() => {
|
|
268
282
|
if (fileQuery === null || fileQuery.length < 1) {
|
|
@@ -428,7 +442,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
428
442
|
if (key.tab && key.shift)
|
|
429
443
|
return;
|
|
430
444
|
// Popup navigation owns Up/Down/Tab/→/Enter/Esc while it is open.
|
|
431
|
-
if (
|
|
445
|
+
if (popupVisible) {
|
|
432
446
|
if (key.upArrow) {
|
|
433
447
|
dispatch({ type: 'select', sel: Math.max(0, clampedSel - 1) });
|
|
434
448
|
return;
|
|
@@ -550,7 +564,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
550
564
|
const split = splitRowAtColumn(row, layout.cursorCol);
|
|
551
565
|
return (_jsxs(_Fragment, { children: [renderSegments(split.before, `b${i}.`), _jsx(Text, { inverse: true, children: split.at ? split.at.text : ' ' }), renderSegments(split.after, `a${i}.`)] }));
|
|
552
566
|
});
|
|
553
|
-
const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (_jsx(Text, { color: theme.muted, children: placeholder })) : (rowNodes.map((node, i) => overflow && i === rowNodes.length - 1 ? (
|
|
567
|
+
const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (_jsx(Text, { wrap: "truncate", color: theme.muted, children: placeholder })) : (rowNodes.map((node, i) => overflow && i === rowNodes.length - 1 ? (
|
|
554
568
|
// The overflow indicator rides the LAST rendered row rather than
|
|
555
569
|
// taking one of its own: an extra row would put the composer's
|
|
556
570
|
// real height one above the `draftRows` it just reported (I-8).
|
|
@@ -565,5 +579,5 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
565
579
|
// the user is typing — `↓ 9` becomes `↓ 10` becomes `↓ 100` — and every
|
|
566
580
|
// change would otherwise re-wrap a draft that fills the line.
|
|
567
581
|
_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 }) }))] }));
|
|
568
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [
|
|
582
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupVisible && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows, layout: popupLayout })), bordered ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: bordered.color, paddingX: 1, children: inputRow })) : (inputRow)] }));
|
|
569
583
|
}
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Status bar (spec §4.8). Width-aware, borderless, EXACTLY one row: a left
|
|
3
3
|
* cluster (run status, model, thinking level) and a right cluster (context
|
|
4
|
-
* gauge,
|
|
5
|
-
* Clusters drop out gracefully on narrow terminals.
|
|
4
|
+
* gauge, occupancy pair, session total, cost, tokens/sec + elapsed while
|
|
5
|
+
* running, off-bottom `↑N`). Clusters drop out gracefully on narrow terminals.
|
|
6
|
+
*
|
|
7
|
+
* THE RIGHT CLUSTER'S WIDTH LADDER (context-usage-gauge-accuracy §3.6):
|
|
8
|
+
*
|
|
9
|
+
* >= 96 [####....] 43% 86.0k/200.0k total 1.2M^ 48.0k v $3.21 12 tok/s 1m02s
|
|
10
|
+
* 72-95 [####....] 43% 86.0k/200.0k $3.21
|
|
11
|
+
* 60-71 [####....] 43% $3.21
|
|
12
|
+
* < 60 43% $3.21
|
|
6
13
|
*
|
|
7
14
|
* Dropping the round border here buys back 3 rows of a 24-row terminal — an
|
|
8
15
|
* eighth of the screen was being spent framing a single line of text.
|
|
@@ -11,14 +18,23 @@ import React from 'react';
|
|
|
11
18
|
import type { Theme } from './theme.js';
|
|
12
19
|
import type { TermCapabilities } from './capabilities.js';
|
|
13
20
|
import type { UsageTotal } from '../agent/reducer.js';
|
|
21
|
+
import type { ContextUsageSnapshot } from '../compaction/types.js';
|
|
14
22
|
import { type AgentMode } from '../agent/agent-mode.js';
|
|
15
23
|
interface StatusBarProps {
|
|
16
24
|
model: string;
|
|
17
25
|
provider: string;
|
|
18
26
|
usageTotal: UsageTotal;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Context occupancy, as one object (context-usage-gauge-accuracy §4.3).
|
|
29
|
+
*
|
|
30
|
+
* IT REPLACED FOUR PARALLEL PROPS - `contextTokens`, `contextWindow`,
|
|
31
|
+
* `contextWindowKnown`, `contextEstimated`. Four props describing one reading
|
|
32
|
+
* are four chances for a caller to update three of them, which is the props-
|
|
33
|
+
* layer form of the "many writers" defect this feature removed from the
|
|
34
|
+
* reducer. `usageTotal` stays separate because it genuinely IS another
|
|
35
|
+
* quantity: session spend, not occupancy.
|
|
36
|
+
*/
|
|
37
|
+
context: ContextUsageSnapshot;
|
|
22
38
|
status: 'idle' | 'running';
|
|
23
39
|
elapsedMs: number;
|
|
24
40
|
thinkingLevel: string;
|
|
@@ -187,16 +203,6 @@ interface StatusBarProps {
|
|
|
187
203
|
warn: number;
|
|
188
204
|
high: number;
|
|
189
205
|
};
|
|
190
|
-
/**
|
|
191
|
-
* Whether `contextTokens` is a DERIVED figure rather than a measured one.
|
|
192
|
-
*
|
|
193
|
-
* It WIDENS THE EXISTING `~`, it does not add a second marker. Today that
|
|
194
|
-
* prefix means "the window is a guess" (`contextWindowKnown`); with compaction
|
|
195
|
-
* it also has to mean "the occupancy is a guess", because the bar is written
|
|
196
|
-
* from an estimate for one turn after every compaction so it can fall
|
|
197
|
-
* immediately (§6.2 / AC-5).
|
|
198
|
-
*/
|
|
199
|
-
contextEstimated?: boolean;
|
|
200
206
|
}
|
|
201
207
|
export declare function StatusBar(props: StatusBarProps): React.ReactElement;
|
|
202
208
|
export {};
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useStdout } from 'ink';
|
|
3
3
|
import { pickGlyphs } from './glyphs.js';
|
|
4
|
-
import { formatCost, formatTokens, formatDuration } from '../agent/usage.js';
|
|
4
|
+
import { formatCost, formatTokens, formatDuration, promptTokensOf } from '../agent/usage.js';
|
|
5
5
|
import { buildGauge } from './gauge.js';
|
|
6
6
|
import { MODE_LABEL } from '../agent/agent-mode.js';
|
|
7
7
|
import { formatRetryChip } from '../agent/retry-view.js';
|
|
@@ -12,6 +12,26 @@ import { TODO_LIMITS } from '../todo/limits.js';
|
|
|
12
12
|
import { PROC_LIMITS } from '../proc/limits.js';
|
|
13
13
|
/** Below this the `eco` chip costs the context gauge more than it is worth. */
|
|
14
14
|
const ECO_MIN_COLS = 72;
|
|
15
|
+
/**
|
|
16
|
+
* Where `86k/200k` starts being shown (context-usage-gauge-accuracy §3.6 / W5).
|
|
17
|
+
*
|
|
18
|
+
* IT TAKES THE COLUMNS THE SESSION TOTAL USED TO HAVE, and the ordering is the
|
|
19
|
+
* point rather than a layout accident (D-6). "How full am I, out of how much" is
|
|
20
|
+
* the question a user asks the gauge; "what has this session cost in tokens" is
|
|
21
|
+
* a different one, and putting the second in front of the first spends the
|
|
22
|
+
* scarce columns on the number the user is more likely to MISREAD - the total
|
|
23
|
+
* looks like an occupancy until you notice it exceeds the window.
|
|
24
|
+
*/
|
|
25
|
+
const ABSOLUTE_PAIR_MIN_COLS = 72;
|
|
26
|
+
/**
|
|
27
|
+
* Where the session total returns, now prefixed.
|
|
28
|
+
*
|
|
29
|
+
* `total ` IS AN ASCII WORD, NOT A SIGMA. `ui/**` is inside the glyph scanner's
|
|
30
|
+
* scope, so a new symbol would need an entry in `glyphs.ts` with an ASCII
|
|
31
|
+
* fallback for a legacy `cmd.exe` - a real cost for a three-character prefix
|
|
32
|
+
* that reads worse.
|
|
33
|
+
*/
|
|
34
|
+
const SESSION_TOTAL_MIN_COLS = 96;
|
|
15
35
|
/**
|
|
16
36
|
* I-5 — DO NOT "CLEAN THIS UP". Ink short-circuits identical output at TWO
|
|
17
37
|
* gates (`ink.js:132` and `log-update.js:13`), so a fixed frame whose React
|
|
@@ -31,23 +51,42 @@ function redrawChar(nonce) {
|
|
|
31
51
|
return nonce % 2 === 0 ? ' ' : NBSP;
|
|
32
52
|
}
|
|
33
53
|
export function StatusBar(props) {
|
|
34
|
-
const { model, provider, usageTotal,
|
|
54
|
+
const { model, provider, usageTotal, context, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, servicesActive, retryActive, ecoRung = 0, fastActive, compactionActive, gaugeMarks, } = props;
|
|
35
55
|
const { stdout } = useStdout();
|
|
36
56
|
const cols = stdout?.columns ?? 80;
|
|
37
57
|
const running = status === 'running';
|
|
38
58
|
const glyphs = pickGlyphs(caps);
|
|
39
|
-
|
|
59
|
+
// THE PERCENTAGE IS NOT RECOMPUTED HERE ANY MORE. It arrives already divided
|
|
60
|
+
// and clamped by the same code the compaction trigger reads, which is the
|
|
61
|
+
// whole of AC-2: a user watching 78 % and a `/compact status` reporting 91 %
|
|
62
|
+
// would rightly conclude one of them is lying.
|
|
63
|
+
const pct = context.pct;
|
|
40
64
|
// `gaugeMarks` is spread rather than passed as `undefined`, so a session with
|
|
41
65
|
// compaction off takes `buildGauge`'s own default and the bar is byte-identical
|
|
42
66
|
// to the pre-feature build.
|
|
43
67
|
const gauge = gaugeMarks
|
|
44
68
|
? buildGauge(pct, cols < 72 ? 8 : 12, theme, caps, gaugeMarks)
|
|
45
69
|
: buildGauge(pct, cols < 72 ? 8 : 12, theme, caps);
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
70
|
+
// `~` BELONGS TO THE NUMERATOR, `?` TO THE DENOMINATOR (P2-7). One glyph used
|
|
71
|
+
// to carry three meanings - the window is a guess, the occupancy is a guess,
|
|
72
|
+
// the occupancy contains an appended-message estimate - and a user could not
|
|
73
|
+
// tell from it which half to distrust. The `~` keeps every case it had: an
|
|
74
|
+
// unknown window still produces it, so a session that never touches the new
|
|
75
|
+
// absolute pair sees the identical string it saw before.
|
|
76
|
+
const occupancyApproximate = context.source === 'estimate' || context.deltaTokens > 0;
|
|
77
|
+
const pctLabel = `${context.windowKnown && !occupancyApproximate ? '' : '~'}${gauge.pct}%`;
|
|
78
|
+
// `?` ON THE WINDOW, AND ONLY ON THE NEW READOUT. `windowKnown` is false
|
|
79
|
+
// exactly when `buildRuntimeModel` invented the 128k placeholder, and saying
|
|
80
|
+
// `128k?` is the difference between a denominator the user can act on and one
|
|
81
|
+
// they will assume came from their model card.
|
|
82
|
+
const windowLabel = `${formatTokens(context.window)}${context.windowKnown ? '' : '?'}`;
|
|
83
|
+
const absolutePair = `${formatTokens(context.occupied)}/${windowLabel}`;
|
|
84
|
+
// THE PROMPT-SIDE TOTAL, NOT `inputTokens` (P1-3 / AC-11). Anthropic reports
|
|
85
|
+
// `input_tokens` EXCLUDING cached tokens, so the old expression under-reported
|
|
86
|
+
// for anyone behind a caching gateway - on the same row as a `$` that has
|
|
87
|
+
// always priced the cache. Any session with cache hits sees this number grow,
|
|
88
|
+
// and that is the fix rather than a regression.
|
|
89
|
+
const tokens = `total ${formatTokens(promptTokensOf(usageTotal))}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
|
|
51
90
|
const statusGlyph = running ? theme.symbols.toolRunning : theme.symbols.toolPending;
|
|
52
91
|
// Nothing at all in build mode with nothing pending: the default posture is
|
|
53
92
|
// not news, and a permanent word here would be furniture.
|
|
@@ -69,5 +108,5 @@ export function StatusBar(props) {
|
|
|
69
108
|
? `todo ${todoActive.done}/${todoActive.total}`
|
|
70
109
|
: `[${todoActive.done}/${todoActive.total}]`] })), servicesActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= PROC_LIMITS.statusCompactCols
|
|
71
110
|
? `svc ${servicesActive.live}`
|
|
72
|
-
: `[${servicesActive.live}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [compactionActive && cols >= COMPACTION_LIMITS.statusCompactCols && (_jsxs(Text, { color: compactionActive.inFlight ? theme.accent : theme.muted, children: [compactionActive.inFlight ? 'compacting' : 'compact', ' '] })), fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >=
|
|
111
|
+
: `[${servicesActive.live}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [compactionActive && cols >= COMPACTION_LIMITS.statusCompactCols && (_jsxs(Text, { color: compactionActive.inFlight ? theme.accent : theme.muted, children: [compactionActive.inFlight ? 'compacting' : 'compact', ' '] })), fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= ABSOLUTE_PAIR_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: [" ", absolutePair] })), cols >= SESSION_TOTAL_MIN_COLS && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
|
|
73
112
|
}
|
package/dist/ui/TeamPanel.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import React from 'react';
|
|
20
20
|
import type { Theme } from './theme.js';
|
|
21
21
|
import type { TermCapabilities } from './capabilities.js';
|
|
22
|
+
import { type TeamPanelLayout } from './layout/team-panel.js';
|
|
22
23
|
import type { SubagentRun, TeamSnapshot } from '../team/types.js';
|
|
23
24
|
export interface TeamPanelProps {
|
|
24
25
|
snapshot: TeamSnapshot;
|
|
@@ -35,6 +36,7 @@ export interface TeamPanelProps {
|
|
|
35
36
|
caps: TermCapabilities;
|
|
36
37
|
/** Injectable clock so the panel test does not depend on wall time. */
|
|
37
38
|
now?: number;
|
|
39
|
+
layout?: TeamPanelLayout;
|
|
38
40
|
}
|
|
39
41
|
export declare function activityBudget(cols: number): number;
|
|
40
42
|
/**
|
|
@@ -55,4 +57,4 @@ export declare function activityBudget(cols: number): number;
|
|
|
55
57
|
* columns and break at 80.
|
|
56
58
|
*/
|
|
57
59
|
export declare function activityLine(run: SubagentRun, cols: number): string;
|
|
58
|
-
export declare function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, }: TeamPanelProps): React.ReactElement;
|
|
60
|
+
export declare function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }: TeamPanelProps): React.ReactElement | null;
|