@aragon-agent/cli 0.6.33 → 0.6.35
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 +2305 -2242
- package/README.md +119 -18
- package/dist/agent/controller.d.ts +29 -0
- package/dist/agent/controller.js +42 -1
- package/dist/agent/kit-apply.d.ts +56 -0
- package/dist/agent/kit-apply.js +209 -0
- package/dist/agent/model-profile-settings.d.ts +28 -0
- package/dist/agent/model-profile-settings.js +142 -29
- package/dist/agent/reducer.d.ts +1 -1
- package/dist/commands/builtins.js +4 -0
- package/dist/commands/kit-command.d.ts +20 -0
- package/dist/commands/kit-command.js +131 -0
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +1 -1
- package/dist/config/kit-resolution.d.ts +93 -0
- package/dist/config/kit-resolution.js +226 -0
- package/dist/config/model-kits.d.ts +224 -0
- package/dist/config/model-kits.js +302 -0
- package/dist/config/model-profile-store.d.ts +25 -0
- package/dist/config/model-profiles.d.ts +41 -3
- package/dist/config/model-profiles.js +46 -5
- package/dist/config/schema.d.ts +16 -0
- package/dist/input/keymap.js +18 -4
- package/dist/ui/App.js +135 -15
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +8 -2
- package/dist/ui/PromptInput.js +1 -1
- package/dist/ui/ScrollbackShell.d.ts +38 -13
- package/dist/ui/ScrollbackShell.js +4 -3
- package/dist/ui/StatusBar.d.ts +13 -0
- package/dist/ui/StatusBar.js +10 -3
- package/dist/ui/TeamPanel.d.ts +12 -0
- package/dist/ui/TeamPanel.js +10 -31
- package/dist/ui/Transcript.js +22 -2
- package/dist/ui/entries/TeamCard.d.ts +18 -2
- package/dist/ui/entries/TeamCard.js +47 -13
- package/dist/ui/layout/header-hints.d.ts +11 -1
- package/dist/ui/layout/header-hints.js +35 -3
- package/dist/ui/layout/kit-chip.d.ts +50 -0
- package/dist/ui/layout/kit-chip.js +81 -0
- package/dist/ui/layout/virtual-window.js +9 -0
- package/dist/ui/overlays/HelpOverlay.js +7 -0
- package/dist/ui/overlays/KitEditor.d.ts +23 -0
- package/dist/ui/overlays/KitEditor.js +101 -0
- package/dist/ui/overlays/KitManager.d.ts +23 -0
- package/dist/ui/overlays/KitManager.js +104 -0
- package/dist/ui/overlays/KitPicker.d.ts +49 -0
- package/dist/ui/overlays/KitPicker.js +58 -0
- package/dist/ui/overlays/ModelProfilePicker.js +16 -6
- package/dist/ui/overlays/SettingsScreen.js +139 -16
- package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
- package/dist/ui/overlays/kit-picker-state.js +136 -0
- package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
- package/dist/ui/overlays/kit-settings-state.js +267 -0
- package/dist/ui/overlays/model-profile-state.d.ts +26 -3
- package/dist/ui/overlays/model-profile-state.js +59 -6
- package/dist/ui/scrollback/LiveTail.d.ts +44 -10
- package/dist/ui/scrollback/LiveTail.js +20 -4
- package/dist/ui/scrollback/limits.d.ts +16 -0
- package/dist/ui/scrollback/limits.js +16 -0
- package/dist/ui/scrollback/live-preview.js +10 -3
- package/dist/ui/scrollback/overflow-print.d.ts +108 -0
- package/dist/ui/scrollback/overflow-print.js +120 -0
- package/dist/ui/scrollback/renderer.d.ts +11 -1
- package/dist/ui/scrollback/renderer.js +53 -25
- package/dist/ui/scrollback/tail-anchor.d.ts +68 -0
- package/dist/ui/scrollback/tail-anchor.js +77 -0
- package/dist/ui/scrollback/tail-window.d.ts +21 -8
- package/dist/ui/scrollback/tail-window.js +26 -10
- package/dist/ui/scrollback/use-scrollback-view.d.ts +38 -10
- package/dist/ui/scrollback/use-scrollback-view.js +372 -85
- package/dist/ui/team-status.d.ts +141 -0
- package/dist/ui/team-status.js +224 -0
- package/dist/ui/transcript-text.js +13 -4
- package/dist/ui/use-wheel-routing.js +2 -2
- package/package.json +2 -2
package/dist/ui/App.js
CHANGED
|
@@ -47,6 +47,7 @@ import { TODO_LIMITS } from '../todo/limits.js';
|
|
|
47
47
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
48
48
|
import { buildTodoRailLayout } from './layout/todo-layout.js';
|
|
49
49
|
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
50
|
+
import { isLiveTeamCard, rosterCounts } from './team-status.js';
|
|
50
51
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
51
52
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
52
53
|
import { useHeightStore } from './use-height-store.js';
|
|
@@ -62,6 +63,9 @@ import { publishSessionView } from './session-bridge.js';
|
|
|
62
63
|
import { OverlayFrame, overlayPageSize } from './layout/OverlayFrame.js';
|
|
63
64
|
import { helpRows } from './overlays/HelpOverlay.js';
|
|
64
65
|
import { ModelPicker } from './overlays/ModelPicker.js';
|
|
66
|
+
import { KitPicker } from './overlays/KitPicker.js';
|
|
67
|
+
import { INACTIVE_KIT } from '../config/kit-resolution.js';
|
|
68
|
+
import { kitApplyMessage, kitNoteMessages } from '../agent/kit-apply.js';
|
|
65
69
|
import { createModelProbe } from '../agent/model-probe.js';
|
|
66
70
|
import { SettingsScreen, compactionSettingsFrom, fastSettingsFrom, } from './overlays/SettingsScreen.js';
|
|
67
71
|
import { ConfirmDialog } from './overlays/ConfirmDialog.js';
|
|
@@ -1724,6 +1728,39 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1724
1728
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
1725
1729
|
toast('info', `Model set to ${provider}:${model}.`);
|
|
1726
1730
|
};
|
|
1731
|
+
/**
|
|
1732
|
+
* `ctrl+K` chose a row (agent-model-kits §4.7 / AC-25).
|
|
1733
|
+
*
|
|
1734
|
+
* `null` is `Current setup`, which is the RELEASE action, not a no-op: it runs
|
|
1735
|
+
* `clearSessionKit` so the live connections go back to the file's bindings.
|
|
1736
|
+
* Both paths close the overlay, and both report through the same toast funnel -
|
|
1737
|
+
* including every `KitNote`, because a kit that turns the fast tier on spends
|
|
1738
|
+
* money and must never do it silently (§4.2 / F-23).
|
|
1739
|
+
*/
|
|
1740
|
+
const handleKitChoose = (kitId) => {
|
|
1741
|
+
// NO KIT PORT, NO CRASH: a legacy host can still press the key, and the only
|
|
1742
|
+
// honest answer is a sentence rather than a stack trace.
|
|
1743
|
+
const port = controller;
|
|
1744
|
+
if (typeof port.applySessionKit !== 'function' || typeof port.clearSessionKit !== 'function') {
|
|
1745
|
+
dispatch({ type: 'setOverlay', overlay: null });
|
|
1746
|
+
toast('error', 'This build cannot switch kits.');
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
const result = kitId === null
|
|
1750
|
+
? controller.clearSessionKit() : controller.applySessionKit(kitId);
|
|
1751
|
+
dispatch({ type: 'setOverlay', overlay: null });
|
|
1752
|
+
// The config is already updated synchronously, so this reads the kit that is
|
|
1753
|
+
// in effect RIGHT NOW rather than the id the user asked for - if the apply
|
|
1754
|
+
// fell back to anything, the toast says so.
|
|
1755
|
+
const applied = controller.getActiveKit().name;
|
|
1756
|
+
const text = !result.ok ? kitApplyMessage(result)
|
|
1757
|
+
: kitId === null ? "Kit released; using the config file's settings."
|
|
1758
|
+
: `Kit ${applied} applied for this session.`;
|
|
1759
|
+
const notes = kitNoteMessages(result.notes);
|
|
1760
|
+
// A NOTE IS A WARNING, NOT A SUCCESS (F-23): the toast colour is the only
|
|
1761
|
+
// signal a user gets that applying a kit can start spending money.
|
|
1762
|
+
toast(result.ok ? (notes.length ? 'warn' : 'success') : 'error', [text, ...notes].join(' '));
|
|
1763
|
+
};
|
|
1727
1764
|
const closeConfirm = () => {
|
|
1728
1765
|
setConfirmState(null);
|
|
1729
1766
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
@@ -1967,6 +2004,24 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1967
2004
|
setStatusExpanded(value => !value);
|
|
1968
2005
|
return;
|
|
1969
2006
|
}
|
|
2007
|
+
// Ctrl+K: the Agent-kit picker (agent-model-kits D-15 / §4.7).
|
|
2008
|
+
//
|
|
2009
|
+
// NOT AN INPUT FRAME, unlike Ctrl+I below. Ctrl+I collides with the Tab byte,
|
|
2010
|
+
// so a stdin translator has to disambiguate it; 0x0B is unambiguous in every
|
|
2011
|
+
// encoding this app supports (utf8 byte, win32-input-mode's pass-through of
|
|
2012
|
+
// control chars, and CSI-u's `ctrlByte` fold), so the raw byte IS the key.
|
|
2013
|
+
//
|
|
2014
|
+
// `killToEnd` HAS MOVED TO Alt+K FIRST (keymap.ts): `PromptInput`'s
|
|
2015
|
+
// recognizer runs before its `key.ctrl` early-return, so leaving the old
|
|
2016
|
+
// binding would delete the draft in one listener while this one opened the
|
|
2017
|
+
// overlay. Two listeners both see the event; `return` only leaves one.
|
|
2018
|
+
const isCtrlK = (key.ctrl && (input === 'k' || input === 'K')) || input === '\x0b';
|
|
2019
|
+
if (isCtrlK) {
|
|
2020
|
+
if (tooSmall || stateRef.current.overlay)
|
|
2021
|
+
return;
|
|
2022
|
+
dispatch({ type: 'setOverlay', overlay: 'kit' });
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
1970
2025
|
// Ctrl+I arrives as INDEX_KEY_FRAME, never as key.ctrl + 'i': legacy
|
|
1971
2026
|
// terminals encode it as the Tab byte and the stdin filter's translators
|
|
1972
2027
|
// are the only layer that can tell them apart (see input/limits.ts).
|
|
@@ -2004,7 +2059,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2004
2059
|
const overlay = stateRef.current.overlay;
|
|
2005
2060
|
// Modes A only. `model` / `confirm` manage their own keys; registering an
|
|
2006
2061
|
// arrow handler for them would double every keypress (R-12).
|
|
2007
|
-
const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan'
|
|
2062
|
+
const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan'
|
|
2063
|
+
|| overlay === 'queue' || overlay === 'kit';
|
|
2008
2064
|
if (key.pageUp) {
|
|
2009
2065
|
if (controlled)
|
|
2010
2066
|
setOverlayScroll((n) => Math.max(0, n - overlayPage));
|
|
@@ -2102,12 +2158,36 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2102
2158
|
// `ctrl+o log` hint, the keystroke would silently expand AN OLDER TOOL
|
|
2103
2159
|
// CARD, and nothing would error. The terminal one-row record is skipped -
|
|
2104
2160
|
// it has nothing to reveal, and expanding it would consume the press.
|
|
2105
|
-
|
|
2161
|
+
//
|
|
2162
|
+
// TWO TIERS, AND THE ORDER IS THE WHOLE FIX (tui-final-reply-truncated-
|
|
2163
|
+
// unscrollable B1). The closed list above answers "which card is this key for",
|
|
2164
|
+
// and it must keep answering it EXACTLY as before: a settled answer is almost
|
|
2165
|
+
// always the newest entry, so adding `assistant` to THIS filter would take the
|
|
2166
|
+
// press away from the tool card the user just watched finish. Only when no card
|
|
2167
|
+
// is asking for the key does it fall through to the Agent's last answer - which
|
|
2168
|
+
// is the escape hatch `docs/plans/tui-full-height-live-region/spec.md` §13
|
|
2169
|
+
// (known limitation 2) already promised and never wired up.
|
|
2170
|
+
const entries = stateRef.current.entries;
|
|
2171
|
+
const card = [...entries]
|
|
2106
2172
|
.reverse()
|
|
2107
|
-
.find((e) =>
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2173
|
+
.find((e) =>
|
|
2174
|
+
// THE LIVE DISPATCH CARD IS NOT EXPANDABLE (tui-subagent-double-roster
|
|
2175
|
+
// A-2'): it is suppressed on screen, so expanding it would print a frozen
|
|
2176
|
+
// copy of a card that is still being rewritten. Once it settles it becomes
|
|
2177
|
+
// an ordinary history card and this key reaches it again.
|
|
2178
|
+
!isLiveTeamCard(e) &&
|
|
2179
|
+
(e.kind === 'tool' ||
|
|
2180
|
+
e.kind === 'team' ||
|
|
2181
|
+
e.kind === 'compaction' ||
|
|
2182
|
+
(e.kind === 'service' && !e.terminal)));
|
|
2183
|
+
// NATIVE MODE ONLY. The fallback exists because native mode cannot scroll inside the
|
|
2184
|
+
// frame - the answer's head is reachable only if it is printed. Full-screen mode has a
|
|
2185
|
+
// viewport that already reaches it, and it has no `expanded` rendering for an answer,
|
|
2186
|
+
// so falling through there would consume the press and show nothing.
|
|
2187
|
+
const answer = card === undefined && native
|
|
2188
|
+
? [...entries].reverse().find((e) => e.kind === 'assistant' && !e.streaming)
|
|
2189
|
+
: undefined;
|
|
2190
|
+
const last = card ?? answer;
|
|
2111
2191
|
// Native mode cannot rewrite what is already printed, so it PRINTS an expanded
|
|
2112
2192
|
// copy as a new block instead of toggling the old one (spec 4.7).
|
|
2113
2193
|
if (last && native) {
|
|
@@ -2206,6 +2286,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2206
2286
|
const overlay = state.overlay;
|
|
2207
2287
|
const settingsDraft = useMemo(() => overlay === 'settings'
|
|
2208
2288
|
? controller.createModelSettingsDraft() : undefined, [controller, overlay]);
|
|
2289
|
+
// THE FILE'S BINDINGS, FOR THE KIT PICKER'S FIRST ROW (§4.6 / D-13). While a
|
|
2290
|
+
// session kit is held the live config mirrors the KIT (I-6), so `Current setup`
|
|
2291
|
+
// can only describe what choosing it restores by reading the file - one read per
|
|
2292
|
+
// opening, the same price `settingsDraft` one line up already pays.
|
|
2293
|
+
const kitDiskConfig = useMemo(() => overlay === 'kit'
|
|
2294
|
+
? controller.createModelSettingsDraft().baseline : undefined, [controller, overlay]);
|
|
2209
2295
|
// ONE PROBE PER APP (model-profile-connection-test 5 row 9): the port needs the
|
|
2210
2296
|
// controller's provider registry, which the overlay has no business owning.
|
|
2211
2297
|
// Built only while the settings overlay is open, so no other screen pays for it.
|
|
@@ -2219,6 +2305,22 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2219
2305
|
return createModelProbe({ registry: controller.getProviderRegistry() });
|
|
2220
2306
|
}, [controller, overlay]);
|
|
2221
2307
|
const hasKey = controller.hasApiKey();
|
|
2308
|
+
// THE ACTIVE KIT, DERIVED PER FRAME (D-5 / F-22): `controller.getActiveKit()` is
|
|
2309
|
+
// pure, so it cannot go stale the way a cached copy would after `ctrl+K`,
|
|
2310
|
+
// `/model` or a saved default. Absent when no kit is in effect, which is what
|
|
2311
|
+
// keeps the status row byte-identical for users who own none (I-9).
|
|
2312
|
+
//
|
|
2313
|
+
// A TEST DOUBLE OR AN EMBEDDED HOST MAY PREDATE THIS PORT: the same tolerance
|
|
2314
|
+
// the probe port gets one screen over ("absent means the rows read
|
|
2315
|
+
// unresolved") applies here - no kit, no crash.
|
|
2316
|
+
const activeKit = typeof controller.getActiveKit === 'function'
|
|
2317
|
+
? controller.getActiveKit() : INACTIVE_KIT;
|
|
2318
|
+
const kitChip = activeKit.source === 'none' ? undefined : {
|
|
2319
|
+
name: activeKit.name,
|
|
2320
|
+
modified: activeKit.modified,
|
|
2321
|
+
otherNames: (cfg.modelProfiles?.kits ?? [])
|
|
2322
|
+
.filter((kit) => kit.id !== activeKit.id).map((kit) => kit.name),
|
|
2323
|
+
};
|
|
2222
2324
|
const empty = state.entries.length === 0;
|
|
2223
2325
|
// RENDER SCOPE, so the chip and the composer hint agree with what the Ctrl+C
|
|
2224
2326
|
// handler will do on the very next keystroke.
|
|
@@ -2322,7 +2424,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2322
2424
|
// ALWAYS CALLED, INERT WHEN `enabled` IS FALSE: hooks cannot be conditional, and
|
|
2323
2425
|
// an inert hook is what keeps `scrollMode: 'app'` byte-identical (I-S9).
|
|
2324
2426
|
const viewCols = native ? Math.max(1, cols - 1) : cols;
|
|
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
|
|
2427
|
+
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,
|
|
2428
|
+
// ALWAYS TRUE (agent-model-kits §4.7 / RV-P1-2): the requirement is that
|
|
2429
|
+
// `ctrl+G` reveals `ctrl+K`, and the user with an empty library needs that
|
|
2430
|
+
// hint most - the picker itself carries the "create one in /settings" row.
|
|
2431
|
+
kitHint: true }));
|
|
2326
2432
|
// The header and the opener are SLOTS OF THE SHELL, not printed bytes (D3/D4): at
|
|
2327
2433
|
// `H = rows - 1` a printed row is pushed off the screen by the very next frame, so
|
|
2328
2434
|
// `printBanner()` was a way of making the banner invisible. Neither is passed here.
|
|
@@ -2339,6 +2445,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2339
2445
|
itemCount: state.todos?.items.length ?? 0,
|
|
2340
2446
|
panelEnabled: cfg.todo.panel,
|
|
2341
2447
|
teamRowsNatural: teamLayout.rowCount,
|
|
2448
|
+
// The top give-way measures a window entry's height, and an expanded tool card is a
|
|
2449
|
+
// different height from a collapsed one - a forgotten value would print rows the region
|
|
2450
|
+
// is not drawing.
|
|
2451
|
+
expandedToolIds: state.expandedToolIds,
|
|
2342
2452
|
});
|
|
2343
2453
|
scrollbackViewRef.current = scrollbackView;
|
|
2344
2454
|
// The two hooks `cli.tsx` calls from OUTSIDE React: the exit summary (every exit
|
|
@@ -2369,7 +2479,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2369
2479
|
const openerCols = nativeBudget ? nativeBudget.previewCols : contentCols;
|
|
2370
2480
|
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(openerRows, openerCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: openerRows, theme: theme, caps: caps })) : null;
|
|
2371
2481
|
const overlayMaxRows = nativeBudget ? nativeBudget.overlayRows : viewportBudget;
|
|
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 === '
|
|
2482
|
+
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 === 'kit' ? (_jsx(KitPicker, { isActive: !tooSmall, library: cfg.modelProfiles, config: cfg, ...(kitDiskConfig ? { diskConfig: kitDiskConfig } : {}), active: activeKit,
|
|
2483
|
+
// ONLY THE APPLY IS BLOCKED while work is in flight (§4.7): a list a
|
|
2484
|
+
// user cannot even OPEN reads as a broken key.
|
|
2485
|
+
busy: controller.isModelSettingsBusy(), maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onChoose: handleKitChoose, onClose: () => dispatch({ type: 'setOverlay', overlay: null }) })) : overlay === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
|
|
2373
2486
|
provider: cfg.provider,
|
|
2374
2487
|
model: cfg.model,
|
|
2375
2488
|
baseUrl: cfg.baseUrl ?? '',
|
|
@@ -2436,9 +2549,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2436
2549
|
contentCols: nativeBudget.contentCols,
|
|
2437
2550
|
// The tail window, joined here because it belongs to the VIEW pipeline rather
|
|
2438
2551
|
// than to the band: `window=<start>/<n>` is what makes the visible-vs-printed
|
|
2439
|
-
// gap observable (known limitation 5)
|
|
2552
|
+
// gap observable (known limitation 5), and `head=` counts the rows the top
|
|
2553
|
+
// give-way printed for it, so the mechanism is never invisible.
|
|
2440
2554
|
windowStart: scrollbackView.windowStart,
|
|
2441
2555
|
windowEntries: scrollbackView.tailEntries.length,
|
|
2556
|
+
headRows: scrollbackView.headRows,
|
|
2442
2557
|
}
|
|
2443
2558
|
: null,
|
|
2444
2559
|
// A getter: the bridge's `frameReady` and geometry change outside React renders.
|
|
@@ -2500,8 +2615,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2500
2615
|
compacting: compactionLive || !!state.compaction?.inFlight,
|
|
2501
2616
|
mode: state.agentMode, pendingMode: state.pendingAgentMode,
|
|
2502
2617
|
servicesActive: { live: liveServices }, ecoRung: governor.rung,
|
|
2503
|
-
|
|
2504
|
-
|
|
2618
|
+
// THE STATUS-DETAIL ROW COUNTS LIKE THE ROSTER AND THE CHIP (A-1): this was the
|
|
2619
|
+
// last inline phase filter, left behind when the StatusBar chip converged, and it
|
|
2620
|
+
// feeds the `Team n/m` on the second status row.
|
|
2621
|
+
teamActive: state.team
|
|
2622
|
+
? { running: rosterCounts(state.team.runs).running, total: state.team.runs.length }
|
|
2623
|
+
: undefined,
|
|
2505
2624
|
todoActive: state.todos ? { done: state.todos.doneCount, total: state.todos.total } : undefined,
|
|
2506
2625
|
fastActive: state.fast?.live ? { inFlight: state.fast.inFlight } : undefined,
|
|
2507
2626
|
...feedbackProjection,
|
|
@@ -2534,7 +2653,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2534
2653
|
const rail = fullscreenRailVisible && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2535
2654
|
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 })] }));
|
|
2536
2655
|
const detailsNode = _jsx(BottomStatusRow, { plan: details, columns: viewCols, theme: theme });
|
|
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'
|
|
2656
|
+
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 } : {}), ...(kitChip ? { kit: kitChip } : {}), waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
|
|
2538
2657
|
? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2539
2658
|
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2540
2659
|
// projection of "many sources of truth" into the props layer, and a
|
|
@@ -2544,9 +2663,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2544
2663
|
// `contextWindow` override reach the bar.
|
|
2545
2664
|
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
|
|
2546
2665
|
? {
|
|
2666
|
+
// `rosterCounts`, NOT a fourth inline phase filter (A-1): the chip and
|
|
2667
|
+
// the roster header above it must never disagree.
|
|
2547
2668
|
teamActive: {
|
|
2548
|
-
running: state.team.runs
|
|
2549
|
-
r.phase !== 'failed' && r.phase !== 'aborted').length,
|
|
2669
|
+
running: rosterCounts(state.team.runs).running,
|
|
2550
2670
|
total: state.team.runs.length,
|
|
2551
2671
|
},
|
|
2552
2672
|
}
|
|
@@ -2584,7 +2704,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2584
2704
|
// fallback for a frame with no preview rows. The WHOLE list is passed: the window
|
|
2585
2705
|
// belongs to `tail-window.ts`, and `prev` must come from the same list.
|
|
2586
2706
|
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 }))
|
|
2707
|
+
? (_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, measuredRows: scrollbackView.measuredTailRows }))
|
|
2588
2708
|
: 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 }));
|
|
2589
2709
|
}
|
|
2590
2710
|
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 }));
|
package/dist/ui/Header.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ interface HeaderProps {
|
|
|
15
15
|
statusExpanded?: boolean;
|
|
16
16
|
scrolledLines?: number;
|
|
17
17
|
overlayOpen?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the expanded status row teaches `ctrl+K` (agent-model-kits §4.7).
|
|
20
|
+
*
|
|
21
|
+
* DEFAULTS TO TRUE because that is the requirement - `ctrl+G` must reveal the
|
|
22
|
+
* kit key - and because a default of `false` would leave the one caller that
|
|
23
|
+
* forgets the prop silently teaching nothing.
|
|
24
|
+
*/
|
|
25
|
+
kitHint?: boolean;
|
|
18
26
|
}
|
|
19
|
-
export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns, statusExpanded, scrolledLines, overlayOpen }: HeaderProps): React.ReactElement;
|
|
27
|
+
export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns, statusExpanded, scrolledLines, overlayOpen, kitHint }: HeaderProps): React.ReactElement;
|
|
20
28
|
export {};
|
package/dist/ui/Header.js
CHANGED
|
@@ -3,13 +3,19 @@ import { basename } from 'node:path';
|
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { Logo } from './Logo.js';
|
|
5
5
|
import { planHeaderHints, HEADER_HINT_LIMITS } from './layout/header-hints.js';
|
|
6
|
-
export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, statusExpanded = false, scrolledLines = 0, overlayOpen = false }) {
|
|
6
|
+
export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, statusExpanded = false, scrolledLines = 0, overlayOpen = false, kitHint = true }) {
|
|
7
7
|
// The right-hand hints are planned, not assembled (tui-branch-terminal-shortcut
|
|
8
8
|
// 3.9 / D-11). `plan.text` is exactly `plan.cells` wide and already carries the
|
|
9
9
|
// scroll chip's own trailing spacing, so this component can no longer clip a
|
|
10
10
|
// hint by guessing one cell short - and the ladder that decides which hints
|
|
11
11
|
// survive a narrow terminal lives in a pure function with its own tests.
|
|
12
|
-
|
|
12
|
+
//
|
|
13
|
+
// `kitHint` is the `^K switch kit` clause (agent-model-kits §4.7): `App`
|
|
14
|
+
// passes `true` always, because the requirement is that `ctrl+G` reveals the
|
|
15
|
+
// key - and the user with no kits needs that hint most.
|
|
16
|
+
const plan = planHeaderHints({
|
|
17
|
+
columns, statusExpanded, overlayOpen, scrolledLines, kitHint,
|
|
18
|
+
});
|
|
13
19
|
// THE TWO HALVES ARE SEPARATED BY CELLS, NOT BY LUCK (defect 12). The left half
|
|
14
20
|
// is `wrap="truncate"`, which draws its ellipsis in the LAST cell it owns; with
|
|
15
21
|
// the right half starting in the very next cell a narrow terminal read
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -596,7 +596,7 @@ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, st
|
|
|
596
596
|
return;
|
|
597
597
|
}
|
|
598
598
|
if (key.ctrl)
|
|
599
|
-
return; // App owns Ctrl+C / L / T / O.
|
|
599
|
+
return; // App owns Ctrl+C / L / T / O, and now Ctrl+K (the kit picker).
|
|
600
600
|
if (key.return) {
|
|
601
601
|
if (key.meta || key.shift) {
|
|
602
602
|
insert('\n');
|
|
@@ -19,30 +19,51 @@
|
|
|
19
19
|
* default mode showed neither. The band is a row pool inside the live region:
|
|
20
20
|
*
|
|
21
21
|
* +---------------------------------------------+-----------------+
|
|
22
|
-
* |
|
|
22
|
+
* | tail window (height = band.previewRows) | TODO rail |
|
|
23
23
|
* +---------------------------------------------+ (height = |
|
|
24
|
-
* |
|
|
24
|
+
* | team roster (height = band.teamRows) | band.railRows) |
|
|
25
25
|
* +---------------------------------------------+-----------------+
|
|
26
26
|
*
|
|
27
|
+
* THE ROSTER IS THE LEFT COLUMN'S BOTTOM SLOT, not its top one
|
|
28
|
+
* (tui-subagent-double-roster A-3). `band.ts` allocates the same two heights either
|
|
29
|
+
* way - only the drawing order changed - so every row number, and every test that
|
|
30
|
+
* asserts one, is untouched. What moves is the reading: the roster is transient
|
|
31
|
+
* status about the run in progress, so it belongs against the composer (where the
|
|
32
|
+
* user's eyes already are, and where full-screen mode's viewport `footer` puts it),
|
|
33
|
+
* and the conversation's own tail stays above it. As a bonus the rows a short tail
|
|
34
|
+
* does not use now fall between the tail and the roster instead of between the tail
|
|
35
|
+
* and the input box, which is the "blank space above the composer" shape the
|
|
36
|
+
* plan-A-prime work removed one level up.
|
|
37
|
+
*
|
|
27
38
|
* Every box in it has an explicit `height` and `width` plus `overflow="hidden"`, and
|
|
28
39
|
* that is a correctness requirement rather than tidiness: a box that grows with its
|
|
29
40
|
* content would make the region taller than `budget.liveRows`, and a frame as tall as
|
|
30
41
|
* the terminal makes Ink erase the user's scrollback.
|
|
31
42
|
*
|
|
32
|
-
*
|
|
43
|
+
* THE BAND'S PREVIEW BOX IS TOP-ALIGNED; THE OTHER SLOTS ARE FIXED
|
|
44
|
+
* (D3 / D4 / I-F7; tui-first-reply-bottom-aligned, plan A-prime):
|
|
33
45
|
* - `header` is the fixed 1-row slot at the very top. It is NOT printed into the
|
|
34
46
|
* scrollback any more: at `H = rows - 1` any printed row is pushed off the screen by
|
|
35
47
|
* the next frame, so "print the header" would mean "the header is never visible".
|
|
36
|
-
* -
|
|
37
|
-
*
|
|
38
|
-
* rows in the upper half of the screen - the original
|
|
39
|
-
* region, where the `gap` assertion could not see it
|
|
40
|
-
*
|
|
41
|
-
*
|
|
48
|
+
* - the band's preview box is TOP-aligned unconditionally, which is why the alignment is
|
|
49
|
+
* no longer a function of `opener`. `opener` is a 12-row card in a 24-row column, so
|
|
50
|
+
* bottom-aligning it left 12 blank rows in the upper half of the screen - the original
|
|
51
|
+
* defect, still inside the region, where the `gap` assertion could not see it
|
|
52
|
+
* (RV-P0-1). `tail` had the same shape for a short conversation: four rows pressed
|
|
53
|
+
* against the composer with every unused row ABOVE them, which is the defect plan
|
|
54
|
+
* A-prime fixes. `tail` therefore decides its own alignment one level down
|
|
55
|
+
* (`scrollback/tail-anchor.ts`): top while its content fits, and the old `flex-end`
|
|
56
|
+
* only on the frames where the content overflows and the newest row must stay visible.
|
|
57
|
+
* `opener` and `tail` share this box because two boxes would be one more height to
|
|
58
|
+
* get wrong, and they are mutually exclusive by construction (`App` keys them off
|
|
59
|
+
* `entries.length === 0`).
|
|
42
60
|
*
|
|
43
61
|
* `liveTailLines` STAYS. It is the plain-text fallback for the frames where the real
|
|
44
62
|
* 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.
|
|
63
|
+
* are given, so the two can never be drawn on top of each other. It is sliced to
|
|
64
|
+
* `budget.liveTailRows` below, which is the same number as `band.previewRows`: that slice
|
|
65
|
+
* is what makes the unconditional `flex-start` safe here, because a fallback taller than
|
|
66
|
+
* the box would clip its NEWEST line - the one direction this box must never clip.
|
|
46
67
|
*/
|
|
47
68
|
import React from 'react';
|
|
48
69
|
import type { Theme } from './theme.js';
|
|
@@ -60,13 +81,17 @@ export interface ScrollbackShellProps {
|
|
|
60
81
|
/** Plain preview rows: the fallback when `tail` is absent (and what `/perf` counts). */
|
|
61
82
|
liveTailLines: readonly string[];
|
|
62
83
|
/**
|
|
63
|
-
* The empty-session card. Mutually exclusive with `tail`, TOP-aligned
|
|
64
|
-
*
|
|
84
|
+
* The empty-session card. Mutually exclusive with `tail`, TOP-aligned like the rest of
|
|
85
|
+
* the band's preview box (RV-P0-1).
|
|
65
86
|
*/
|
|
66
87
|
opener?: React.ReactNode;
|
|
67
88
|
/** Real components for the tail window (`LiveTail`). */
|
|
68
89
|
tail?: React.ReactNode;
|
|
69
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* The subagent roster: the BOTTOM slot of the band's left column, i.e. the row
|
|
92
|
+
* closest to the composer. It is the only live subagent surface during a
|
|
93
|
+
* dispatch - the transcript's `team` card is suppressed while it is active.
|
|
94
|
+
*/
|
|
70
95
|
team?: React.ReactNode;
|
|
71
96
|
/** The TODO rail, drawn as the band's right-hand column. */
|
|
72
97
|
rail?: React.ReactNode;
|
|
@@ -6,13 +6,14 @@ export function ScrollbackShell({ budget, header, overlay, liveTailLines, opener
|
|
|
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
8
|
const band = budget.band;
|
|
9
|
-
// `opener ?? tail ?? lines` is ONE slot, not three:
|
|
10
|
-
//
|
|
9
|
+
// `opener ?? tail ?? lines` is ONE slot, not three: they are mutually exclusive, so
|
|
10
|
+
// the two can never both be drawn and fight for the height. This box's alignment is
|
|
11
|
+
// fixed (`flex-start`); `tail` alone may fall back to `flex-end` when it overflows.
|
|
11
12
|
const leftColumn = opener ?? tail;
|
|
12
13
|
const tailLines = liveTailLines.slice(-budget.liveTailRows);
|
|
13
14
|
// The overlay and the band are alternatives, not siblings: the band reports
|
|
14
15
|
// `bandRows = remaining` for an overlay frame, so drawing both would double the
|
|
15
16
|
// 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.
|
|
17
|
+
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.previewRows > 0 && (_jsx(Box, { flexDirection: "column", height: band.previewRows, flexShrink: 0, justifyContent: "flex-start", overflow: "hidden", children: leftColumn ?? tailLines.map((line, index) => (_jsx(Text, { wrap: "truncate", color: theme.muted, children: line }, index))) })), band.teamRows > 0 && (_jsx(Box, { height: band.teamRows, flexShrink: 0, overflow: "hidden", children: team }))] }), band.railRows > 0 && (_jsx(Box, { height: band.railRows, width: band.railCols, flexShrink: 0, overflow: "hidden", children: rail }))] })) : null;
|
|
17
18
|
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 }))] }));
|
|
18
19
|
}
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -50,6 +50,19 @@ export interface StatusBarProps {
|
|
|
50
50
|
* existing test render exactly what they rendered before (I-6).
|
|
51
51
|
*/
|
|
52
52
|
sessionId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The agent kit in effect, or absent when there is none (agent-model-kits
|
|
55
|
+
* §4.8 / D-12).
|
|
56
|
+
*
|
|
57
|
+
* OPTIONAL AND ABSENT BY DEFAULT, for the same reason `sessionId` is: with no
|
|
58
|
+
* kit the plan is hidden, `kitCells` is 0, and this row's output is
|
|
59
|
+
* byte-identical to the pre-feature build (I-9).
|
|
60
|
+
*/
|
|
61
|
+
kit?: {
|
|
62
|
+
name: string;
|
|
63
|
+
modified: boolean;
|
|
64
|
+
otherNames: readonly string[];
|
|
65
|
+
} | null;
|
|
53
66
|
agentMode?: AgentMode;
|
|
54
67
|
pendingAgentMode?: AgentMode | null;
|
|
55
68
|
teamActive?: {
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -6,6 +6,7 @@ import { pickGlyphs } from './glyphs.js';
|
|
|
6
6
|
import { liveSpinner } from './ActivityLine.js';
|
|
7
7
|
import { planPrimaryStatusFields } from './layout/status-layout.js';
|
|
8
8
|
import { planSessionChip } from './layout/session-chip.js';
|
|
9
|
+
import { planKitChip } from './layout/kit-chip.js';
|
|
9
10
|
/** An invisible leading byte change forces Ink to repaint on Ctrl+L. */
|
|
10
11
|
const redrawChar = (nonce) => nonce % 2 ? String.fromCharCode(0x00a0) : ' ';
|
|
11
12
|
export function StatusBar(props) {
|
|
@@ -21,8 +22,14 @@ export function StatusBar(props) {
|
|
|
21
22
|
...(props.sessionId ? { sessionId: props.sessionId } : {}),
|
|
22
23
|
});
|
|
23
24
|
const chipCells = chip.visible ? chip.cells + chip.gap : 0;
|
|
25
|
+
// THE KIT CHIP IS PLANNED SECOND, OUT OF WHAT THE SESSION CHIP LEFT (D-12):
|
|
26
|
+
// the session id is never truncated, so it is not the thing that gives way.
|
|
27
|
+
// With no kit the plan is hidden and `kitCells` is 0, which keeps this row
|
|
28
|
+
// byte-identical to the build before the feature (I-9).
|
|
29
|
+
const kit = planKitChip({ columns: cols - chipCells, kit: props.kit });
|
|
30
|
+
const kitCells = kit.visible ? kit.cells + kit.gap : 0;
|
|
24
31
|
const plan = planPrimaryStatusFields({
|
|
25
|
-
reserveRight: chipCells,
|
|
32
|
+
reserveRight: chipCells + kitCells,
|
|
26
33
|
columns: cols, phase, pendingCount: props.pendingSteering?.length ?? 0,
|
|
27
34
|
context: props.context, usageTotal: props.usageTotal,
|
|
28
35
|
thinkingLevel: props.thinkingLevel, elapsedMs: props.elapsedMs, tokPerSec: props.tokPerSec,
|
|
@@ -39,7 +46,7 @@ export function StatusBar(props) {
|
|
|
39
46
|
const tones = { normal: props.theme.primary, muted: props.theme.muted,
|
|
40
47
|
warning: props.theme.noticeWarn, error: props.theme.noticeError,
|
|
41
48
|
accent: props.theme.accent };
|
|
42
|
-
return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), _jsx(Box, { width: Math.max(0, cols - 1 - chipCells), flexShrink: 0, flexDirection: "row", overflow: "hidden", children: plan.fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: props.theme.muted, children: plan.separator }) }), _jsx(Box, { width: item.cells, flexShrink: 0, children: _jsx(Text, { color: tones[item.tone], wrap: "truncate", children: item.id === 'phase' && active
|
|
49
|
+
return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), _jsx(Box, { width: Math.max(0, cols - 1 - chipCells - kitCells), flexShrink: 0, flexDirection: "row", overflow: "hidden", children: plan.fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: props.theme.muted, children: plan.separator }) }), _jsx(Box, { width: item.cells, flexShrink: 0, children: _jsx(Text, { color: tones[item.tone], wrap: "truncate", children: item.id === 'phase' && active
|
|
43
50
|
? _jsxs(_Fragment, { children: [spinner ?? glyph, item.text.slice(glyph.length)] })
|
|
44
|
-
: item.text }) })] }, item.id)) }), chip.visible && _jsx(Box, { width: chipCells, flexShrink: 0, justifyContent: "flex-end", overflow: "hidden", children: _jsx(Text, { color: props.theme.muted, wrap: "truncate", children: chip.text }) })] });
|
|
51
|
+
: item.text }) })] }, item.id)) }), kit.visible && _jsx(Box, { width: kitCells, flexShrink: 0, justifyContent: "flex-end", overflow: "hidden", children: _jsx(Text, { color: props.theme.muted, wrap: "truncate", children: kit.text }) }), chip.visible && _jsx(Box, { width: chipCells, flexShrink: 0, justifyContent: "flex-end", overflow: "hidden", children: _jsx(Text, { color: props.theme.muted, wrap: "truncate", children: chip.text }) })] });
|
|
45
52
|
}
|
package/dist/ui/TeamPanel.d.ts
CHANGED
|
@@ -15,6 +15,18 @@
|
|
|
15
15
|
* with the other transient rows — toast, composer, status bar — rather than
|
|
16
16
|
* inside the scrolling transcript, where it would be interleaved with history
|
|
17
17
|
* and scroll out of sight exactly when the user wants to watch it.
|
|
18
|
+
*
|
|
19
|
+
* IT IS THE ONLY LIVE SUBAGENT SURFACE (tui-subagent-double-roster A-2). The
|
|
20
|
+
* transcript's `team` card is suppressed while its dispatch is active
|
|
21
|
+
* (`team-status.ts::isLiveTeamCard`), so this roster is what the user watches
|
|
22
|
+
* during a dispatch and the card becomes the history afterwards. Native mode
|
|
23
|
+
* draws it as the BOTTOM slot of the band's left column (`ScrollbackShell`),
|
|
24
|
+
* i.e. directly above the composer; full-screen mode draws it in the viewport
|
|
25
|
+
* footer, which is already above the composer.
|
|
26
|
+
*
|
|
27
|
+
* EVERY PHASE -> GLYPH / COLOUR / COUNT DECISION COMES FROM `team-status.ts`.
|
|
28
|
+
* The local copies this file used to carry are gone on purpose: the transcript
|
|
29
|
+
* card kept a second copy and the two disagreed about what "running" looks like.
|
|
18
30
|
*/
|
|
19
31
|
import React from 'react';
|
|
20
32
|
import type { Theme } from './theme.js';
|
package/dist/ui/TeamPanel.js
CHANGED
|
@@ -2,26 +2,11 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
4
|
import { pickGlyphs } from './glyphs.js';
|
|
5
|
+
import { childElapsedMs, childStatus, isRunningStatus, rosterCounts, rosterHeadline, statusColor, statusMarkerCell, } from './team-status.js';
|
|
5
6
|
import { formatDuration } from '../agent/usage.js';
|
|
6
7
|
import { describeToolActivity, sanitizeActivity } from '../team/activity.js';
|
|
7
8
|
import { TEAM_LIMITS } from '../team/limits.js';
|
|
8
9
|
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
9
|
-
function isRunning(run) {
|
|
10
|
-
return run.phase === 'starting' || run.phase === 'thinking' || run.phase === 'tool' ||
|
|
11
|
-
run.phase === 'waiting';
|
|
12
|
-
}
|
|
13
|
-
function isSettled(run) {
|
|
14
|
-
return run.phase === 'done' || run.phase === 'failed' || run.phase === 'aborted';
|
|
15
|
-
}
|
|
16
|
-
function statusColor(run, theme) {
|
|
17
|
-
if (run.phase === 'failed' || run.phase === 'aborted')
|
|
18
|
-
return theme.toolError;
|
|
19
|
-
if (run.phase === 'done')
|
|
20
|
-
return theme.toolDone;
|
|
21
|
-
if (isRunning(run))
|
|
22
|
-
return theme.toolRunning;
|
|
23
|
-
return theme.muted;
|
|
24
|
-
}
|
|
25
10
|
/**
|
|
26
11
|
* How many characters the activity column may spend at this width (P0-1).
|
|
27
12
|
*
|
|
@@ -172,10 +157,12 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
|
|
|
172
157
|
return null;
|
|
173
158
|
const glyphs = pickGlyphs(caps);
|
|
174
159
|
const at = now ?? Date.now();
|
|
175
|
-
|
|
176
|
-
|
|
160
|
+
// ONE COUNTING AUTHORITY (A-1): `queued` used to fall between `isRunning` and
|
|
161
|
+
// `isSettled`, so a fresh dispatch read `0 running 0 done` above rows that each
|
|
162
|
+
// said `queued`.
|
|
163
|
+
const counts = rosterCounts(snapshot.runs);
|
|
177
164
|
const elapsed = formatDuration(Math.max(0, at - snapshot.startedAt));
|
|
178
|
-
const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [
|
|
165
|
+
const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [rosterHeadline(counts, glyphs, cols), " ", glyphs.midDot, " ", elapsed] })] }), snapshot.messageCount > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.teamMail, " ", snapshot.messageCount] }))] }));
|
|
179
166
|
// Short terminals or a long draft leave only the shared header projection,
|
|
180
167
|
// which still answers whether anything is running and for how long.
|
|
181
168
|
if (projection.collapsed) {
|
|
@@ -187,13 +174,14 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
|
|
|
187
174
|
// the requirement permits.
|
|
188
175
|
const { visible, hiddenTotal, hiddenRunning } = projection;
|
|
189
176
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [header, visible.map((run) => {
|
|
190
|
-
const
|
|
177
|
+
const status = childStatus(run);
|
|
178
|
+
const color = statusColor(status, theme);
|
|
191
179
|
// One spinner per RUNNING row, and the row cap is 5 precisely because
|
|
192
180
|
// five is the documented maximum number of simultaneous spinners. Both
|
|
193
181
|
// `reducedMotion` and a non-Unicode terminal fall back to the same
|
|
194
182
|
// static marker, exactly as `AssistantEntry` and `ToolCard` already do.
|
|
195
|
-
const animate =
|
|
196
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) :
|
|
183
|
+
const animate = isRunningStatus(status) && !reducedMotion && caps.unicode;
|
|
184
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : statusMarkerCell(status, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityWithBadge(run, cols, at)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(childElapsedMs(run, at)) : ''] })] }, run.label));
|
|
197
185
|
}), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), (() => {
|
|
198
186
|
// The panel's supervisor line (AC-5): one row, only when the
|
|
199
187
|
// supervisor has actually acted - the per-row badges above already
|
|
@@ -202,12 +190,3 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
|
|
|
202
190
|
return line ? (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', line] })) : null;
|
|
203
191
|
})(), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
|
|
204
192
|
}
|
|
205
|
-
function marker(run, glyphs) {
|
|
206
|
-
if (run.phase === 'done')
|
|
207
|
-
return glyphs.toolDone;
|
|
208
|
-
if (run.phase === 'failed' || run.phase === 'aborted')
|
|
209
|
-
return glyphs.toolError;
|
|
210
|
-
if (run.phase === 'queued')
|
|
211
|
-
return glyphs.toolPending;
|
|
212
|
-
return glyphs.spinnerStill;
|
|
213
|
-
}
|