@aragon-agent/cli 0.6.29 → 0.6.32

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 (123) hide show
  1. package/CHANGELOG.md +2223 -2103
  2. package/README.md +2957 -2759
  3. package/dist/agent/controller.d.ts +46 -0
  4. package/dist/agent/controller.js +175 -7
  5. package/dist/agent/job-notices.d.ts +143 -0
  6. package/dist/agent/job-notices.js +182 -0
  7. package/dist/agent/project-guidance-prompt.js +20 -20
  8. package/dist/agent/reducer.d.ts +21 -1
  9. package/dist/agent/reducer.js +7 -0
  10. package/dist/agent/system-prompt.d.ts +12 -0
  11. package/dist/agent/system-prompt.js +2 -0
  12. package/dist/cli.js +399 -62
  13. package/dist/commands/builtins.js +27 -8
  14. package/dist/commands/perf.d.ts +79 -2
  15. package/dist/commands/perf.js +51 -3
  16. package/dist/commands/registry.d.ts +6 -0
  17. package/dist/config/cli-commands.d.ts +27 -0
  18. package/dist/config/cli-commands.js +54 -1
  19. package/dist/config/env.js +42 -0
  20. package/dist/config/load.d.ts +3 -1
  21. package/dist/config/load.js +61 -1
  22. package/dist/config/schema.d.ts +100 -0
  23. package/dist/config/schema.js +43 -0
  24. package/dist/config/store.js +14 -1
  25. package/dist/config/ui-state.d.ts +9 -0
  26. package/dist/config/ui-state.js +9 -0
  27. package/dist/exec/events.d.ts +29 -3
  28. package/dist/exec/events.js +3 -12
  29. package/dist/exec/runner.d.ts +24 -0
  30. package/dist/exec/runner.js +79 -0
  31. package/dist/input/cursor-report.d.ts +61 -0
  32. package/dist/input/cursor-report.js +97 -0
  33. package/dist/input/stdin-filter.d.ts +22 -0
  34. package/dist/input/stdin-filter.js +48 -3
  35. package/dist/proc/limits.d.ts +76 -2
  36. package/dist/proc/limits.js +83 -2
  37. package/dist/proc/log-mirror.d.ts +59 -0
  38. package/dist/proc/log-mirror.js +203 -0
  39. package/dist/proc/log-ring.d.ts +22 -0
  40. package/dist/proc/log-ring.js +20 -1
  41. package/dist/proc/notices.d.ts +57 -0
  42. package/dist/proc/notices.js +101 -0
  43. package/dist/proc/progress.d.ts +40 -0
  44. package/dist/proc/progress.js +135 -0
  45. package/dist/proc/prompt.d.ts +24 -2
  46. package/dist/proc/prompt.js +51 -2
  47. package/dist/proc/supervisor.d.ts +35 -2
  48. package/dist/proc/supervisor.js +142 -43
  49. package/dist/proc/types.d.ts +89 -1
  50. package/dist/proc/wait.d.ts +57 -0
  51. package/dist/proc/wait.js +133 -0
  52. package/dist/session/persist.js +3 -1
  53. package/dist/session/validate-session.js +17 -0
  54. package/dist/tools/bash-tool.d.ts +12 -1
  55. package/dist/tools/bash-tool.js +100 -13
  56. package/dist/tools/index.d.ts +26 -2
  57. package/dist/tools/index.js +43 -2
  58. package/dist/tools/job-tools.d.ts +54 -0
  59. package/dist/tools/job-tools.js +329 -0
  60. package/dist/tools/proc-tools.d.ts +1 -1
  61. package/dist/tools/proc-tools.js +49 -17
  62. package/dist/ui/ActivityLine.js +7 -0
  63. package/dist/ui/App.d.ts +11 -1
  64. package/dist/ui/App.js +181 -63
  65. package/dist/ui/ScrollbackShell.d.ts +54 -0
  66. package/dist/ui/ScrollbackShell.js +15 -0
  67. package/dist/ui/StatusBar.d.ts +5 -0
  68. package/dist/ui/StatusBar.js +1 -0
  69. package/dist/ui/Transcript.d.ts +14 -0
  70. package/dist/ui/Transcript.js +14 -2
  71. package/dist/ui/entries/ServiceCard.d.ts +17 -1
  72. package/dist/ui/entries/ServiceCard.js +39 -4
  73. package/dist/ui/frame-differ.d.ts +12 -0
  74. package/dist/ui/layout/status-layout.d.ts +13 -1
  75. package/dist/ui/layout/status-layout.js +10 -0
  76. package/dist/ui/layout/virtual-window.js +14 -1
  77. package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
  78. package/dist/ui/overlays/HelpOverlay.js +21 -3
  79. package/dist/ui/run-status-row.d.ts +12 -0
  80. package/dist/ui/screen.d.ts +29 -0
  81. package/dist/ui/screen.js +40 -0
  82. package/dist/ui/scrollback/LiveTail.d.ts +64 -0
  83. package/dist/ui/scrollback/LiveTail.js +27 -0
  84. package/dist/ui/scrollback/band.d.ts +105 -0
  85. package/dist/ui/scrollback/band.js +109 -0
  86. package/dist/ui/scrollback/budget.d.ts +72 -0
  87. package/dist/ui/scrollback/budget.js +81 -0
  88. package/dist/ui/scrollback/canvas.d.ts +99 -0
  89. package/dist/ui/scrollback/canvas.js +113 -0
  90. package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
  91. package/dist/ui/scrollback/cursor-probe.js +133 -0
  92. package/dist/ui/scrollback/drain.d.ts +32 -0
  93. package/dist/ui/scrollback/drain.js +64 -0
  94. package/dist/ui/scrollback/exit-summary.d.ts +23 -0
  95. package/dist/ui/scrollback/exit-summary.js +25 -0
  96. package/dist/ui/scrollback/limits.d.ts +74 -0
  97. package/dist/ui/scrollback/limits.js +74 -0
  98. package/dist/ui/scrollback/live-preview.d.ts +26 -0
  99. package/dist/ui/scrollback/live-preview.js +106 -0
  100. package/dist/ui/scrollback/renderer.d.ts +38 -0
  101. package/dist/ui/scrollback/renderer.js +158 -0
  102. package/dist/ui/scrollback/scrollback-writer.d.ts +162 -0
  103. package/dist/ui/scrollback/scrollback-writer.js +549 -0
  104. package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
  105. package/dist/ui/scrollback/settled-boundary.js +106 -0
  106. package/dist/ui/scrollback/use-scrollback-view.d.ts +103 -0
  107. package/dist/ui/scrollback/use-scrollback-view.js +294 -0
  108. package/dist/ui/scrollback/writer-shapes.d.ts +127 -0
  109. package/dist/ui/scrollback/writer-shapes.js +159 -0
  110. package/dist/ui/stdout-frame-writer.js +12 -2
  111. package/dist/ui/terminal-plan.d.ts +52 -0
  112. package/dist/ui/terminal-plan.js +49 -0
  113. package/dist/ui/transcript-text.d.ts +11 -0
  114. package/dist/ui/transcript-text.js +21 -7
  115. package/dist/ui/use-startup-notices.d.ts +12 -1
  116. package/dist/ui/use-startup-notices.js +28 -2
  117. package/package.json +3 -3
  118. package/skills/project-indexer/SKILL.md +163 -163
  119. package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
  120. package/skills/project-indexer/templates/cleancode-template.md +94 -94
  121. package/skills/project-indexer/templates/config-template.md +69 -69
  122. package/skills/project-indexer/templates/index-template.md +129 -129
  123. package/skills/skill-creator/SKILL.md +96 -96
