@aragon-agent/cli 0.6.31 → 0.6.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +119 -0
- package/README.md +2970 -2826
- package/dist/agent/controller.d.ts +46 -0
- package/dist/agent/controller.js +175 -7
- package/dist/agent/job-notices.d.ts +143 -0
- package/dist/agent/job-notices.js +182 -0
- package/dist/agent/reducer.d.ts +21 -1
- package/dist/agent/reducer.js +7 -0
- package/dist/agent/system-prompt.d.ts +12 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +236 -8
- package/dist/commands/builtins.js +23 -8
- package/dist/commands/perf.d.ts +77 -1
- package/dist/commands/perf.js +37 -5
- package/dist/config/cli-commands.d.ts +27 -0
- package/dist/config/cli-commands.js +54 -1
- package/dist/config/env.js +38 -0
- package/dist/config/load.js +50 -1
- package/dist/config/schema.d.ts +73 -0
- package/dist/config/schema.js +34 -0
- package/dist/config/store.js +14 -1
- package/dist/exec/events.d.ts +29 -3
- package/dist/exec/events.js +3 -12
- package/dist/exec/runner.d.ts +24 -0
- package/dist/exec/runner.js +79 -0
- package/dist/input/cursor-report.d.ts +61 -0
- package/dist/input/cursor-report.js +97 -0
- package/dist/input/stdin-filter.d.ts +22 -0
- package/dist/input/stdin-filter.js +48 -3
- package/dist/proc/limits.d.ts +76 -2
- package/dist/proc/limits.js +83 -2
- package/dist/proc/log-mirror.d.ts +59 -0
- package/dist/proc/log-mirror.js +203 -0
- package/dist/proc/log-ring.d.ts +22 -0
- package/dist/proc/log-ring.js +20 -1
- package/dist/proc/notices.d.ts +57 -0
- package/dist/proc/notices.js +101 -0
- package/dist/proc/progress.d.ts +40 -0
- package/dist/proc/progress.js +135 -0
- package/dist/proc/prompt.d.ts +24 -2
- package/dist/proc/prompt.js +51 -2
- package/dist/proc/supervisor.d.ts +35 -2
- package/dist/proc/supervisor.js +142 -43
- package/dist/proc/types.d.ts +89 -1
- package/dist/proc/wait.d.ts +57 -0
- package/dist/proc/wait.js +133 -0
- package/dist/session/persist.js +3 -1
- package/dist/session/validate-session.js +17 -0
- package/dist/tools/bash-tool.d.ts +12 -1
- package/dist/tools/bash-tool.js +100 -13
- package/dist/tools/index.d.ts +26 -2
- package/dist/tools/index.js +43 -2
- package/dist/tools/job-tools.d.ts +54 -0
- package/dist/tools/job-tools.js +329 -0
- package/dist/tools/proc-tools.d.ts +1 -1
- package/dist/tools/proc-tools.js +49 -17
- package/dist/ui/ActivityLine.js +7 -0
- package/dist/ui/App.js +98 -12
- package/dist/ui/ScrollbackShell.d.ts +62 -11
- package/dist/ui/ScrollbackShell.js +11 -3
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.d.ts +14 -0
- package/dist/ui/Transcript.js +14 -2
- package/dist/ui/entries/ServiceCard.d.ts +17 -1
- package/dist/ui/entries/ServiceCard.js +39 -4
- package/dist/ui/layout/status-layout.d.ts +13 -1
- package/dist/ui/layout/status-layout.js +10 -0
- package/dist/ui/layout/virtual-window.js +14 -1
- package/dist/ui/run-status-row.d.ts +12 -0
- package/dist/ui/screen.d.ts +17 -4
- package/dist/ui/screen.js +14 -6
- package/dist/ui/scrollback/LiveTail.d.ts +79 -0
- package/dist/ui/scrollback/LiveTail.js +32 -0
- package/dist/ui/scrollback/band.d.ts +114 -0
- package/dist/ui/scrollback/band.js +130 -0
- package/dist/ui/scrollback/budget.d.ts +51 -10
- package/dist/ui/scrollback/budget.js +69 -20
- package/dist/ui/scrollback/canvas.d.ts +99 -0
- package/dist/ui/scrollback/canvas.js +113 -0
- package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
- package/dist/ui/scrollback/cursor-probe.js +133 -0
- package/dist/ui/scrollback/limits.d.ts +61 -4
- package/dist/ui/scrollback/limits.js +61 -4
- package/dist/ui/scrollback/live-preview.d.ts +18 -10
- package/dist/ui/scrollback/live-preview.js +14 -11
- package/dist/ui/scrollback/scrollback-writer.d.ts +104 -16
- package/dist/ui/scrollback/scrollback-writer.js +368 -82
- package/dist/ui/scrollback/tail-window.d.ts +60 -0
- package/dist/ui/scrollback/tail-window.js +73 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +34 -15
- package/dist/ui/scrollback/use-scrollback-view.js +110 -59
- package/dist/ui/scrollback/writer-shapes.d.ts +68 -2
- package/dist/ui/scrollback/writer-shapes.js +53 -8
- package/package.json +3 -3
package/dist/ui/App.js
CHANGED
|
@@ -41,6 +41,9 @@ import { AppShell } from './layout/AppShell.js';
|
|
|
41
41
|
import { ScrollbackShell } from './ScrollbackShell.js';
|
|
42
42
|
import { useScrollbackView } from './scrollback/use-scrollback-view.js';
|
|
43
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';
|
|
44
47
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
45
48
|
import { buildTodoRailLayout } from './layout/todo-layout.js';
|
|
46
49
|
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
@@ -2247,11 +2250,27 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2247
2250
|
// there is the exact cost `tui-render-performance` exists to remove. The
|
|
2248
2251
|
// running tool, when there is one, is within a few entries of the end.
|
|
2249
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;
|
|
2250
2259
|
if (running) {
|
|
2251
2260
|
for (let i = state.entries.length - 1; i >= 0; i -= 1) {
|
|
2252
2261
|
const e = state.entries[i];
|
|
2253
2262
|
if (e.kind === 'tool' && e.status === 'running') {
|
|
2254
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
|
+
}
|
|
2255
2274
|
break;
|
|
2256
2275
|
}
|
|
2257
2276
|
}
|
|
@@ -2274,9 +2293,17 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2274
2293
|
pending: state.pendingSteering, columns: baseRail.contentCols, terminalRows: rows,
|
|
2275
2294
|
availableRows: Math.max(0, viewportBudget - composerBaseRows - 1), paused: !running,
|
|
2276
2295
|
}), [state.pendingSteering, baseRail.contentCols, rows, viewportBudget, composerBaseRows, running]);
|
|
2296
|
+
// THE POOL, NOT THE VIEWPORT MINUS A QUEUE THAT IS NOT DRAWN
|
|
2297
|
+
// (tui-full-height-live-region RV-P1-3). `QueuePanel` only mounts in the full-screen
|
|
2298
|
+
// layout, so native mode used to reserve rows for a card nobody paints. And it MUST be
|
|
2299
|
+
// `viewportBudget` rather than `nativeBudget.band.*`: `viewportRows === remaining` by
|
|
2300
|
+
// construction, and reading the band back here would close a loop with `teamRows`
|
|
2301
|
+
// (AC-34, asserted textually by `live-band.test.ts`).
|
|
2277
2302
|
const teamLayout = buildTeamPanelLayout({
|
|
2278
2303
|
snapshot: state.team, terminalRows: rows,
|
|
2279
|
-
availableRows:
|
|
2304
|
+
availableRows: native
|
|
2305
|
+
? Math.max(0, viewportBudget - 1)
|
|
2306
|
+
: Math.max(0, viewportBudget - composerBaseRows - queueLayout.rows - 1),
|
|
2280
2307
|
});
|
|
2281
2308
|
const railLayout = buildTodoRailLayout({
|
|
2282
2309
|
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
@@ -2284,8 +2311,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2284
2311
|
teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows: 0, composerBaseRows,
|
|
2285
2312
|
});
|
|
2286
2313
|
const { visible: railVisible, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
2287
|
-
//
|
|
2288
|
-
|
|
2314
|
+
// The FULL-SCREEN rail only: native mode draws its rail inside the live region's
|
|
2315
|
+
// panel band (`nativeRail` below), because there is no fixed frame to hang a column
|
|
2316
|
+
// on. This name says which of the two it is; the old `showRail` read like a global
|
|
2317
|
+
// switch and that is how native mode ended up with no rail at all.
|
|
2318
|
+
const fullscreenRailVisible = railVisible && !native;
|
|
2289
2319
|
const transcriptEntries = useMemo(() => selectTranscriptEntries(state.entries, state.pendingSteering), [state.entries, state.pendingSteering]);
|
|
2290
2320
|
// --- Native scroll (tui-native-terminal-scroll). -----------------------------
|
|
2291
2321
|
//
|
|
@@ -2293,16 +2323,22 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2293
2323
|
// an inert hook is what keeps `scrollMode: 'app'` byte-identical (I-S9).
|
|
2294
2324
|
const viewCols = native ? Math.max(1, cols - 1) : cols;
|
|
2295
2325
|
const header = (_jsx(Header, { columns: viewCols, statusExpanded: statusExpanded, scrolledLines: scrolledLines, overlayOpen: overlay !== null, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
|
|
2296
|
-
// The
|
|
2297
|
-
//
|
|
2298
|
-
//
|
|
2299
|
-
|
|
2326
|
+
// The header and the opener are SLOTS OF THE SHELL, not printed bytes (D3/D4): at
|
|
2327
|
+
// `H = rows - 1` a printed row is pushed off the screen by the very next frame, so
|
|
2328
|
+
// `printBanner()` was a way of making the banner invisible. Neither is passed here.
|
|
2329
|
+
// The opener node is built below, because its height now comes from the native band.
|
|
2300
2330
|
const scrollbackView = useScrollbackView({
|
|
2301
2331
|
enabled: native, bridge: scrollback, entries: state.entries, runActive: running,
|
|
2302
2332
|
theme, caps, density: cfg.density, thinkingVisible: state.thinkingVisible,
|
|
2303
|
-
reducedMotion, rows, cols,
|
|
2333
|
+
reducedMotion, rows, cols,
|
|
2304
2334
|
overlayOpen: overlay !== null, draftRows, popupRows: overlay ? 0 : popupRows,
|
|
2305
2335
|
statusExpanded, forceInactive: native && guardTripped,
|
|
2336
|
+
// The band's inputs. `teamRowsNatural` is the roster layout computed above, so the
|
|
2337
|
+
// band clamps a number this frame already trusts instead of measuring the roster a
|
|
2338
|
+
// second time (`buildTeamPanelLayout` stays the single authority).
|
|
2339
|
+
itemCount: state.todos?.items.length ?? 0,
|
|
2340
|
+
panelEnabled: cfg.todo.panel,
|
|
2341
|
+
teamRowsNatural: teamLayout.rowCount,
|
|
2306
2342
|
});
|
|
2307
2343
|
scrollbackViewRef.current = scrollbackView;
|
|
2308
2344
|
// The two hooks `cli.tsx` calls from OUTSIDE React: the exit summary (every exit
|
|
@@ -2324,6 +2360,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2324
2360
|
}, [native, scrollback, controller, glyphs]);
|
|
2325
2361
|
useEffect(() => { setGuardTripped(false); }, [rows, cols]);
|
|
2326
2362
|
const nativeBudget = native ? scrollbackView.budget : null;
|
|
2363
|
+
// THE EMPTY-SESSION CARD, ONE NODE FOR BOTH MODES (D4 / RV-P0-1). The wordmark and the
|
|
2364
|
+
// getting-started card are the first block of content in `app` mode (they scroll away
|
|
2365
|
+
// with the transcript) and the shell's `opener` slot in native mode. Its height used to
|
|
2366
|
+
// be a `rows - 8` guess in native mode; the honest input is the band's own pool, which
|
|
2367
|
+
// is why this node is built after `nativeBudget`.
|
|
2368
|
+
const openerRows = nativeBudget ? nativeBudget.band.bandRows : viewportBudget;
|
|
2369
|
+
const openerCols = nativeBudget ? nativeBudget.previewCols : contentCols;
|
|
2370
|
+
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(openerRows, openerCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: openerRows, theme: theme, caps: caps })) : null;
|
|
2327
2371
|
const overlayMaxRows = nativeBudget ? nativeBudget.overlayRows : viewportBudget;
|
|
2328
2372
|
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: {
|
|
2329
2373
|
provider: cfg.provider,
|
|
@@ -2384,6 +2428,19 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2384
2428
|
mode: native ? 'native' : 'fullscreen',
|
|
2385
2429
|
viewportRows: nativeBudget ? nativeBudget.liveRows : viewportBudget,
|
|
2386
2430
|
offset: scrolledLines,
|
|
2431
|
+
// `null` outside native mode, and `/perf` prints `n/a` for it: a band row of
|
|
2432
|
+
// zeroes in full-screen mode would read like a panel that failed to show.
|
|
2433
|
+
band: nativeBudget
|
|
2434
|
+
? {
|
|
2435
|
+
...nativeBudget.band,
|
|
2436
|
+
contentCols: nativeBudget.contentCols,
|
|
2437
|
+
// The tail window, joined here because it belongs to the VIEW pipeline rather
|
|
2438
|
+
// than to the band: `window=<start>/<n>` is what makes the visible-vs-printed
|
|
2439
|
+
// gap observable (known limitation 5).
|
|
2440
|
+
windowStart: scrollbackView.windowStart,
|
|
2441
|
+
windowEntries: scrollbackView.tailEntries.length,
|
|
2442
|
+
}
|
|
2443
|
+
: null,
|
|
2387
2444
|
// A getter: the bridge's `frameReady` and geometry change outside React renders.
|
|
2388
2445
|
get scrollbar() {
|
|
2389
2446
|
return readScrollbarPerf({
|
|
@@ -2434,6 +2491,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2434
2491
|
columns: viewCols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
|
|
2435
2492
|
context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
|
|
2436
2493
|
elapsedMs, tokPerSec, speedKnown, activeTool: state.activeTool, runOutcome: state.runOutcome,
|
|
2494
|
+
// The bounded-wait label, when `job_wait` is what is holding the run. Absent
|
|
2495
|
+
// for every other tool, which keeps every existing plan byte-identical.
|
|
2496
|
+
...(wait ? { wait } : {}),
|
|
2437
2497
|
waitingForConfirmation: !!humanRequest || !!confirmState,
|
|
2438
2498
|
stopping: interruptGesture.current.phase === 'stopping',
|
|
2439
2499
|
interruptPhase: interruptGesture.current.phase,
|
|
@@ -2456,10 +2516,25 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2456
2516
|
updateAvailable: !!updateSnapshot && shouldRenderUpdateLine(updateSnapshot) } });
|
|
2457
2517
|
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' }) }));
|
|
2458
2518
|
const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2459
|
-
|
|
2519
|
+
// --- Native panel band (release-gate-and-native-panels §6.3/§6.4) -------------
|
|
2520
|
+
//
|
|
2521
|
+
// SEPARATE NODES, NOT THE TWO ABOVE (RV-P1-7). Those carry `cols={contentCols}` for
|
|
2522
|
+
// the full-screen layout, which has to stay byte-identical (I-3); reusing them here
|
|
2523
|
+
// would silently narrow it. Every number below comes from `budget.band`, and the
|
|
2524
|
+
// number that decides whether a panel mounts IS the number it lays out against - a
|
|
2525
|
+
// panel that mounts on one value and lays out against another renders nothing and
|
|
2526
|
+
// reports nothing (TodoPanel's own I-10 / P1-3 note).
|
|
2527
|
+
const nativeBand = nativeBudget ? nativeBudget.band : null;
|
|
2528
|
+
const nativeRail = state.todos && nativeBand && nativeBand.railRows >= TODO_LIMITS.panelMinRows
|
|
2529
|
+
? (_jsx(TodoPanel, { snapshot: state.todos, width: nativeBand.railCols, rows: nativeBand.railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2530
|
+
// `cols` is the band's left column, not `contentCols`: `TeamPanel.activityBudget` is
|
|
2531
|
+
// continuous in `cols`, so a roster laid out against a column it is not in is exactly
|
|
2532
|
+
// the bug that pushes the elapsed column off an 80-column terminal.
|
|
2533
|
+
const nativeTeam = state.team && nativeBand ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: nativeBand.previewCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2534
|
+
const rail = fullscreenRailVisible && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2460
2535
|
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 })] }));
|
|
2461
2536
|
const detailsNode = _jsx(BottomStatusRow, { plan: details, columns: viewCols, theme: theme });
|
|
2462
|
-
const statusNode = (_jsx(StatusBar, { columns: viewCols, speedKnown: speedKnown, ...feedbackProjection, interruptPhase: interruptGesture.current.phase, pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, runOutcome: state.runOutcome, reducedMotion: reducedMotion, ...(sessionId ? { sessionId } : {}), waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
|
|
2537
|
+
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'
|
|
2463
2538
|
? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2464
2539
|
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2465
2540
|
// projection of "many sources of truth" into the props layer, and a
|
|
@@ -2475,7 +2550,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2475
2550
|
total: state.team.runs.length,
|
|
2476
2551
|
},
|
|
2477
2552
|
}
|
|
2478
|
-
: {}), ...(state.todos && !
|
|
2553
|
+
: {}), ...(state.todos && !(native
|
|
2554
|
+
? (nativeBudget?.band.railRows ?? 0) > 0
|
|
2555
|
+
: fullscreenRailVisible)
|
|
2479
2556
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
2480
2557
|
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
|
|
2481
2558
|
// Manual compaction starts before its next snapshot is published.
|
|
@@ -2499,7 +2576,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2499
2576
|
}
|
|
2500
2577
|
: {}) }));
|
|
2501
2578
|
if (nativeBudget) {
|
|
2502
|
-
|
|
2579
|
+
// THE OPENER IS MOUNTED ON THE SHELL'S TOP-ALIGNED SLOT (D4 / RV-P0-1): it is sized
|
|
2580
|
+
// from the band's pool above, and bottom-aligning it would reproduce the reported
|
|
2581
|
+
// "blank upper half" INSIDE the region, where the canvas `gap` assertion cannot see it.
|
|
2582
|
+
return (_jsx(ScrollbackShell, { budget: nativeBudget, header: header, overlay: overlayNode, liveTailLines: scrollbackView.liveTailLines, ...(opener ? { opener } : {}),
|
|
2583
|
+
// Real components for the tail window (I-7); the plain rows above stay as the
|
|
2584
|
+
// fallback for a frame with no preview rows. The WHOLE list is passed: the window
|
|
2585
|
+
// belongs to `tail-window.ts`, and `prev` must come from the same list.
|
|
2586
|
+
tail: nativeBudget.band.previewRows > 0
|
|
2587
|
+
? (_jsx(LiveTail, { entries: state.entries, 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 }))
|
|
2588
|
+
: 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 }));
|
|
2503
2589
|
}
|
|
2504
2590
|
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 }));
|
|
2505
2591
|
}
|
|
@@ -1,28 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ScrollbackShell — the
|
|
3
|
-
* (tui-native-terminal-scroll §4.6).
|
|
2
|
+
* ScrollbackShell — the LIVE REGION of native-scroll mode
|
|
3
|
+
* (tui-native-terminal-scroll §4.6, tui-full-height-live-region D3/D4).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* terminal
|
|
5
|
+
* THE REGION IS THE SCREEN. The root box is `budget.liveRows` tall, and that number is
|
|
6
|
+
* `rows - 1` - the whole area above the writer's parked row. It used to be only as tall
|
|
7
|
+
* as the panel band asked for, and every row it did not ask for became the canvas
|
|
8
|
+
* `gap`: blank rows ABOVE the content, which is what a user reports as "the app starts
|
|
9
|
+
* halfway down the terminal" and as "a big blank between the result and the running
|
|
10
|
+
* card". `H = rows - 1` is also the ONLY height that both fills the screen and keeps Ink
|
|
11
|
+
* off its `clearTerminal` branch (that branch fires at `outputHeight >= rows` and
|
|
12
|
+
* contains `ESC[3J`, which erases the user's scrollback).
|
|
10
13
|
*
|
|
11
|
-
* Top to bottom: [overlay |
|
|
14
|
+
* Top to bottom: [header][overlay | panel band][composer][status][details].
|
|
15
|
+
*
|
|
16
|
+
* THE PANEL BAND IS THE PART NATIVE MODE WAS MISSING
|
|
17
|
+
* (release-gate-and-native-panels §6.1). A fixed frame can put a rail beside the
|
|
18
|
+
* transcript; without one the rail and the roster simply had nowhere to go, so the
|
|
19
|
+
* default mode showed neither. The band is a row pool inside the live region:
|
|
20
|
+
*
|
|
21
|
+
* +---------------------------------------------+-----------------+
|
|
22
|
+
* | team roster (height = band.teamRows) | TODO rail |
|
|
23
|
+
* +---------------------------------------------+ (height = |
|
|
24
|
+
* | tail window (height = band.previewRows) | band.railRows) |
|
|
25
|
+
* +---------------------------------------------+-----------------+
|
|
26
|
+
*
|
|
27
|
+
* Every box in it has an explicit `height` and `width` plus `overflow="hidden"`, and
|
|
28
|
+
* that is a correctness requirement rather than tidiness: a box that grows with its
|
|
29
|
+
* content would make the region taller than `budget.liveRows`, and a frame as tall as
|
|
30
|
+
* the terminal makes Ink erase the user's scrollback.
|
|
31
|
+
*
|
|
32
|
+
* TWO SLOTS ARE NOT BOTTOM-ALIGNED, AND ONLY TWO (D3 / D4 / I-F7):
|
|
33
|
+
* - `header` is the fixed 1-row slot at the very top. It is NOT printed into the
|
|
34
|
+
* scrollback any more: at `H = rows - 1` any printed row is pushed off the screen by
|
|
35
|
+
* the next frame, so "print the header" would mean "the header is never visible".
|
|
36
|
+
* - `opener` is the empty-session card, and it is the one thing inside the band that is
|
|
37
|
+
* TOP-aligned: a 12-row card bottom-aligned in a 24-row column would leave 12 blank
|
|
38
|
+
* rows in the upper half of the screen - the original defect, still inside the
|
|
39
|
+
* region, where the `gap` assertion could not see it. `opener` and `tail` are
|
|
40
|
+
* mutually exclusive by construction (`App` keys them off `entries.length === 0`),
|
|
41
|
+
* and they share one box because two boxes would be one more height to get wrong.
|
|
42
|
+
*
|
|
43
|
+
* `liveTailLines` STAYS. It is the plain-text fallback for the frames where the real
|
|
44
|
+
* tail is not mounted (and the floor of the measurement path); `tail` wins when both
|
|
45
|
+
* are given, so the two can never be drawn on top of each other.
|
|
12
46
|
*/
|
|
13
47
|
import React from 'react';
|
|
14
48
|
import type { Theme } from './theme.js';
|
|
15
49
|
import type { ScrollbackBudget } from './scrollback/budget.js';
|
|
16
50
|
export interface ScrollbackShellProps {
|
|
17
51
|
budget: ScrollbackBudget;
|
|
18
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* REQUIRED: the one-row slot at the top of the region, `budget.headerRows` tall.
|
|
54
|
+
* Required rather than optional for the reason `budget` and `band` are - a frame
|
|
55
|
+
* that forgot it would render a header-less screen with nothing to catch it.
|
|
56
|
+
*/
|
|
57
|
+
header: React.ReactNode;
|
|
58
|
+
/** Shown instead of the band when an overlay is open. */
|
|
19
59
|
overlay: React.ReactNode | null;
|
|
20
|
-
/** Plain preview rows
|
|
60
|
+
/** Plain preview rows: the fallback when `tail` is absent (and what `/perf` counts). */
|
|
21
61
|
liveTailLines: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* The empty-session card. Mutually exclusive with `tail`, TOP-aligned, and the only
|
|
64
|
+
* thing in this shell that is not bottom-aligned or a fixed slot (RV-P0-1).
|
|
65
|
+
*/
|
|
66
|
+
opener?: React.ReactNode;
|
|
67
|
+
/** Real components for the tail window (`LiveTail`). */
|
|
68
|
+
tail?: React.ReactNode;
|
|
69
|
+
/** The subagent roster, drawn at the top of the band's left column. */
|
|
70
|
+
team?: React.ReactNode;
|
|
71
|
+
/** The TODO rail, drawn as the band's right-hand column. */
|
|
72
|
+
rail?: React.ReactNode;
|
|
22
73
|
composer: React.ReactNode;
|
|
23
74
|
status: React.ReactNode;
|
|
24
75
|
details?: React.ReactNode;
|
|
25
76
|
placeholder: React.ReactNode;
|
|
26
77
|
theme: Theme;
|
|
27
78
|
}
|
|
28
|
-
export declare function ScrollbackShell({ budget, overlay, liveTailLines, composer, status, details, placeholder, theme, }: ScrollbackShellProps): React.ReactElement;
|
|
79
|
+
export declare function ScrollbackShell({ budget, header, overlay, liveTailLines, opener, tail, team, rail, composer, status, details, placeholder, theme, }: ScrollbackShellProps): React.ReactElement;
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
export function ScrollbackShell({ budget, overlay, liveTailLines, composer, status, details, placeholder, theme, }) {
|
|
3
|
+
export function ScrollbackShell({ budget, header, overlay, liveTailLines, opener, tail, team, rail, composer, status, details, placeholder, theme, }) {
|
|
4
4
|
const width = budget.contentCols;
|
|
5
5
|
if (budget.inactive) {
|
|
6
6
|
return (_jsx(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: budget.liveRows > 0 ? placeholder : null }));
|
|
7
7
|
}
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const band = budget.band;
|
|
9
|
+
// `opener ?? tail ?? lines` is ONE slot, not three: whichever is mounted decides the
|
|
10
|
+
// alignment of the box, so the two can never both be drawn and fight for the height.
|
|
11
|
+
const leftColumn = opener ?? tail;
|
|
12
|
+
const tailLines = liveTailLines.slice(-budget.liveTailRows);
|
|
13
|
+
// The overlay and the band are alternatives, not siblings: the band reports
|
|
14
|
+
// `bandRows = remaining` for an overlay frame, so drawing both would double the
|
|
15
|
+
// region's height and take the frame to the terminal height (the `ESC[3J` case).
|
|
16
|
+
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: opener ? 'flex-start' : 'flex-end', overflow: "hidden", children: leftColumn ?? 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;
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: [budget.headerRows > 0 && (_jsx(Box, { height: budget.headerRows, flexShrink: 0, overflow: "hidden", children: header })), 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 }))] }));
|
|
10
18
|
}
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -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;
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -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,
|
package/dist/ui/Transcript.d.ts
CHANGED
|
@@ -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. */
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -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;
|
|
@@ -81,8 +81,19 @@ function statusClause(props, dot) {
|
|
|
81
81
|
const took = readyAt === undefined ? '' : `${dot}${formatDuration(readyAt - startedAt)}`;
|
|
82
82
|
return `ready${took}`;
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// A LIVE JOB SHOWS ITS ELAPSED TIME (D-8), because a task's whole story is
|
|
85
|
+
// "how long has this been going". The number comes from the transcript's 1 Hz
|
|
86
|
+
// `nowSec`; without it the card falls back to the spawn time and reads `0ms`,
|
|
87
|
+
// which is honest about never having been ticked rather than wrong about it.
|
|
88
|
+
if (status === 'running') {
|
|
89
|
+
// NO CLOCK, NO FIGURE. Falling back to `startedAt` would print `0ms` on every
|
|
90
|
+
// settled frame, which reads as "it just started" - a wrong number rather than
|
|
91
|
+
// an absent one.
|
|
92
|
+
if (props.nowSec === undefined)
|
|
93
|
+
return 'running';
|
|
94
|
+
const elapsed = Math.max(0, props.nowSec * 1000 - startedAt);
|
|
95
|
+
return `running${dot}${formatDuration(elapsed)}`;
|
|
96
|
+
}
|
|
86
97
|
if (status === 'starting')
|
|
87
98
|
return 'starting';
|
|
88
99
|
const lived = endedAt === undefined ? '' : `${dot}${formatDuration(endedAt - startedAt)}`;
|
|
@@ -92,20 +103,44 @@ function statusClause(props, dot) {
|
|
|
92
103
|
return `failed${lived}`;
|
|
93
104
|
return `exited${exitCode === null ? '' : ` code ${exitCode}`}${lived}`;
|
|
94
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The progress row, or `undefined` when there is nothing to draw.
|
|
108
|
+
*
|
|
109
|
+
* STALLED OUTRANKS PROGRESS: "no output for 6m" is the more urgent of the two
|
|
110
|
+
* facts, and drawing both would cost a second row the height estimator does not
|
|
111
|
+
* know about. The `no output` figure needs `nowSec`; without it the badge is
|
|
112
|
+
* omitted rather than printed with a wrong number.
|
|
113
|
+
*/
|
|
114
|
+
function progressRow(props) {
|
|
115
|
+
if (props.stalled === true) {
|
|
116
|
+
if (props.nowSec === undefined || props.lastOutputAt === undefined) {
|
|
117
|
+
return { text: 'no output', stalled: true };
|
|
118
|
+
}
|
|
119
|
+
const quiet = Math.max(0, props.nowSec * 1000 - props.lastOutputAt);
|
|
120
|
+
return { text: `no output ${formatDuration(quiet)}`, stalled: true };
|
|
121
|
+
}
|
|
122
|
+
if (props.progress)
|
|
123
|
+
return { text: props.progress, stalled: false };
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
95
126
|
function ServiceCardImpl(props) {
|
|
96
127
|
const { serviceId, command, url, rows, expanded, terminal, theme, caps } = props;
|
|
97
128
|
const glyphs = pickGlyphs(caps);
|
|
98
129
|
const color = serviceCardColor(props, theme);
|
|
99
130
|
const dot = ` ${glyphs.midDot} `;
|
|
100
131
|
const clause = statusClause(props, dot);
|
|
132
|
+
// THE NOUN, NOT THE KIND: `task t1` and `service s1` are the same component
|
|
133
|
+
// drawing two kinds of job, and every id keeps its existing prefix (I-2).
|
|
134
|
+
const noun = props.jobKind === 'task' ? 'task' : 'service';
|
|
101
135
|
// THE TERMINAL RECORD IS ONE ROW, ALWAYS. It is a SECOND event at a SECOND
|
|
102
136
|
// time — the only thing `<Static>` permits once the live card is printed — so
|
|
103
137
|
// it names what happened and nothing else (D-11).
|
|
104
138
|
if (terminal) {
|
|
105
|
-
return (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: color, wrap: "truncate", children: ["
|
|
139
|
+
return (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: color, wrap: "truncate", children: [noun, " ", serviceId, " ", clause, props.killIncomplete ? ' (may have left a detached child)' : ''] }) }));
|
|
106
140
|
}
|
|
107
141
|
const shown = rows.slice(-(expanded ? EXPANDED_TAIL_ROWS : COLLAPSED_TAIL_ROWS));
|
|
108
|
-
|
|
142
|
+
const progress = progressRow(props);
|
|
143
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: theme.accent, bold: true, children: [noun, " ", serviceId] }), _jsxs(Text, { color: theme.muted, wrap: "truncate", children: [' ', command] }), _jsxs(Text, { color: color, wrap: "truncate", children: [dot, clause] })] }), progress !== undefined && (_jsx(Box, { flexDirection: "row", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: progress.stalled ? theme.noticeWarn : theme.muted, wrap: "truncate", children: progress.stalled ? progress.text : `[progress] ${progress.text}` }) })), url !== undefined && (_jsxs(Box, { flexDirection: "row", flexShrink: 0, paddingLeft: 1, children: [_jsx(Text, { color: theme.primary, children: url }), _jsxs(Text, { color: theme.muted, children: [dot, "ctrl+c stop", dot, "ctrl+o log"] })] })), shown.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: shown.map((line, index) => (_jsx(Text, { color: theme.muted, wrap: "truncate", children: line }, index))) }))] }));
|
|
109
144
|
}
|
|
110
145
|
/**
|
|
111
146
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
@@ -15,6 +15,18 @@ export interface StatusLayoutInput {
|
|
|
15
15
|
name: string;
|
|
16
16
|
toolCallId: string;
|
|
17
17
|
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* A bounded wait in flight (long-task-jobs §4.9).
|
|
20
|
+
*
|
|
21
|
+
* PRESENT ONLY WHILE `job_wait` IS THE ACTIVE TOOL, and it REPLACES the
|
|
22
|
+
* `Running job_wait` label rather than joining it: "Waiting on t3 (up to 30s)"
|
|
23
|
+
* is the state the user asked about (what, and how long at most), while the
|
|
24
|
+
* tool name is an implementation detail of how the runtime is waiting.
|
|
25
|
+
*/
|
|
26
|
+
wait?: {
|
|
27
|
+
jobId: string;
|
|
28
|
+
upToSeconds: number;
|
|
29
|
+
};
|
|
18
30
|
runOutcome?: 'none' | 'ended' | 'interrupted' | 'failed';
|
|
19
31
|
spinner?: string;
|
|
20
32
|
waitingForConfirmation?: boolean;
|
|
@@ -77,7 +89,7 @@ export interface StatusLinePlan {
|
|
|
77
89
|
export declare function cleanStatusText(text: string): string;
|
|
78
90
|
export declare function statusField(id: string, text: string, tone?: StatusField['tone']): StatusField;
|
|
79
91
|
export declare function statusLinePlan(fields: readonly StatusField[], separator?: string): StatusLinePlan;
|
|
80
|
-
export declare function phaseLabels(input: Pick<StatusLayoutInput, 'phase' | 'activeTool' | 'runOutcome' | 'waitingForConfirmation' | 'stopping' | 'compacting' | 'retryActive'>): readonly [string, string];
|
|
92
|
+
export declare function phaseLabels(input: Pick<StatusLayoutInput, 'phase' | 'activeTool' | 'runOutcome' | 'waitingForConfirmation' | 'stopping' | 'compacting' | 'retryActive' | 'wait'>): readonly [string, string];
|
|
81
93
|
export declare function validContext(context: ContextUsageSnapshot): boolean;
|
|
82
94
|
export declare function shortDuration(ms: number): string;
|
|
83
95
|
/** Every field survives each width rung; only the redraw carrier is outside this plan. */
|
|
@@ -25,6 +25,16 @@ export function phaseLabels(input) {
|
|
|
25
25
|
return copy.compacting;
|
|
26
26
|
if (input.retryActive)
|
|
27
27
|
return copy.retrying;
|
|
28
|
+
// A WAIT OUTRANKS THE TOOL NAME (long-task-jobs AC-28): `Running job_wait`
|
|
29
|
+
// answers "which tool", `Waiting on t3 (up to 30s)` answers "what am I waiting
|
|
30
|
+
// for and how long may it take", which is the question a user staring at a
|
|
31
|
+
// quiet terminal actually has. Both forms carry it - the short rung is
|
|
32
|
+
// truncated by width, not reworded.
|
|
33
|
+
if (input.wait) {
|
|
34
|
+
const label = `Waiting on ${cleanStatusText(input.wait.jobId)} ` +
|
|
35
|
+
`(up to ${Math.max(0, Math.round(input.wait.upToSeconds))}s)`;
|
|
36
|
+
return [label, label];
|
|
37
|
+
}
|
|
28
38
|
if (input.activeTool)
|
|
29
39
|
return [copy.tool[0], copy.tool[1] + ' ' + cleanStatusText(input.activeTool.name)];
|
|
30
40
|
if (input.phase === 'idle' && input.runOutcome && input.runOutcome !== 'none')
|
|
@@ -218,6 +218,13 @@ const TOOL_LIVE_TAIL_ROWS = 8;
|
|
|
218
218
|
const SERVICE_COLLAPSED_TAIL_ROWS = 2;
|
|
219
219
|
/** Tail rows an EXPANDED `ServiceCard` draws; the snapshot cap (8). */
|
|
220
220
|
const SERVICE_EXPANDED_TAIL_ROWS = 8;
|
|
221
|
+
/**
|
|
222
|
+
* The progress row the live `ServiceCard` draws when `progress` or `stalled` is
|
|
223
|
+
* set (long-task-jobs §4.9), duplicated as the number `1` for the same reason as
|
|
224
|
+
* the two constants above: `virtual-window.ts` counts lines, `ServiceCard.tsx`
|
|
225
|
+
* draws them, and the pairing is kept by a test (AC-27) rather than by an import.
|
|
226
|
+
*/
|
|
227
|
+
const SERVICE_PROGRESS_ROWS = 1;
|
|
221
228
|
/**
|
|
222
229
|
* Pure UPPER-BOUND estimate of an entry's height, used until a real measurement
|
|
223
230
|
* exists.
|
|
@@ -358,7 +365,13 @@ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisibl
|
|
|
358
365
|
const tail = expanded
|
|
359
366
|
? Math.min(entry.rows.length, SERVICE_EXPANDED_TAIL_ROWS)
|
|
360
367
|
: Math.min(entry.rows.length, SERVICE_COLLAPSED_TAIL_ROWS);
|
|
361
|
-
|
|
368
|
+
// THE PROGRESS ROW IS ONE MORE LINE (long-task-jobs AC-27), and it is only
|
|
369
|
+
// ever one because `ServiceCard` draws it with `wrap="truncate"` and the
|
|
370
|
+
// supervisor clips it to `PROC_LIMITS.waitProgressChars`. A `stalled` badge
|
|
371
|
+
// REPLACES it rather than adding to it - the two can never both be drawn,
|
|
372
|
+
// so this stays a single term.
|
|
373
|
+
const progress = entry.progress || entry.stalled ? SERVICE_PROGRESS_ROWS : 0;
|
|
374
|
+
return separation + 1 + progress + url + tail;
|
|
362
375
|
}
|
|
363
376
|
default:
|
|
364
377
|
return separation + 1;
|
|
@@ -17,6 +17,18 @@ export interface RunActivity {
|
|
|
17
17
|
runningTool?: string;
|
|
18
18
|
/** A context compaction is in flight; outranks the tool name and the phrase. */
|
|
19
19
|
compacting?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* A BOUNDED WAIT IS IN FLIGHT (long-task-jobs §4.9).
|
|
22
|
+
*
|
|
23
|
+
* `Running job_wait` is a TOOL NAME; `Waiting on t3 (up to 30s)` is a STATE, and
|
|
24
|
+
* the state is what answers the user's two questions - what is it waiting for,
|
|
25
|
+
* and how long may that take. No countdown: a ticking number would need its own
|
|
26
|
+
* re-render, and the sentence is already complete.
|
|
27
|
+
*/
|
|
28
|
+
wait?: {
|
|
29
|
+
jobId: string;
|
|
30
|
+
upToSeconds: number;
|
|
31
|
+
};
|
|
20
32
|
}
|
|
21
33
|
export interface RunRowPlanInput {
|
|
22
34
|
/** Width of the row's container (the composer's content columns). */
|