@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +108 -11
  3. package/dist/agent/controller.d.ts +39 -2
  4. package/dist/agent/controller.js +125 -0
  5. package/dist/agent/reducer.d.ts +33 -19
  6. package/dist/agent/reducer.js +62 -71
  7. package/dist/agent/usage.d.ts +34 -0
  8. package/dist/agent/usage.js +43 -0
  9. package/dist/cli.js +48 -1
  10. package/dist/commands/builtins.js +9 -0
  11. package/dist/compaction/compactor.d.ts +60 -24
  12. package/dist/compaction/compactor.js +89 -61
  13. package/dist/compaction/context-command.d.ts +33 -0
  14. package/dist/compaction/context-command.js +166 -0
  15. package/dist/compaction/meter.d.ts +251 -0
  16. package/dist/compaction/meter.js +428 -0
  17. package/dist/compaction/pressure.d.ts +6 -0
  18. package/dist/compaction/pressure.js +4 -0
  19. package/dist/compaction/types.d.ts +41 -0
  20. package/dist/compaction/wiring.d.ts +47 -0
  21. package/dist/compaction/wiring.js +88 -27
  22. package/dist/config/cli-commands.js +5 -0
  23. package/dist/config/env.js +15 -1
  24. package/dist/config/load.d.ts +17 -0
  25. package/dist/config/load.js +45 -1
  26. package/dist/config/schema.d.ts +44 -0
  27. package/dist/config/schema.js +36 -0
  28. package/dist/config/store.js +27 -1
  29. package/dist/exec/events.d.ts +138 -1
  30. package/dist/exec/events.js +42 -0
  31. package/dist/exec/index.js +43 -6
  32. package/dist/exec/progress-reporter.d.ts +23 -0
  33. package/dist/exec/progress-reporter.js +88 -0
  34. package/dist/exec/runner.d.ts +34 -0
  35. package/dist/exec/runner.js +137 -25
  36. package/dist/fast/reviewer.d.ts +19 -1
  37. package/dist/fast/reviewer.js +22 -0
  38. package/dist/fast/types.d.ts +15 -0
  39. package/dist/fast/wiring.d.ts +10 -1
  40. package/dist/fast/wiring.js +18 -1
  41. package/dist/todo/limits.d.ts +4 -17
  42. package/dist/todo/limits.js +4 -17
  43. package/dist/todo/panel-rows.d.ts +8 -6
  44. package/dist/todo/panel-rows.js +19 -6
  45. package/dist/ui/App.js +75 -63
  46. package/dist/ui/AutocompletePopup.d.ts +3 -1
  47. package/dist/ui/AutocompletePopup.js +9 -11
  48. package/dist/ui/BottomStatusRow.d.ts +1 -1
  49. package/dist/ui/Composer.d.ts +3 -1
  50. package/dist/ui/Composer.js +2 -2
  51. package/dist/ui/PromptInput.d.ts +4 -1
  52. package/dist/ui/PromptInput.js +19 -5
  53. package/dist/ui/StatusBar.d.ts +21 -15
  54. package/dist/ui/StatusBar.js +48 -9
  55. package/dist/ui/TeamPanel.d.ts +3 -1
  56. package/dist/ui/TeamPanel.js +13 -9
  57. package/dist/ui/TodoPanel.d.ts +5 -5
  58. package/dist/ui/TodoPanel.js +56 -24
  59. package/dist/ui/UpdateLine.d.ts +1 -1
  60. package/dist/ui/capabilities.js +5 -5
  61. package/dist/ui/exit-snapshot.d.ts +9 -0
  62. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  63. package/dist/ui/layout/autocomplete.d.ts +18 -0
  64. package/dist/ui/layout/autocomplete.js +26 -0
  65. package/dist/ui/layout/rail.d.ts +4 -53
  66. package/dist/ui/layout/rail.js +10 -62
  67. package/dist/ui/layout/team-panel.d.ts +18 -0
  68. package/dist/ui/layout/team-panel.js +22 -0
  69. package/dist/ui/layout/todo-layout.d.ts +24 -0
  70. package/dist/ui/layout/todo-layout.js +27 -0
  71. package/dist/ui/transcript-text.js +6 -2
  72. 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 { todoRailRows, todoRailWidth } from './layout/rail.js';
51
- import { TODO_LIMITS } from '../todo/limits.js';
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
- const modelKnown = !!controller.getModelRegistry().getModel(cfg.provider, cfg.model);
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
- // --- The todo rail (todo-plan-execution §3.9). --------------------------
1882
- //
1883
- // ONE NUMBER FOR THE GATE AND FOR THE LAYOUT (I-10). `railRows` is both what
1884
- // decides whether the panel mounts and what the panel lays out against; a
1885
- // panel that mounts on one number and renders against another shows nothing
1886
- // and reports nothing. `todoRailRows` is NOT `viewportBudget`: the live team
1887
- // roster costs up to 8 rows inside `AppShell`'s measured bottom box that the
1888
- // budget knows nothing about (P1-3).
1889
- const railRows = todoRailRows(viewportBudget, state.team !== null);
1890
- const railWidth = todoRailWidth(cols);
1891
- const showRail = fullscreen && // non-goal 4: inline mode has no fixed frame
1892
- cfg.todo.panel && // `/todo panel off`, persisted
1893
- overlay === null && // a modal owns the screen (D-7)
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, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, contextEstimated: state.contextTokensEstimated, 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
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
- const DEFAULT_MAX_ROWS = 6;
5
- export function AutocompletePopup({ items, selected, theme, caps, maxRows = DEFAULT_MAX_ROWS, }) {
6
- if (items.length === 0)
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
- // Keep the highlighted row within a window of `maxRows`.
10
- const start = Math.max(0, Math.min(selected - maxRows + 1, items.length - maxRows));
11
- const windowStart = Math.max(0, start);
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] }, item.label));
18
- }), moreBelow > 0 && _jsxs(Text, { color: theme.muted, children: [" +", moreBelow, " more"] })] }));
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 `todoRailRows`.
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`.
@@ -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 {};
@@ -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({
@@ -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 {};
@@ -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 (popupItems.length > 0) {
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: [popupItems.length > 0 && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows })), bordered ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: bordered.color, paddingX: 1, children: inputRow })) : (inputRow)] }));
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
  }
@@ -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, tokens, cost, tokens/sec + elapsed while running, off-bottom `↑N`).
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
- contextTokens: number;
20
- contextWindow: number;
21
- contextWindowKnown: boolean;
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 {};
@@ -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, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, servicesActive, retryActive, ecoRung = 0, fastActive, compactionActive, gaugeMarks, contextEstimated = false, } = props;
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
- const pct = contextWindow > 0 ? Math.min(100, Math.round((contextTokens / contextWindow) * 100)) : 0;
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
- // ONE `~` FOR TWO KINDS OF APPROXIMATION (§6.2). A second marker would be a
47
- // second thing to explain for a distinction the user does not act on
48
- // differently: either way the number is a guess.
49
- const pctLabel = `${contextWindowKnown && !contextEstimated ? '' : '~'}${gauge.pct}%`;
50
- const tokens = `${formatTokens(usageTotal.inputTokens)}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
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 >= 72 && _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)] }))] })] }));
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
  }
@@ -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;