package/dist/ui/App.js CHANGED
@@ -16,13 +16,13 @@ import { computeCost } from '../agent/usage.js';
16
16
  import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
17
17
  import { updatePersistedConfig } from '../config/store.js';
18
18
  import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
19
- import { bumpSubmitCount, setMouseNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
19
+ import { bumpSubmitCount, setMouseNoticeVersion, setScrollModeNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
20
20
  import { getLogger } from '../logging/logger.js';
21
21
  import { isTerminalStatus } from '../proc/types.js';
22
22
  import { getSecrets } from '../logging/secret-registry.js';
23
23
  import { redactText } from '../logging/redact.js';
24
24
  import { resolveTuiCapabilities } from './capabilities.js';
25
- import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
25
+ import { useStartupNotices, MOUSE_NOTICE_VERSION, SCROLL_MODE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
26
26
  import { getTheme } from './theme.js';
27
27
  import { pickGlyphs } from './glyphs.js';
28
28
  import { Header } from './Header.js';
@@ -38,6 +38,12 @@ import { BottomStatusRow } from './BottomStatusRow.js';
38
38
  // Only the updater's pure visibility predicate enters the status detail plan.
39
39
  import { shouldRenderUpdateLine } from '../update/types.js';
40
40
  import { AppShell } from './layout/AppShell.js';
41
+ import { ScrollbackShell } from './ScrollbackShell.js';
42
+ import { useScrollbackView } from './scrollback/use-scrollback-view.js';
43
+ import { buildExitSummary } from './scrollback/exit-summary.js';
44
+ import { LiveTail } from './scrollback/LiveTail.js';
45
+ import { SCROLLBACK_LIMITS } from './scrollback/limits.js';
46
+ import { TODO_LIMITS } from '../todo/limits.js';
41
47
  import { ScrollViewport } from './layout/ScrollViewport.js';
42
48
  import { buildTodoRailLayout } from './layout/todo-layout.js';
43
49
  import { buildTeamPanelLayout } from './layout/team-panel.js';
@@ -132,7 +138,11 @@ function seedViewState(seed) {
132
138
  ? viewReducer(base, { type: 'restoreEntries', entries: seed.entries })
133
139
  : base;
134
140
  }
135
- export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, initialEntries, session, sessionBridge, sessionNotice, }) {
141
+ export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, scrollMode = 'app', scrollback, initialEntries, session, sessionBridge, sessionNotice, }) {
142
+ const native = scrollMode === 'native' && scrollback !== undefined;
143
+ /** Set when the native writer swallowed repeated tall frames; cleared by a resize. */
144
+ const [guardTripped, setGuardTripped] = useState(false);
145
+ const scrollbackViewRef = useRef(null);
136
146
  const { exit } = useApp();
137
147
  const { stdout } = useStdout();
138
148
  const size = useTerminalSize();
@@ -164,8 +174,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
164
174
  * can never step past the window and skip content. The unsafe direction is the
165
175
  * one that matters, and it is unreachable.
166
176
  */
167
- const overlayPage = overlayPageSize(dimensions.viewportRows, cols);
168
- const tooSmall = dimensions.inactive;
177
+ const overlayPage = overlayPageSize(dimensions.viewportRows + (native ? 1 : 0), cols);
178
+ const tooSmall = dimensions.inactive || (native && guardTripped);
169
179
  // --- Render governor (tui-render-performance L4). ------------------------
170
180
  //
171
181
  // FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
@@ -1054,6 +1064,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1054
1064
  nodeVersion: vtInputWarning.nodeVersion,
1055
1065
  onSeen: () => setVtInputNoticeVersion(VT_INPUT_NOTICE_VERSION),
1056
1066
  }
1067
+ : undefined, native
1068
+ ? { onSeen: () => setScrollModeNoticeVersion(SCROLL_MODE_NOTICE_VERSION) }
1057
1069
  : undefined);
1058
1070
  // --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
1059
1071
  useEffect(() => {
@@ -1491,6 +1503,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1491
1503
  ...(sessionBridge?.branch ? { branch: sessionBridge.branch } : {}),
1492
1504
  dispatch: (action) => {
1493
1505
  if (action.type === 'resetConversation' || action.type === 'restoreEntries') {
1506
+ scrollbackViewRef.current?.notifyReset(action.type === 'resetConversation' ? 'clear' : 'restore');
1494
1507
  metrics.current = createRunMetrics();
1495
1508
  manualMetrics.current = createRunMetrics();
1496
1509
  setScrollResetKey((n) => n + 1);
@@ -1541,6 +1554,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1541
1554
  }
1542
1555
  : {}),
1543
1556
  ...(terminal?.writeForeign ? { writeForeign: terminal.writeForeign } : {}),
1557
+ ...(native ? { nativeScroll: true } : {}),
1544
1558
  });
1545
1559
  /**
1546
1560
  * Submitting a prompt no longer touches `config.json` at all — it appends one
@@ -2042,7 +2056,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2042
2056
  setRedrawNonce((n) => n + 1);
2043
2057
  return;
2044
2058
  }
2045
- {
2059
+ // Native mode has no scrollable viewport: the terminal owns scrolling, so these
2060
+ // keys are left alone. An open overlay is application content and keeps them.
2061
+ if (!native || stateRef.current.overlay) {
2046
2062
  if (key.pageUp) {
2047
2063
  scrollBy('pageUp');
2048
2064
  return;
@@ -2063,10 +2079,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2063
2079
  if (key.ctrl && (input === 't' || input === 'T')) {
2064
2080
  const revealing = !stateRef.current.thinkingVisible;
2065
2081
  dispatch({ type: 'toggleThinking' });
2082
+ // Printed history never changes, so say what the toggle actually reaches.
2083
+ const scope = native ? ' Applies to new messages.' : '';
2066
2084
  if (!revealing)
2067
- toast('info', 'Thinking hidden.');
2085
+ toast('info', `Thinking hidden.${scope}`);
2068
2086
  else
2069
- toast('info', 'Thinking shown.');
2087
+ toast('info', `Thinking shown.${scope}`);
2070
2088
  return;
2071
2089
  }
2072
2090
  if (key.ctrl && (input === 'o' || input === 'O')) {
@@ -2090,7 +2108,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2090
2108
  e.kind === 'team' ||
2091
2109
  e.kind === 'compaction' ||
2092
2110
  (e.kind === 'service' && !e.terminal));
2093
- if (last)
2111
+ // Native mode cannot rewrite what is already printed, so it PRINTS an expanded
2112
+ // copy as a new block instead of toggling the old one (spec 4.7).
2113
+ if (last && native) {
2114
+ if (!scrollbackViewRef.current?.reprintExpanded(last.id))
2115
+ toast('info', 'Nothing to expand.');
2116
+ }
2117
+ else if (last)
2094
2118
  dispatch({ type: 'toggleExpand', id: last.id });
2095
2119
  // WIDENED FROM `No tool output to expand.`: with three expandable kinds the
2096
2120
  // old wording would be wrong in two of the three empty cases.
@@ -2226,11 +2250,27 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2226
2250
  // there is the exact cost `tui-render-performance` exists to remove. The
2227
2251
  // running tool, when there is one, is within a few entries of the end.
2228
2252
  let runningTool;
2253
+ // THE WAIT PROJECTION (long-task-jobs §4.9 / AC-28). Derived here, in the same
2254
+ // reverse loop, because both answers come from the same place: which tool is in
2255
+ // flight, and — when that tool is `job_wait` — which job it is waiting for and
2256
+ // for how long. The timeout is read from the CALL'S OWN ARGUMENTS rather than
2257
+ // from the config, so a `timeoutSeconds` the model chose is what the row says.
2258
+ let wait;
2229
2259
  if (running) {
2230
2260
  for (let i = state.entries.length - 1; i >= 0; i -= 1) {
2231
2261
  const e = state.entries[i];
2232
2262
  if (e.kind === 'tool' && e.status === 'running') {
2233
2263
  runningTool = e.name;
2264
+ if (e.name === 'job_wait' && e.args) {
2265
+ const args = e.args;
2266
+ const requested = typeof args.timeoutSeconds === 'number'
2267
+ ? args.timeoutSeconds
2268
+ : cfg.jobs.waitSeconds;
2269
+ wait = {
2270
+ jobId: typeof args.job === 'string' ? args.job : 'job',
2271
+ upToSeconds: Math.max(0, Math.round(requested)),
2272
+ };
2273
+ }
2234
2274
  break;
2235
2275
  }
2236
2276
  }
@@ -2243,7 +2283,6 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2243
2283
  const frameBudget = buildFrameBudget({ rows, cols, draftRows,
2244
2284
  popupRows: overlay ? 0 : popupRows, statusExpanded });
2245
2285
  const viewportBudget = frameBudget.viewportRows;
2246
- const overlayMaxRows = viewportBudget;
2247
2286
  const composerBaseRows = 0;
2248
2287
  const baseRail = buildTodoRailLayout({
2249
2288
  cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
@@ -2263,9 +2302,58 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2263
2302
  itemCount: state.todos?.items.length ?? 0, viewportBudget,
2264
2303
  teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows: 0, composerBaseRows,
2265
2304
  });
2266
- const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
2305
+ const { visible: railVisible, width: railWidth, rows: railRows, contentCols } = railLayout;
2306
+ // The FULL-SCREEN rail only: native mode draws its rail inside the live region's
2307
+ // panel band (`nativeRail` below), because there is no fixed frame to hang a column
2308
+ // on. This name says which of the two it is; the old `showRail` read like a global
2309
+ // switch and that is how native mode ended up with no rail at all.
2310
+ const fullscreenRailVisible = railVisible && !native;
2267
2311
  const transcriptEntries = useMemo(() => selectTranscriptEntries(state.entries, state.pendingSteering), [state.entries, state.pendingSteering]);
2268
- const overlayNode = overlay === 'queue' ? (_jsx(QueueOverlay, { pending: state.pendingSteering, cols: cols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: cols, rows: helpRows(theme, caps), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { isActive: !tooSmall, registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
2312
+ // --- Native scroll (tui-native-terminal-scroll). -----------------------------
2313
+ //
2314
+ // ALWAYS CALLED, INERT WHEN `enabled` IS FALSE: hooks cannot be conditional, and
2315
+ // an inert hook is what keeps `scrollMode: 'app'` byte-identical (I-S9).
2316
+ const viewCols = native ? Math.max(1, cols - 1) : cols;
2317
+ const header = (_jsx(Header, { columns: viewCols, statusExpanded: statusExpanded, scrolledLines: scrolledLines, overlayOpen: overlay !== null, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
2318
+ // The wordmark + getting-started card now live INSIDE the viewport, as its
2319
+ // first block of content, so they scroll away instead of being yanked out of
2320
+ // the header the moment the first message lands.
2321
+ const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(native ? Math.max(0, rows - 8) : viewportBudget, native ? viewCols : contentCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: native ? Math.max(0, rows - 8) : viewportBudget, theme: theme, caps: caps })) : null;
2322
+ const scrollbackView = useScrollbackView({
2323
+ enabled: native, bridge: scrollback, entries: state.entries, runActive: running,
2324
+ theme, caps, density: cfg.density, thinkingVisible: state.thinkingVisible,
2325
+ reducedMotion, rows, cols, headerNode: header, openerNode: opener,
2326
+ overlayOpen: overlay !== null, draftRows, popupRows: overlay ? 0 : popupRows,
2327
+ statusExpanded, forceInactive: native && guardTripped,
2328
+ // The band's inputs. `teamRowsNatural` is the roster layout computed above, so the
2329
+ // band clamps a number this frame already trusts instead of measuring the roster a
2330
+ // second time (`buildTeamPanelLayout` stays the single authority).
2331
+ itemCount: state.todos?.items.length ?? 0,
2332
+ panelEnabled: cfg.todo.panel,
2333
+ teamRowsNatural: teamLayout.rowCount,
2334
+ });
2335
+ scrollbackViewRef.current = scrollbackView;
2336
+ // The two hooks `cli.tsx` calls from OUTSIDE React: the exit summary (every exit
2337
+ // path, some of which run after this tree is gone, so it reads refs, never state)
2338
+ // and the writer's tall-frame guard. A resize clears the degraded state.
2339
+ useLayoutEffect(() => {
2340
+ if (!native || !scrollback)
2341
+ return undefined;
2342
+ scrollback.getSummaryRows = () => {
2343
+ const live = controller.getConfig();
2344
+ return buildExitSummary({
2345
+ entries: stateRef.current.entries, usageTotal: stateRef.current.usageTotal,
2346
+ provider: live.provider, model: live.model, elapsedMs: Date.now() - startedAt.current,
2347
+ ...(identityRef.current ? { sessionId: identityRef.current.id } : {}), glyphs,
2348
+ });
2349
+ };
2350
+ scrollback.onGuardTrip = () => setGuardTripped(true);
2351
+ return () => { scrollback.onGuardTrip = null; };
2352
+ }, [native, scrollback, controller, glyphs]);
2353
+ useEffect(() => { setGuardTripped(false); }, [rows, cols]);
2354
+ const nativeBudget = native ? scrollbackView.budget : null;
2355
+ const overlayMaxRows = nativeBudget ? nativeBudget.overlayRows : viewportBudget;
2356
+ const overlayNode = overlay === 'queue' ? (_jsx(QueueOverlay, { pending: state.pendingSteering, cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: viewCols, rows: helpRows(theme, caps, native), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { isActive: !tooSmall, registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
2269
2357
  provider: cfg.provider,
2270
2358
  model: cfg.model,
2271
2359
  baseUrl: cfg.baseUrl ?? '',
@@ -2299,15 +2387,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2299
2387
  // the save handler cannot disagree about what `(inherit)` means.
2300
2388
  ...fastSettingsFrom(cfg.fast),
2301
2389
  ...compactionSettingsFrom(cfg.compaction),
2302
- }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, draft: settingsDraft, probe: probe, onProfileSave: handleProfileSave, onReload: () => controller.createModelSettingsDraft(), onClose: () => dispatch({ type: 'setOverlay', overlay: null }),
2390
+ }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, draft: settingsDraft, probe: probe, onProfileSave: handleProfileSave, onReload: () => controller.createModelSettingsDraft(), onClose: () => dispatch({ type: 'setOverlay', overlay: null }),
2303
2391
  // Read from `cfg` (render scope, re-read every frame), so `/retry off`
2304
2392
  // while the screen is open shows the new value on the next paint.
2305
- retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { isActive: !tooSmall, state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { isActive: !tooSmall, questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { isActive: !tooSmall, plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
2306
- const header = (_jsx(Header, { columns: cols, statusExpanded: statusExpanded, scrolledLines: scrolledLines, overlayOpen: overlay !== null, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
2307
- // The wordmark + getting-started card now live INSIDE the viewport, as its
2308
- // first block of content, so they scroll away instead of being yanked out of
2309
- // the header the moment the first message lands.
2310
- const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: viewportBudget, theme: theme, caps: caps })) : null;
2393
+ retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { isActive: !tooSmall, state: confirmState, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { isActive: !tooSmall, questions: humanRequest.questions, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { isActive: !tooSmall, plan: humanRequest.plan, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
2311
2394
  const density = cfg.density;
2312
2395
  const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
2313
2396
  // The fixed status bar remains visible underneath every overlay and toast.
@@ -2326,9 +2409,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2326
2409
  heightsMeasured: heights.stats().measured,
2327
2410
  heightsEstimated: heights.stats().estimated,
2328
2411
  cols: contentCols,
2329
- mode: 'fullscreen',
2330
- viewportRows: viewportBudget,
2412
+ mode: native ? 'native' : 'fullscreen',
2413
+ viewportRows: nativeBudget ? nativeBudget.liveRows : viewportBudget,
2331
2414
  offset: scrolledLines,
2415
+ // `null` outside native mode, and `/perf` prints `n/a` for it: a band row of
2416
+ // zeroes in full-screen mode would read like a panel that failed to show.
2417
+ band: nativeBudget
2418
+ ? { ...nativeBudget.band, contentCols: nativeBudget.contentCols }
2419
+ : null,
2332
2420
  // A getter: the bridge's `frameReady` and geometry change outside React renders.
2333
2421
  get scrollbar() {
2334
2422
  return readScrollbarPerf({
@@ -2376,9 +2464,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2376
2464
  level: latestToast.level === 'success' ? 'info' : latestToast.level } : undefined,
2377
2465
  });
2378
2466
  const statusInput = {
2379
- columns: cols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
2467
+ columns: viewCols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
2380
2468
  context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
2381
2469
  elapsedMs, tokPerSec, speedKnown, activeTool: state.activeTool, runOutcome: state.runOutcome,
2470
+ // The bounded-wait label, when `job_wait` is what is holding the run. Absent
2471
+ // for every other tool, which keeps every existing plan byte-identical.
2472
+ ...(wait ? { wait } : {}),
2382
2473
  waitingForConfirmation: !!humanRequest || !!confirmState,
2383
2474
  stopping: interruptGesture.current.phase === 'stopping',
2384
2475
  interruptPhase: interruptGesture.current.phase,
@@ -2392,7 +2483,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2392
2483
  ...feedbackProjection,
2393
2484
  };
2394
2485
  const details = planStatusDetail({ status: statusInput, model: cfg.model, provider: cfg.provider,
2395
- hints: { interactionPhase: interactionPhase.current, cols, completion, overlay,
2486
+ hints: { interactionPhase: interactionPhase.current, cols: viewCols, completion, overlay,
2396
2487
  selectionPending: selectionController?.hasPendingSelection() ?? false,
2397
2488
  copyInFlight: copyState.busy, copyCleanupPending: copyState.cleanupPending,
2398
2489
  hasDraft: hasDraftRef.current, services: liveServices,
@@ -2401,45 +2492,72 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2401
2492
  updateAvailable: !!updateSnapshot && shouldRenderUpdateLine(updateSnapshot) } });
2402
2493
  const composer = (_jsx(Composer, { deleteDisambiguated: terminal?.deleteDisambiguated ?? false, onCompletionContextChange: setCompletion, cols: frameBudget.composerCols, terminalRows: rows, statusExpanded: statusExpanded, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: false, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: frameBudget.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onDraftRows: onDraftRows, onDraftPresence: onDraftPresence, draftClearNonce: draftClearNonce, onNotice: notify, theme: theme, caps: caps, onSubmit: handleSubmit, onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
2403
2494
  const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2404
- const rail = showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2495
+ // --- Native panel band (release-gate-and-native-panels §6.3/§6.4) -------------
2496
+ //
2497
+ // SEPARATE NODES, NOT THE TWO ABOVE (RV-P1-7). Those carry `cols={contentCols}` for
2498
+ // the full-screen layout, which has to stay byte-identical (I-3); reusing them here
2499
+ // would silently narrow it. Every number below comes from `budget.band`, and the
2500
+ // number that decides whether a panel mounts IS the number it lays out against - a
2501
+ // panel that mounts on one value and lays out against another renders nothing and
2502
+ // reports nothing (TodoPanel's own I-10 / P1-3 note).
2503
+ const nativeBand = nativeBudget ? nativeBudget.band : null;
2504
+ const nativeRail = state.todos && nativeBand && nativeBand.railRows >= TODO_LIMITS.panelMinRows
2505
+ ? (_jsx(TodoPanel, { snapshot: state.todos, width: nativeBand.railCols, rows: nativeBand.railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2506
+ // `cols` is the band's left column, not `contentCols`: `TeamPanel.activityBudget` is
2507
+ // continuous in `cols`, so a roster laid out against a column it is not in is exactly
2508
+ // the bug that pushes the elapsed column off an 80-column terminal.
2509
+ const nativeTeam = state.team && nativeBand ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: nativeBand.previewCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2510
+ const rail = fullscreenRailVisible && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2405
2511
  const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: overlayNode, footer: _jsxs(_Fragment, { children: [team, _jsx(QueuePanel, { layout: queueLayout, theme: theme })] }), rail: rail, scrollbar: terminal?.scrollbar, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, onGeometryChange: onGeometryChange, geometryKey: `${rows}:${cols}:${viewportBudget}:${frameBudget.composerSlotRows}:${frameBudget.statusRows}:${contentCols}:${overlay}:${tooSmall}`, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: transcriptEntries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }));
2406
- return (_jsx(AppShell, { inactive: tooSmall, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), rows: rows, cols: cols, header: header, viewport: viewport, viewportRows: frameBudget.viewportRows, composer: composer, composerSlotRows: frameBudget.composerSlotRows, statusRows: frameBudget.statusRows, details: _jsx(BottomStatusRow, { plan: details, columns: cols, theme: theme }), status: _jsx(StatusBar, { columns: cols, speedKnown: speedKnown, ...feedbackProjection, interruptPhase: interruptGesture.current.phase, pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, runOutcome: state.runOutcome, reducedMotion: reducedMotion, ...(sessionId ? { sessionId } : {}), waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
2407
- ? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
2408
- // ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
2409
- // projection of "many sources of truth" into the props layer, and a
2410
- // caller could update three of them; a single reading cannot be
2411
- // partially stale. The window and its `known` flag now travel INSIDE
2412
- // it, resolved by the meter - which is what makes the user's
2413
- // `contextWindow` override reach the bar.
2414
- 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
2415
- ? {
2416
- teamActive: {
2417
- running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
2418
- r.phase !== 'failed' && r.phase !== 'aborted').length,
2419
- total: state.team.runs.length,
2420
- },
2421
- }
2422
- : {}), ...(state.todos && !showRail
2423
- ? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
2424
- : {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
2425
- // Manual compaction starts before its next snapshot is published.
2426
- compactionActive: { inFlight: statusInput.compacting ?? false }, ...(controller.isCompactionEnabled()
2427
- ? {
2428
- gaugeMarks: {
2429
- warn: cfg.compaction.warnThreshold * 100,
2430
- high: cfg.compaction.threshold * 100,
2431
- },
2432
- }
2433
- : {}), ...(state.retry
2434
- ? {
2435
- retryActive: {
2436
- attempt: state.retry.attempt,
2437
- max: state.retry.maxRetries,
2438
- // Recomputed at RENDER from the absolute `resumeAt`, which is
2439
- // what keeps the chip and the transcript card showing the same
2440
- // number without either of them owning a second clock.
2441
- secondsLeft: secondsLeft(state.retry.resumeAt, Date.now()),
2442
- },
2443
- }
2444
- : {}) }) }));
2512
+ const detailsNode = _jsx(BottomStatusRow, { plan: details, columns: viewCols, theme: theme });
2513
+ const statusNode = (_jsx(StatusBar, { columns: viewCols, speedKnown: speedKnown, ...feedbackProjection, interruptPhase: interruptGesture.current.phase, pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, ...(wait ? { wait } : {}), runOutcome: state.runOutcome, reducedMotion: reducedMotion, ...(sessionId ? { sessionId } : {}), waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
2514
+ ? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
2515
+ // ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
2516
+ // projection of "many sources of truth" into the props layer, and a
2517
+ // caller could update three of them; a single reading cannot be
2518
+ // partially stale. The window and its `known` flag now travel INSIDE
2519
+ // it, resolved by the meter - which is what makes the user's
2520
+ // `contextWindow` override reach the bar.
2521
+ 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
2522
+ ? {
2523
+ teamActive: {
2524
+ running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
2525
+ r.phase !== 'failed' && r.phase !== 'aborted').length,
2526
+ total: state.team.runs.length,
2527
+ },
2528
+ }
2529
+ : {}), ...(state.todos && !(native
2530
+ ? (nativeBudget?.band.railRows ?? 0) > 0
2531
+ : fullscreenRailVisible)
2532
+ ? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
2533
+ : {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
2534
+ // Manual compaction starts before its next snapshot is published.
2535
+ compactionActive: { inFlight: statusInput.compacting ?? false }, ...(controller.isCompactionEnabled()
2536
+ ? {
2537
+ gaugeMarks: {
2538
+ warn: cfg.compaction.warnThreshold * 100,
2539
+ high: cfg.compaction.threshold * 100,
2540
+ },
2541
+ }
2542
+ : {}), ...(state.retry
2543
+ ? {
2544
+ retryActive: {
2545
+ attempt: state.retry.attempt,
2546
+ max: state.retry.maxRetries,
2547
+ // Recomputed at RENDER from the absolute `resumeAt`, which is
2548
+ // what keeps the chip and the transcript card showing the same
2549
+ // number without either of them owning a second clock.
2550
+ secondsLeft: secondsLeft(state.retry.resumeAt, Date.now()),
2551
+ },
2552
+ }
2553
+ : {}) }));
2554
+ if (nativeBudget) {
2555
+ return (_jsx(ScrollbackShell, { budget: nativeBudget, overlay: overlayNode, liveTailLines: scrollbackView.liveTailLines,
2556
+ // Real components for the unfinished tail (I-7); the plain rows above stay as
2557
+ // the fallback for a frame with no preview rows.
2558
+ tail: nativeBudget.band.previewRows > 0
2559
+ ? (_jsx(LiveTail, { unsettled: scrollbackView.unsettledEntries, maxEntries: SCROLLBACK_LIMITS.liveTailMaxEntries, rows: nativeBudget.band.previewRows, cols: nativeBudget.previewCols, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: cfg.density, nowSec: nowSec, theme: theme, caps: caps }))
2560
+ : undefined, team: nativeTeam, rail: nativeRail, composer: composer, status: statusNode, details: detailsNode, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), theme: theme }));
2561
+ }
2562
+ return (_jsx(AppShell, { inactive: tooSmall, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), rows: rows, cols: cols, header: header, viewport: viewport, viewportRows: frameBudget.viewportRows, composer: composer, composerSlotRows: frameBudget.composerSlotRows, statusRows: frameBudget.statusRows, details: detailsNode, status: statusNode }));
2445
2563
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * ScrollbackShell — the bounded LIVE REGION of native-scroll mode
3
+ * (tui-native-terminal-scroll §4.6).
4
+ *
5
+ * The full-screen `AppShell` fills the whole terminal; this shell is only as tall as
6
+ * `budget.liveRows` (the root box height IS that number: giving it the frame height
7
+ * would make the "small region" a whole screen again, and Ink would then repaint a
8
+ * screenful per frame). Everything settled has already been printed into the
9
+ * terminal's own scrollback above it.
10
+ *
11
+ * Top to bottom: [overlay | panel band][composer][status][details].
12
+ *
13
+ * THE PANEL BAND IS THE PART NATIVE MODE WAS MISSING
14
+ * (release-gate-and-native-panels §6.1). A fixed frame can put a rail beside the
15
+ * transcript; without one the rail and the roster simply had nowhere to go, so the
16
+ * default mode showed neither. The band is a row pool inside the live region:
17
+ *
18
+ * +---------------------------------------------+-----------------+
19
+ * | team roster (height = band.teamRows) | TODO rail |
20
+ * +---------------------------------------------+ (height = |
21
+ * | unfinished tail (height = band.previewRows) | band.railRows) |
22
+ * +---------------------------------------------+-----------------+
23
+ *
24
+ * Every box in it has an explicit `height` and `width` plus `overflow="hidden"`, and
25
+ * that is a correctness requirement rather than tidiness: a box that grows with its
26
+ * content would make the region taller than `budget.liveRows`, and a frame as tall as
27
+ * the terminal makes Ink erase the user's scrollback (I-4).
28
+ *
29
+ * `liveTailLines` STAYS. It is the plain-text fallback for the frames where the real
30
+ * tail is not mounted (and the floor of the measurement path); `tail` wins when both
31
+ * are given, so the two can never be drawn on top of each other.
32
+ */
33
+ import React from 'react';
34
+ import type { Theme } from './theme.js';
35
+ import type { ScrollbackBudget } from './scrollback/budget.js';
36
+ export interface ScrollbackShellProps {
37
+ budget: ScrollbackBudget;
38
+ /** Shown instead of the band when an overlay is open. */
39
+ overlay: React.ReactNode | null;
40
+ /** Plain preview rows: the fallback when `tail` is absent (and what `/perf` counts). */
41
+ liveTailLines: readonly string[];
42
+ /** Real components for the unfinished tail (`LiveTail`). */
43
+ tail?: React.ReactNode;
44
+ /** The subagent roster, drawn at the top of the band's left column. */
45
+ team?: React.ReactNode;
46
+ /** The TODO rail, drawn as the band's right-hand column. */
47
+ rail?: React.ReactNode;
48
+ composer: React.ReactNode;
49
+ status: React.ReactNode;
50
+ details?: React.ReactNode;
51
+ placeholder: React.ReactNode;
52
+ theme: Theme;
53
+ }
54
+ export declare function ScrollbackShell({ budget, overlay, liveTailLines, tail, team, rail, composer, status, details, placeholder, theme, }: ScrollbackShellProps): React.ReactElement;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function ScrollbackShell({ budget, overlay, liveTailLines, tail, team, rail, composer, status, details, placeholder, theme, }) {
4
+ const width = budget.contentCols;
5
+ if (budget.inactive) {
6
+ return (_jsx(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: budget.liveRows > 0 ? placeholder : null }));
7
+ }
8
+ const band = budget.band;
9
+ const tailLines = liveTailLines.slice(-budget.liveTailRows);
10
+ // The overlay and the band are alternatives, not siblings: the band reports
11
+ // `bandRows = remaining` for an overlay frame, so drawing both would double the
12
+ // region's height and take the frame to the terminal height (the `ESC[3J` case).
13
+ const top = band.reason === 'overlay' ? (_jsx(Box, { flexDirection: "column", height: budget.overlayRows, flexShrink: 0, overflow: "hidden", children: overlay })) : band.bandRows > 0 ? (_jsxs(Box, { flexDirection: "row", height: band.bandRows, flexShrink: 0, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", width: budget.previewCols, flexShrink: 0, overflow: "hidden", children: [band.teamRows > 0 && (_jsx(Box, { height: band.teamRows, flexShrink: 0, overflow: "hidden", children: team })), band.previewRows > 0 && (_jsx(Box, { flexDirection: "column", height: band.previewRows, flexShrink: 0, justifyContent: "flex-end", overflow: "hidden", children: tail ?? tailLines.map((line, index) => (_jsx(Text, { wrap: "truncate", color: theme.muted, children: line }, index))) }))] }), band.railRows > 0 && (_jsx(Box, { height: band.railRows, width: band.railCols, flexShrink: 0, overflow: "hidden", children: rail }))] })) : null;
14
+ return (_jsxs(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: [top, _jsx(Box, { flexDirection: "column", height: budget.composerSlotRows, justifyContent: "flex-end", flexShrink: 0, overflow: "hidden", children: composer }), _jsx(Box, { height: budget.statusRows > 0 ? 1 : 0, flexShrink: 0, overflow: "hidden", children: status }), budget.statusRows === 2 && (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: details }))] }));
15
+ }
@@ -19,6 +19,11 @@ export interface StatusBarProps {
19
19
  name: string;
20
20
  toolCallId: string;
21
21
  } | null;
22
+ /** A bounded wait in flight; replaces the `Running job_wait` label (§4.9). */
23
+ wait?: {
24
+ jobId: string;
25
+ upToSeconds: number;
26
+ };
22
27
  runOutcome?: "none" | "ended" | "interrupted" | "failed";
23
28
  reducedMotion?: boolean;
24
29
  waitingForConfirmation?: boolean;
@@ -27,6 +27,7 @@ export function StatusBar(props) {
27
27
  context: props.context, usageTotal: props.usageTotal,
28
28
  thinkingLevel: props.thinkingLevel, elapsedMs: props.elapsedMs, tokPerSec: props.tokPerSec,
29
29
  activeTool: props.activeTool, runOutcome: props.runOutcome, spinner: glyph,
30
+ ...(props.wait ? { wait: props.wait } : {}),
30
31
  speedKnown: props.speedKnown, feedback: props.feedback, escapeAction: props.escapeAction,
31
32
  interruptPhase: props.interruptPhase,
32
33
  waitingForConfirmation: props.waitingForConfirmation, stopping: props.stopping,
@@ -26,6 +26,20 @@ interface TranscriptProps {
26
26
  theme: Theme;
27
27
  caps: TermCapabilities;
28
28
  }
29
+ /**
30
+ * The value `EntryView` should receive for `nowSec` on THIS entry.
31
+ *
32
+ * `undefined` for everything that is not a running tool, which is what keeps
33
+ * every other card's memo boundary intact — `undefined === undefined` in the
34
+ * comparator, on every frame, forever.
35
+ *
36
+ * EXPORTED for `ui/scrollback/LiveTail.tsx` (release-gate-and-native-panels §6.5), and
37
+ * that is the whole of this feature's change to this file: the live region mounts the
38
+ * same `EntryView` the transcript does, so it must feed it the same per-entry clock.
39
+ * A copy of this rule in the band would be a second answer to "does this card tick?"
40
+ * and the two would disagree the first time a consumer is added.
41
+ */
42
+ export declare function nowSecFor(entry: Entry, nowSec: number | undefined): number | undefined;
29
43
  export interface EntryViewProps {
30
44
  entry: Entry;
31
45
  /** Previous entry in the list; `undefined` for the first item. */
@@ -27,8 +27,14 @@ import { advanceTailRows } from './layout/follow-state.js';
27
27
  * `undefined` for everything that is not a running tool, which is what keeps
28
28
  * every other card's memo boundary intact — `undefined === undefined` in the
29
29
  * comparator, on every frame, forever.
30
+ *
31
+ * EXPORTED for `ui/scrollback/LiveTail.tsx` (release-gate-and-native-panels §6.5), and
32
+ * that is the whole of this feature's change to this file: the live region mounts the
33
+ * same `EntryView` the transcript does, so it must feed it the same per-entry clock.
34
+ * A copy of this rule in the band would be a second answer to "does this card tick?"
35
+ * and the two would disagree the first time a consumer is added.
30
36
  */
31
- function nowSecFor(entry, nowSec) {
37
+ export function nowSecFor(entry, nowSec) {
32
38
  if (entry.kind === 'tool' && entry.status === 'running')
33
39
  return nowSec;
34
40
  // THE LIVE COMPACTION CARD IS THE SECOND CONSUMER (hardening W5). It is at most
@@ -36,6 +42,12 @@ function nowSecFor(entry, nowSec) {
36
42
  // function protects is unchanged for every other card, forever.
37
43
  if (entry.kind === 'compaction' && entry.live)
38
44
  return nowSec;
45
+ // THE THIRD CONSUMER IS A LIVE JOB (long-task-jobs D-8), and it is the reason
46
+ // this stays a single 1 Hz clock: a task's elapsed time and its `no output`
47
+ // badge both have to move, and a per-card `setInterval` would be one timer per
48
+ // job. `terminal` entries are excluded so a settled record never re-renders.
49
+ if (entry.kind === 'service' && !entry.terminal)
50
+ return nowSec;
39
51
  return undefined;
40
52
  }
41
53
  function noticeColor(level, theme) {
@@ -113,7 +125,7 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
113
125
  ? Math.max(0, nowSec * 1000 - entry.startedAt)
114
126
  : undefined, live: entry.live, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
115
127
  case 'service':
116
- return frame(serviceGlyph(entry, glyphs), serviceCardColor(entry, theme), _jsx(ServiceCard, { serviceId: entry.serviceId, command: entry.command, status: entry.status, url: entry.url, port: entry.port, exitCode: entry.exitCode, startedAt: entry.startedAt, readyAt: entry.readyAt, endedAt: entry.endedAt, rows: entry.rows, terminal: entry.terminal, killIncomplete: entry.killIncomplete, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
128
+ return frame(serviceGlyph(entry, glyphs), serviceCardColor(entry, theme), _jsx(ServiceCard, { serviceId: entry.serviceId, jobKind: entry.jobKind, command: entry.command, status: entry.status, url: entry.url, port: entry.port, exitCode: entry.exitCode, startedAt: entry.startedAt, readyAt: entry.readyAt, endedAt: entry.endedAt, rows: entry.rows, terminal: entry.terminal, killIncomplete: entry.killIncomplete, progress: entry.progress, stalled: entry.stalled, lastOutputAt: entry.lastOutputAt, nowSec: nowSec, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
117
129
  case 'notice':
118
130
  return frame(noticeSymbol(entry.level, theme), noticeColor(entry.level, theme), _jsx(Text, { color: noticeColor(entry.level, theme), children: entry.text }));
119
131
  default:
@@ -23,9 +23,11 @@ import React from 'react';
23
23
  import type { Theme } from '../theme.js';
24
24
  import type { TermCapabilities } from '../capabilities.js';
25
25
  import { pickGlyphs } from '../glyphs.js';
26
- import type { ServiceStatus } from '../../proc/types.js';
26
+ import type { JobKind, ServiceStatus } from '../../proc/types.js';
27
27
  export interface ServiceCardProps {
28
28
  serviceId: string;
29
+ /** `service` or `task`: NOUN ONLY, never a rename of the entry kind (I-2). */
30
+ jobKind?: JobKind;
29
31
  command: string;
30
32
  status: ServiceStatus;
31
33
  url?: string;
@@ -39,6 +41,20 @@ export interface ServiceCardProps {
39
41
  terminal?: boolean;
40
42
  killIncomplete?: boolean;
41
43
  expanded?: boolean;
44
+ /** The extracted progress line, one row, already clipped by the supervisor. */
45
+ progress?: string;
46
+ /** Silence past `jobs.stallSeconds` - a muted badge, never an action. */
47
+ stalled?: boolean;
48
+ /** When output last arrived; with `nowSec` it renders `no output 6m02s`. */
49
+ lastOutputAt?: number;
50
+ /**
51
+ * The transcript's 1 Hz clock (D-8).
52
+ *
53
+ * ONE CLOCK FOR EVERYTHING. A per-card `setInterval` would be one timer per job,
54
+ * and the transcript's comparator already includes `nowSec`, so the live elapsed
55
+ * figure and the `no output` figure update for free.
56
+ */
57
+ nowSec?: number;
42
58
  /** Accepted for comparator consistency; this card never animates (D-14). */
43
59
  reducedMotion?: boolean;
44
60
  theme: Theme;