@aragon-agent/cli 0.5.1 → 0.5.3
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 +117 -0
- package/README.md +155 -5
- package/dist/agent/controller.d.ts +51 -1
- package/dist/agent/controller.js +177 -2
- package/dist/agent/headless.d.ts +12 -0
- package/dist/agent/headless.js +41 -1
- package/dist/agent/reducer.d.ts +48 -0
- package/dist/agent/reducer.js +91 -2
- package/dist/agent/system-prompt.d.ts +17 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/cli.js +131 -6
- package/dist/commands/builtins.js +82 -0
- package/dist/config/app-paths.d.ts +17 -0
- package/dist/config/app-paths.js +21 -0
- package/dist/config/cli-commands.d.ts +26 -2
- package/dist/config/cli-commands.js +63 -4
- package/dist/config/env.d.ts +2 -2
- package/dist/config/env.js +36 -2
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/load.js +88 -5
- package/dist/config/migrate-state-out-of-config.d.ts +32 -0
- package/dist/config/migrate-state-out-of-config.js +85 -0
- package/dist/config/prompt-history.d.ts +86 -0
- package/dist/config/prompt-history.js +332 -0
- package/dist/config/schema.d.ts +125 -9
- package/dist/config/schema.js +90 -5
- package/dist/config/store.js +13 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -0
- package/dist/input/mouse-events.d.ts +61 -0
- package/dist/input/mouse-events.js +153 -0
- package/dist/input/stdin-mouse-filter.d.ts +42 -0
- package/dist/input/stdin-mouse-filter.js +153 -0
- package/dist/logging/install.d.ts +85 -0
- package/dist/logging/install.js +94 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.d.ts +16 -0
- package/dist/session/persist.js +19 -1
- package/dist/team/activity.d.ts +67 -0
- package/dist/team/activity.js +130 -0
- package/dist/team/bus.d.ts +190 -0
- package/dist/team/bus.js +366 -0
- package/dist/team/comm-tools.d.ts +61 -0
- package/dist/team/comm-tools.js +221 -0
- package/dist/team/human-queue.d.ts +55 -0
- package/dist/team/human-queue.js +72 -0
- package/dist/team/limits.d.ts +123 -0
- package/dist/team/limits.js +128 -0
- package/dist/team/normalize.d.ts +39 -0
- package/dist/team/normalize.js +106 -0
- package/dist/team/panel-rows.d.ts +48 -0
- package/dist/team/panel-rows.js +75 -0
- package/dist/team/prompt.d.ts +49 -0
- package/dist/team/prompt.js +87 -0
- package/dist/team/report.d.ts +58 -0
- package/dist/team/report.js +205 -0
- package/dist/team/retry.d.ts +48 -0
- package/dist/team/retry.js +56 -0
- package/dist/team/runtime.d.ts +122 -0
- package/dist/team/runtime.js +406 -0
- package/dist/team/subagent.d.ts +90 -0
- package/dist/team/subagent.js +294 -0
- package/dist/team/task-tool.d.ts +47 -0
- package/dist/team/task-tool.js +118 -0
- package/dist/team/types.d.ts +172 -0
- package/dist/team/types.js +13 -0
- package/dist/tools/index.d.ts +48 -2
- package/dist/tools/index.js +26 -1
- package/dist/ui/App.d.ts +9 -1
- package/dist/ui/App.js +140 -21
- package/dist/ui/Composer.d.ts +6 -1
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +15 -1
- package/dist/ui/PromptInput.js +38 -1
- package/dist/ui/StatusBar.d.ts +21 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/TeamPanel.d.ts +57 -0
- package/dist/ui/TeamPanel.js +134 -0
- package/dist/ui/Transcript.js +10 -0
- package/dist/ui/entries/TeamCard.d.ts +31 -0
- package/dist/ui/entries/TeamCard.js +39 -0
- package/dist/ui/glyphs.d.ts +22 -0
- package/dist/ui/glyphs.js +10 -0
- package/dist/ui/layout/AppShell.d.ts +20 -1
- package/dist/ui/layout/AppShell.js +39 -4
- package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
- package/dist/ui/layout/ScrollIndicator.js +15 -0
- package/dist/ui/layout/ScrollViewport.d.ts +8 -2
- package/dist/ui/layout/ScrollViewport.js +24 -4
- package/dist/ui/layout/regions.d.ts +55 -0
- package/dist/ui/layout/regions.js +52 -0
- package/dist/ui/layout/scroll-indicator.d.ts +27 -0
- package/dist/ui/layout/scroll-indicator.js +35 -0
- package/dist/ui/layout/scroll.d.ts +9 -0
- package/dist/ui/layout/scroll.js +23 -0
- package/dist/ui/overlays/HelpOverlay.js +14 -0
- package/dist/ui/screen.d.ts +14 -1
- package/dist/ui/screen.js +37 -3
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +11 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.d.ts +68 -0
- package/dist/ui/use-wheel-routing.js +139 -0
- package/package.json +2 -2
package/dist/tools/index.d.ts
CHANGED
|
@@ -13,6 +13,19 @@ import type { AgentMode } from '../agent/agent-mode.js';
|
|
|
13
13
|
export interface ConfirmRequest {
|
|
14
14
|
tool: string;
|
|
15
15
|
summary: string;
|
|
16
|
+
/**
|
|
17
|
+
* The tool call's own abort signal, when the executor supplied one.
|
|
18
|
+
*
|
|
19
|
+
* IT IS HERE BECAUSE `withConfirmation` IS THE ONLY PLACE THAT HAS IT. A
|
|
20
|
+
* confirm implementation that has to abandon a pending dialog — the subagent
|
|
21
|
+
* queue in `team/human-queue.ts` is the one that does (team-subagents §3.11 /
|
|
22
|
+
* AC-16) — cannot reach `ctx` any other way, and without it an `Esc` during a
|
|
23
|
+
* dispatch pops a fresh dialog for every request still sitting in the FIFO.
|
|
24
|
+
*
|
|
25
|
+
* Ignored by the interactive and headless confirms, which are resolved by
|
|
26
|
+
* their own channel when a run ends.
|
|
27
|
+
*/
|
|
28
|
+
signal?: AbortSignal;
|
|
16
29
|
}
|
|
17
30
|
export interface BuiltinToolsOptions {
|
|
18
31
|
getCwd: () => string;
|
|
@@ -72,6 +85,39 @@ export interface BuiltinToolsOptions {
|
|
|
72
85
|
* burns a turn.
|
|
73
86
|
*/
|
|
74
87
|
planTools?: AgentTool[];
|
|
88
|
+
/**
|
|
89
|
+
* Team-mode tools, appended verbatim. The lead gets `[task]`; a subagent gets
|
|
90
|
+
* `[team_send, team_wait]`.
|
|
91
|
+
*
|
|
92
|
+
* THIS OPTION EXISTS BECAUSE A TEST FORBIDS THE ALTERNATIVE (D-15 / P0-1).
|
|
93
|
+
* `tools.test.ts::C7` asserts that `HOST_TOOL_NAMES` equals WHAT THIS FACTORY
|
|
94
|
+
* ACTUALLY PRODUCES, so the moment `task` joined `HOST_TOOL_NAMES` (I-7),
|
|
95
|
+
* appending it in `AgentController` after the factory returned would turn C7
|
|
96
|
+
* red — with a failure message about two lists of names that says nothing
|
|
97
|
+
* about team mode.
|
|
98
|
+
*
|
|
99
|
+
* ONE option serves both sides on purpose: the only two call sites are the
|
|
100
|
+
* lead's controller and `createSubagent`, and a second option would just be a
|
|
101
|
+
* second name for "extra tools this caller supplies".
|
|
102
|
+
*
|
|
103
|
+
* An empty array (the `--no-team` path) leaves the tool list byte-identical to
|
|
104
|
+
* the pre-team build — AC-11, provable by object identity exactly as
|
|
105
|
+
* `--no-skills` is.
|
|
106
|
+
*/
|
|
107
|
+
teamTools?: AgentTool[];
|
|
108
|
+
/**
|
|
109
|
+
* Tool names that skip the CEILING wrapper only (never the plan gate, never
|
|
110
|
+
* confirmation).
|
|
111
|
+
*
|
|
112
|
+
* It exists for a subagent's two comm tools and for nothing else (§3.11).
|
|
113
|
+
* `team_send` / `team_wait` are not in the LEAD's registered tool list, so
|
|
114
|
+
* evaluating them against the lead's `allowed-tools` ceiling would refuse them
|
|
115
|
+
* and dead-end the channel — the same dead end `SKILL_TOOL_FLOOR` exists to
|
|
116
|
+
* prevent one level up. Widening this set to anything that can write to disk
|
|
117
|
+
* would turn delegation into a ceiling bypass, which is exactly what §3.11
|
|
118
|
+
* re-applies the lead's policy to children to avoid.
|
|
119
|
+
*/
|
|
120
|
+
policyExempt?: ReadonlySet<string>;
|
|
75
121
|
}
|
|
76
122
|
/**
|
|
77
123
|
* Tools the ceiling always permits (D-G6). The ONLY place that knows which of
|
|
@@ -88,7 +134,7 @@ export interface BuiltinToolsOptions {
|
|
|
88
134
|
* is the whole of what this mechanism is worth, and saying so plainly beats
|
|
89
135
|
* implying it covers more.
|
|
90
136
|
*/
|
|
91
|
-
export declare const SKILL_TOOL_FLOOR: readonly ["read_file", "list_dir", "glob", "grep", "skill", "skill_find", "ask_user", "submit_plan"];
|
|
137
|
+
export declare const SKILL_TOOL_FLOOR: readonly ["read_file", "list_dir", "glob", "grep", "skill", "skill_find", "ask_user", "submit_plan", "task"];
|
|
92
138
|
/**
|
|
93
139
|
* The five tools plan mode refuses: exactly the complement of `SKILL_TOOL_FLOOR`
|
|
94
140
|
* within `HOST_TOOL_NAMES` — "the five that change something", the set this file
|
|
@@ -121,5 +167,5 @@ export declare const PLAN_MODE_BLOCKED_TOOLS: ReadonlySet<string>;
|
|
|
121
167
|
* declaration MEANS, and that reading must not change with how the CLI was
|
|
122
168
|
* started.
|
|
123
169
|
*/
|
|
124
|
-
export declare const HOST_TOOL_NAMES: readonly ["read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "bash", "skill", "skill_find", "skill_install", "skill_create", "ask_user", "submit_plan"];
|
|
170
|
+
export declare const HOST_TOOL_NAMES: readonly ["read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "bash", "skill", "skill_find", "skill_install", "skill_create", "ask_user", "submit_plan", "task"];
|
|
125
171
|
export declare function createBuiltinTools(options: BuiltinToolsOptions): AgentTool[];
|
package/dist/tools/index.js
CHANGED
|
@@ -46,6 +46,12 @@ export const SKILL_TOOL_FLOOR = [
|
|
|
46
46
|
// "read-only".)
|
|
47
47
|
'ask_user',
|
|
48
48
|
'submit_plan',
|
|
49
|
+
// `task` joins the floor rather than the blocked set (D-5), and the choice is
|
|
50
|
+
// forced to be conscious by the partition test below. It cannot itself mutate
|
|
51
|
+
// anything: every child mutation passes through this same wrapper stack one
|
|
52
|
+
// level down, with the child inheriting `agentMode` through the same closure.
|
|
53
|
+
// Blocking it would forfeit parallel research, which is plan mode's best use.
|
|
54
|
+
'task',
|
|
49
55
|
];
|
|
50
56
|
/**
|
|
51
57
|
* The five tools plan mode refuses: exactly the complement of `SKILL_TOOL_FLOOR`
|
|
@@ -99,6 +105,13 @@ export const HOST_TOOL_NAMES = [
|
|
|
99
105
|
'skill_create',
|
|
100
106
|
'ask_user',
|
|
101
107
|
'submit_plan',
|
|
108
|
+
// `team_send` / `team_wait` are DELIBERATELY ABSENT (P2-5). This constant
|
|
109
|
+
// answers "can a skill's `allowed-tools` declaration ever take effect?", and
|
|
110
|
+
// those two are never registered on a lead — so a skill naming them genuinely
|
|
111
|
+
// IS unenforceable, and `aragon skills doctor` saying so is correct rather
|
|
112
|
+
// than a gap. They reach children through `policyExempt`, a different
|
|
113
|
+
// mechanism answering a different question.
|
|
114
|
+
'task',
|
|
102
115
|
];
|
|
103
116
|
function summarize(toolName, params) {
|
|
104
117
|
if (toolName === 'bash')
|
|
@@ -118,6 +131,11 @@ function withConfirmation(tool, confirm) {
|
|
|
118
131
|
const approved = await confirm({
|
|
119
132
|
tool: tool.name,
|
|
120
133
|
summary: summarize(tool.name, params),
|
|
134
|
+
// Forwarded, not consumed, here: this wrapper still awaits the answer,
|
|
135
|
+
// because a confirm that resolved itself on abort would let the tool run
|
|
136
|
+
// unapproved. It is the confirm IMPLEMENTATION that decides what an
|
|
137
|
+
// abort means (see `ConfirmRequest.signal`).
|
|
138
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
121
139
|
});
|
|
122
140
|
if (!approved)
|
|
123
141
|
return errorResult('Cancelled by user.');
|
|
@@ -209,6 +227,12 @@ export function createBuiltinTools(options) {
|
|
|
209
227
|
if (options.planTools && options.planTools.length > 0) {
|
|
210
228
|
tools = [...tools, ...options.planTools];
|
|
211
229
|
}
|
|
230
|
+
// BEFORE both wrappers, so the lead's `task` is covered by the plan gate and
|
|
231
|
+
// the ceiling like everything else, and a child's comm tools are covered by
|
|
232
|
+
// everything except the ceiling (see `policyExempt`).
|
|
233
|
+
if (options.teamTools && options.teamTools.length > 0) {
|
|
234
|
+
tools = [...tools, ...options.teamTools];
|
|
235
|
+
}
|
|
212
236
|
// No decision provider (`--no-skills`, or a caller that never enabled the
|
|
213
237
|
// policy) means the ceiling adds nothing.
|
|
214
238
|
//
|
|
@@ -223,7 +247,8 @@ export function createBuiltinTools(options) {
|
|
|
223
247
|
// available ordering. The ceiling needs no I/O, so it goes before confirmation.
|
|
224
248
|
if (options.toolPolicy) {
|
|
225
249
|
const getDecision = options.toolPolicy;
|
|
226
|
-
|
|
250
|
+
const exempt = options.policyExempt;
|
|
251
|
+
tools = tools.map((t) => exempt?.has(t.name) ? t : withToolPolicy(t, getDecision, options.onToolPolicyEvent));
|
|
227
252
|
}
|
|
228
253
|
// TWO OPTIONS, TWO INDEPENDENT GUARDS, NO SHARED EXIT.
|
|
229
254
|
//
|
package/dist/ui/App.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type { AgentController } from '../agent/controller.js';
|
|
|
9
9
|
import type { ConfirmRequest } from '../tools/index.js';
|
|
10
10
|
import { type Overlay } from '../agent/reducer.js';
|
|
11
11
|
import { type RenderMode } from './layout/frame.js';
|
|
12
|
+
import { type FrameGeometry } from './layout/regions.js';
|
|
13
|
+
import type { MouseSource } from '../input/stdin-mouse-filter.js';
|
|
12
14
|
import type { HumanInputBridge } from '../tools/human-input.js';
|
|
13
15
|
export interface ConfirmBridge {
|
|
14
16
|
handler: ((req: ConfirmRequest) => Promise<boolean>) | null;
|
|
@@ -23,5 +25,11 @@ export interface AppProps {
|
|
|
23
25
|
confirmBridge?: ConfirmBridge;
|
|
24
26
|
/** The plan-mode human channel; absent in tests that do not exercise it. */
|
|
25
27
|
humanInputBridge?: HumanInputBridge;
|
|
28
|
+
/** Wheel events, already stripped out of stdin. Absent whenever mouse support
|
|
29
|
+
* is off: inline mode, `--no-mouse`, a non-TTY, or a filter that failed to
|
|
30
|
+
* build (mouse-wheel-region-routing §4.2). */
|
|
31
|
+
mouseSource?: MouseSource;
|
|
32
|
+
/** Test-only seam (§8.2): frame geometry, bypassing `measureElement`. */
|
|
33
|
+
geometryOverride?: FrameGeometry;
|
|
26
34
|
}
|
|
27
|
-
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, }: AppProps): React.ReactElement;
|
|
35
|
+
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }: AppProps): React.ReactElement;
|
package/dist/ui/App.js
CHANGED
|
@@ -10,8 +10,11 @@ import process from 'node:process';
|
|
|
10
10
|
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
11
11
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
12
12
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
13
|
-
import {
|
|
13
|
+
import { computeCost } from '../agent/usage.js';
|
|
14
|
+
import { coerceMaxTokens, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
14
15
|
import { updatePersistedConfig } from '../config/store.js';
|
|
16
|
+
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
17
|
+
import { bumpSubmitCount, setMouseNoticeSeen } from '../config/ui-state.js';
|
|
15
18
|
import { getLogger } from '../logging/logger.js';
|
|
16
19
|
import { registerSecret } from '../logging/secret-registry.js';
|
|
17
20
|
import { detectCapabilities } from './capabilities.js';
|
|
@@ -25,12 +28,15 @@ import { Transcript, TranscriptList } from './Transcript.js';
|
|
|
25
28
|
import { PromptInput } from './PromptInput.js';
|
|
26
29
|
import { Composer } from './Composer.js';
|
|
27
30
|
import { StatusBar } from './StatusBar.js';
|
|
31
|
+
import { TeamPanel } from './TeamPanel.js';
|
|
28
32
|
import { ToastStack } from './ToastStack.js';
|
|
29
33
|
import { AppShell } from './layout/AppShell.js';
|
|
30
34
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
31
35
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
32
36
|
import { frameHeight, MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
33
37
|
import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
38
|
+
import { UNMEASURED_GEOMETRY } from './layout/regions.js';
|
|
39
|
+
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
34
40
|
import { installConsoleBridge } from './console-bridge.js';
|
|
35
41
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
36
42
|
import { OverlayFrame } from './layout/OverlayFrame.js';
|
|
@@ -46,9 +52,7 @@ import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js'
|
|
|
46
52
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
47
53
|
/** Merge pending delta actions ~33 ms and flush the coalescer buffer. */
|
|
48
54
|
const COALESCE_MS = 33;
|
|
49
|
-
|
|
50
|
-
const OVERLAY_PAGE = 8;
|
|
51
|
-
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, }) {
|
|
55
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }) {
|
|
52
56
|
const { exit } = useApp();
|
|
53
57
|
const { stdout } = useStdout();
|
|
54
58
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -58,9 +62,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
58
62
|
const [confirmState, setConfirmState] = useState(null);
|
|
59
63
|
/** The request the question / plan overlay is currently rendering. */
|
|
60
64
|
const [humanRequest, setHumanRequest] = useState(null);
|
|
61
|
-
|
|
65
|
+
// Seeded from the history file, not from `CliConfig`: the list is no longer
|
|
66
|
+
// part of the resolved config, so every other `loadConfig()` caller is spared
|
|
67
|
+
// reading it (config-state-separation D-4).
|
|
68
|
+
const [promptHistory, setPromptHistory] = useState(() => loadPromptHistory());
|
|
62
69
|
// Scroll plumbing: intent goes down, one derived display number comes back.
|
|
63
70
|
const [scrollIntent, setScrollIntent] = useState();
|
|
71
|
+
/** One prompt-history step per wheel notch over the composer band (§4.7). */
|
|
72
|
+
const [historyIntent, setHistoryIntent] = useState();
|
|
64
73
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
65
74
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
66
75
|
// I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
|
|
@@ -98,6 +107,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
98
107
|
/** Every outstanding `gate.request()`. Held in a ref, never in state: it is
|
|
99
108
|
* bookkeeping for promises, and re-rendering on it would be noise. */
|
|
100
109
|
const pendingHuman = useRef(new Set());
|
|
110
|
+
/** Written by `AppShell`'s layout effect, read at wheel-event time (§4.5). */
|
|
111
|
+
const geometryRef = useRef(UNMEASURED_GEOMETRY);
|
|
101
112
|
/**
|
|
102
113
|
* THE SINGLE SETTLE PATH for a human request. Idempotent per entry, and it
|
|
103
114
|
* clears the overlay as its last act.
|
|
@@ -190,6 +201,62 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
190
201
|
unsubscribe();
|
|
191
202
|
};
|
|
192
203
|
}, [controller, flushPending, cancelPendingHuman]);
|
|
204
|
+
// --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
|
|
205
|
+
//
|
|
206
|
+
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
|
|
207
|
+
// deliberately not a member of core's `AgentEvent` union (D-10 / I-5), so it
|
|
208
|
+
// cannot arrive through `controller.subscribe`.
|
|
209
|
+
//
|
|
210
|
+
// These actions are NOT routed through the streaming coalescer. That buffer
|
|
211
|
+
// exists to merge text/thinking deltas; `agent_update` is already coalesced at
|
|
212
|
+
// the source at 120 ms per child (§5.2), and pushing it through a second
|
|
213
|
+
// buffer would only delay the roster.
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
const unsubscribeTeam = controller.subscribeTeam((event) => {
|
|
216
|
+
switch (event.type) {
|
|
217
|
+
case 'dispatch_start':
|
|
218
|
+
dispatch({
|
|
219
|
+
type: 'teamStart',
|
|
220
|
+
dispatchId: event.dispatchId,
|
|
221
|
+
requested: event.requested,
|
|
222
|
+
specs: event.specs,
|
|
223
|
+
});
|
|
224
|
+
break;
|
|
225
|
+
case 'agent_update': {
|
|
226
|
+
const snapshot = controller.getTeamSnapshot();
|
|
227
|
+
if (snapshot)
|
|
228
|
+
dispatch({ type: 'teamUpdate', snapshot });
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'message': {
|
|
232
|
+
const snapshot = controller.getTeamSnapshot();
|
|
233
|
+
if (snapshot)
|
|
234
|
+
dispatch({ type: 'teamUpdate', snapshot });
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case 'usage':
|
|
238
|
+
// Child spend is REAL SPEND and must reach the status bar, or the
|
|
239
|
+
// session cost readout under-reports by however much the team consumed
|
|
240
|
+
// — the single most misleading possible failure of this feature (R-5).
|
|
241
|
+
dispatch({
|
|
242
|
+
type: 'teamUsage',
|
|
243
|
+
usage: event.usage,
|
|
244
|
+
costDelta: computeCost(event.usage, controller.getModelInfo().cost),
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
case 'dispatch_end':
|
|
248
|
+
dispatch({ type: 'teamEnd', outcome: event.outcome });
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
return () => {
|
|
253
|
+
unsubscribeTeam();
|
|
254
|
+
// Nothing else disposes the runtime on an inline-mode exit or a crash
|
|
255
|
+
// that unmounts the tree, and an orphaned child holds the event loop open
|
|
256
|
+
// (R-15). `cli.tsx` calls this too; `dispose()` is idempotent.
|
|
257
|
+
controller.dispose();
|
|
258
|
+
};
|
|
259
|
+
}, [controller]);
|
|
193
260
|
// --- Elapsed timer + tokens/sec baseline while running. ----------------
|
|
194
261
|
useEffect(() => {
|
|
195
262
|
if (state.status !== 'running') {
|
|
@@ -233,7 +300,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
233
300
|
return undefined;
|
|
234
301
|
return installConsoleBridge((level, text) => dispatch({ type: 'notice', level, text }));
|
|
235
302
|
}, [fullscreen]);
|
|
236
|
-
|
|
303
|
+
// `enabled` is `mouseSource`, not `cfg.mouse`: a user whose terminal drops
|
|
304
|
+
// the events must never be shown advice about a mode not in effect (§6.1).
|
|
305
|
+
useStartupNotices(dispatch, {
|
|
306
|
+
enabled: !!mouseSource,
|
|
307
|
+
// `state.json`, not `config.json`: whether a one-shot notice has been shown
|
|
308
|
+
// is bookkeeping, and the comment on the old persisted key said so.
|
|
309
|
+
onSeen: () => setMouseNoticeSeen(true),
|
|
310
|
+
});
|
|
237
311
|
// --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
|
|
238
312
|
useEffect(() => {
|
|
239
313
|
publishExitSnapshot({
|
|
@@ -390,18 +464,25 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
390
464
|
refreshSkills: () => setSkillsNonce((n) => n + 1),
|
|
391
465
|
applyAgentMode: applyMode,
|
|
392
466
|
});
|
|
467
|
+
/**
|
|
468
|
+
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
469
|
+
* line to the history file and bumps one counter in `state.json`.
|
|
470
|
+
*
|
|
471
|
+
* `setPromptHistory` IS NOT OPTIONAL, and deleting it is the single easiest
|
|
472
|
+
* mistake to make here: `appendPrompt` keeps its own module-level array, but
|
|
473
|
+
* the `history` prop the composer recalls from is THIS React state. Without
|
|
474
|
+
* this line `↑` still works after a restart (the file is correct) and fails
|
|
475
|
+
* only for prompts submitted in the current session.
|
|
476
|
+
*/
|
|
393
477
|
const recordPrompt = (text) => {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
// and never enters React state — nothing re-renders on its account.
|
|
399
|
-
const nextCount = (controller.getConfig().submitCount ?? 0) + 1;
|
|
400
|
-
controller.setSubmitCount(nextCount);
|
|
401
|
-
persistConfig({ promptHistory: next, submitCount: nextCount });
|
|
478
|
+
setPromptHistory(appendPrompt(text));
|
|
479
|
+
// Read back through `controller.getConfig()` and never entering React
|
|
480
|
+
// state, exactly as before — nothing re-renders on the counter's account.
|
|
481
|
+
controller.setSubmitCount(bumpSubmitCount());
|
|
402
482
|
};
|
|
403
|
-
|
|
404
|
-
|
|
483
|
+
/** `repeat` folds a coalesced wheel burst; the keyboard always sends one. */
|
|
484
|
+
const scrollBy = useCallback((kind, repeat = 1) => {
|
|
485
|
+
setScrollIntent((prev) => ({ kind, nonce: (prev?.nonce ?? 0) + 1, repeat }));
|
|
405
486
|
}, []);
|
|
406
487
|
/**
|
|
407
488
|
* Send a message to the agent, bypassing slash parsing.
|
|
@@ -500,11 +581,30 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
500
581
|
}
|
|
501
582
|
resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
|
|
502
583
|
};
|
|
584
|
+
// --- Mouse wheel region routing (§4.6). Table and coalescer live in the
|
|
585
|
+
// hook (D-13); only the wiring it cannot own stays here. Inline arrows are
|
|
586
|
+
// fine — the hook re-reads its options through a ref every render. The
|
|
587
|
+
// `Math.max(0, …)` mirrors the keyboard branch below, and for the same
|
|
588
|
+
// reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
589
|
+
const { clearCoalescer } = useWheelRouting({
|
|
590
|
+
mouseSource,
|
|
591
|
+
geometryRef,
|
|
592
|
+
geometryOverride,
|
|
593
|
+
getOverlay: () => stateRef.current.overlay,
|
|
594
|
+
onScroll: scrollBy,
|
|
595
|
+
onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
|
|
596
|
+
onHistoryStep: (dir) => setHistoryIntent((prev) => ({ dir, nonce: (prev?.nonce ?? 0) + 1 })),
|
|
597
|
+
});
|
|
503
598
|
// A fresh overlay always opens at the top; carrying the previous overlay's
|
|
504
599
|
// offset over would open Help mid-list for no reason the user can see.
|
|
600
|
+
//
|
|
601
|
+
// `clearCoalescer()` rides along here rather than adding a second subscriber
|
|
602
|
+
// to the same transition (§4.7 rule 2): an accumulated-but-unflushed
|
|
603
|
+
// transcript scroll must not survive an overlay opening.
|
|
505
604
|
useEffect(() => {
|
|
506
605
|
setOverlayScroll(0);
|
|
507
|
-
|
|
606
|
+
clearCoalescer();
|
|
607
|
+
}, [state.overlay, clearCoalescer]);
|
|
508
608
|
// --- Initial overlay / prompt (after handlers are defined). ------------
|
|
509
609
|
useEffect(() => {
|
|
510
610
|
if (initialOverlay)
|
|
@@ -612,7 +712,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
612
712
|
return;
|
|
613
713
|
}
|
|
614
714
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
615
|
-
|
|
715
|
+
// `team` joins `tool` here (team-subagents §6.3): a dispatch card collapses
|
|
716
|
+
// its per-agent summaries behind the same key, and `toggleExpand` is keyed
|
|
717
|
+
// on entry id so it needs no change.
|
|
718
|
+
const last = [...stateRef.current.entries]
|
|
719
|
+
.reverse()
|
|
720
|
+
.find((e) => e.kind === 'tool' || e.kind === 'team');
|
|
616
721
|
if (last)
|
|
617
722
|
dispatch({ type: 'toggleExpand', id: last.id });
|
|
618
723
|
else
|
|
@@ -666,6 +771,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
666
771
|
apiKey: '',
|
|
667
772
|
logLevel: cfg.log.level,
|
|
668
773
|
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
|
|
774
|
+
// A MEASUREMENT MUST NEVER OUTLIVE THE FRAME IT DESCRIBED (P1-5): neither
|
|
775
|
+
// path below renders the full-screen `AppShell`, so nothing else would
|
|
776
|
+
// overwrite a stale `bottom`. See `UNMEASURED_GEOMETRY` for why there is no
|
|
777
|
+
// arithmetic fallback.
|
|
778
|
+
if (!fullscreen || rows < MIN_FULLSCREEN_ROWS)
|
|
779
|
+
geometryRef.current = UNMEASURED_GEOMETRY;
|
|
669
780
|
// A resize can drop below the usable floor. Do NOT leave the alt-screen —
|
|
670
781
|
// entering and leaving it on every drag is worse than a placeholder — and take
|
|
671
782
|
// the height from `frameHeight()` like everything else, because this is the
|
|
@@ -683,11 +794,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
683
794
|
// the header the moment the first message lands.
|
|
684
795
|
const opener = empty ? (_jsx(SessionOpener, { variant: fullscreen ? pickOpenerVariant(viewportBudget, cols, caps) : 'none', version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: fullscreen ? viewportBudget : Number.POSITIVE_INFINITY, theme: theme, caps: caps })) : null;
|
|
685
796
|
const density = cfg.density;
|
|
686
|
-
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps, windowSize: cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }), overlayNode] }));
|
|
797
|
+
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps, windowSize: cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }), overlayNode] }));
|
|
687
798
|
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
688
799
|
// rows; unbounded, it pushes the status bar past the frame height where
|
|
689
800
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
690
801
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
691
|
-
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
692
|
-
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, toast: _jsx(ToastStack, { toasts: state.toasts, theme: theme, mode: mode }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode
|
|
802
|
+
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, historyIntent: historyIntent, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
803
|
+
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, geometryRef: geometryRef, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, toast: _jsx(ToastStack, { toasts: state.toasts, theme: theme, mode: mode }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ...(state.team
|
|
804
|
+
? {
|
|
805
|
+
teamActive: {
|
|
806
|
+
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
807
|
+
r.phase !== 'failed' && r.phase !== 'aborted').length,
|
|
808
|
+
total: state.team.runs.length,
|
|
809
|
+
},
|
|
810
|
+
}
|
|
811
|
+
: {}) }) }));
|
|
693
812
|
}
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -35,9 +35,14 @@ export interface ComposerProps {
|
|
|
35
35
|
/** Mirror of the controller's effective mode; drives the chip and the border. */
|
|
36
36
|
agentMode: AgentMode;
|
|
37
37
|
popupMaxRows?: number;
|
|
38
|
+
/** Pure pass-through to `PromptInput` (mouse-wheel-region-routing §5.6). */
|
|
39
|
+
historyIntent?: {
|
|
40
|
+
dir: 'up' | 'down';
|
|
41
|
+
nonce: number;
|
|
42
|
+
};
|
|
38
43
|
theme: Theme;
|
|
39
44
|
caps: TermCapabilities;
|
|
40
45
|
onSubmit: (text: string) => void;
|
|
41
46
|
onHelp?: () => void;
|
|
42
47
|
}
|
|
43
|
-
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
48
|
+
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
package/dist/ui/Composer.js
CHANGED
|
@@ -56,7 +56,7 @@ function hintText(running, submitCount, glyphs, mode) {
|
|
|
56
56
|
'? help',
|
|
57
57
|
].join(dot);
|
|
58
58
|
}
|
|
59
|
-
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
|
|
59
|
+
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }) {
|
|
60
60
|
const [hasDraft, setHasDraft] = useState(false);
|
|
61
61
|
const onDraftChange = useCallback((next) => setHasDraft(next), []);
|
|
62
62
|
// Plan mode inserts ONE branch, above `draft` and below `running`. The
|
|
@@ -73,7 +73,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
|
|
|
73
73
|
: theme.idleBorder ?? theme.border;
|
|
74
74
|
const glyphs = pickGlyphs(caps);
|
|
75
75
|
const visible = showHint && hintsEnabled;
|
|
76
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
|
|
76
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, historyIntent: historyIntent }), visible && (
|
|
77
77
|
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
78
78
|
// costs zero rows on a 24-row terminal.
|
|
79
79
|
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(running, submitCount, glyphs, agentMode) })] }))] }));
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -55,6 +55,20 @@ interface PromptInputProps {
|
|
|
55
55
|
};
|
|
56
56
|
/** Fires only when the draft flips empty ⇄ non-empty, for the border color. */
|
|
57
57
|
onDraftChange?: (hasDraft: boolean) => void;
|
|
58
|
+
/**
|
|
59
|
+
* One prompt-history step, pushed by the wheel router when a notch lands on
|
|
60
|
+
* the composer band (mouse-wheel-region-routing §5.6). `nonce` is monotonic
|
|
61
|
+
* so two notches in the same direction are two steps.
|
|
62
|
+
*
|
|
63
|
+
* Applied through the SAME `verticalOrHistory` / popup-selection branches the
|
|
64
|
+
* keyboard uses, deliberately: it means the wheel and `↑`/`↓` cannot drift
|
|
65
|
+
* apart, and the "never clobber an in-progress draft" guard protects the
|
|
66
|
+
* wheel for free.
|
|
67
|
+
*/
|
|
68
|
+
historyIntent?: {
|
|
69
|
+
dir: 'up' | 'down';
|
|
70
|
+
nonce: number;
|
|
71
|
+
};
|
|
58
72
|
}
|
|
59
73
|
/** Apply an edit intent as a pure buffer/cursor transform. */
|
|
60
74
|
export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
|
|
@@ -84,5 +98,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
84
98
|
end: number;
|
|
85
99
|
query: string;
|
|
86
100
|
} | null;
|
|
87
|
-
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }: PromptInputProps): React.ReactElement;
|
|
101
|
+
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, historyIntent, }: PromptInputProps): React.ReactElement;
|
|
88
102
|
export {};
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -127,7 +127,7 @@ function isControlSeq(input) {
|
|
|
127
127
|
// ---------------------------------------------------------------------------
|
|
128
128
|
// Component
|
|
129
129
|
// ---------------------------------------------------------------------------
|
|
130
|
-
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }) {
|
|
130
|
+
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, historyIntent, }) {
|
|
131
131
|
const [buffer, setBuffer] = useState('');
|
|
132
132
|
const [cursor, setCursor] = useState(0);
|
|
133
133
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
@@ -261,6 +261,43 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
261
261
|
recallDown();
|
|
262
262
|
}
|
|
263
263
|
};
|
|
264
|
+
// --- Wheel-driven history step (mouse-wheel-region-routing §5.6). ---------
|
|
265
|
+
// The applier closes over the buffer, the cursor and the popup state, so it
|
|
266
|
+
// is re-created on every render and reached through a ref: depending on it
|
|
267
|
+
// directly would re-fire the effect on every keystroke.
|
|
268
|
+
const applyHistoryIntent = useRef(() => { });
|
|
269
|
+
applyHistoryIntent.current = (dir) => {
|
|
270
|
+
// Mirrors the keyboard branch exactly — with the completion popup open,
|
|
271
|
+
// Up/Down move the selection rather than recalling history.
|
|
272
|
+
if (popupItems.length > 0) {
|
|
273
|
+
setSel(dir === 'up'
|
|
274
|
+
? Math.max(0, clampedSel - 1)
|
|
275
|
+
: Math.min(popupItems.length - 1, clampedSel + 1));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
verticalOrHistory(dir);
|
|
279
|
+
};
|
|
280
|
+
const historyIntentRef = useRef(historyIntent);
|
|
281
|
+
historyIntentRef.current = historyIntent;
|
|
282
|
+
// I-9: seeded from the incoming nonce during the FIRST render, so a mount
|
|
283
|
+
// that inherits a nonce cannot replay it. This component really does remount
|
|
284
|
+
// — `App` returns the terminal-too-small placeholder before `AppShell` —
|
|
285
|
+
// so without the guard, shrinking the window mid-wheel and growing it back
|
|
286
|
+
// recalls a history entry with no gesture behind it.
|
|
287
|
+
const lastHistoryNonce = useRef(historyIntent?.nonce);
|
|
288
|
+
const historyNonce = historyIntent?.nonce ?? 0;
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
const current = historyIntentRef.current;
|
|
291
|
+
if (!current || !isActive)
|
|
292
|
+
return;
|
|
293
|
+
if (current.nonce === lastHistoryNonce.current)
|
|
294
|
+
return;
|
|
295
|
+
lastHistoryNonce.current = current.nonce;
|
|
296
|
+
applyHistoryIntent.current(current.dir);
|
|
297
|
+
// Keyed on the nonce alone, same reasoning as `ScrollViewport`'s intent
|
|
298
|
+
// effect: `historyIntent` is a fresh object on every parent render.
|
|
299
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
300
|
+
}, [historyNonce]);
|
|
264
301
|
// --- Input handling. -----------------------------------------------------
|
|
265
302
|
useInput((input, key) => {
|
|
266
303
|
// FIRST STATEMENT, BEFORE THE POPUP BRANCH, AND IT HAS TO STAY THERE.
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -49,6 +49,27 @@ interface StatusBarProps {
|
|
|
49
49
|
agentMode?: AgentMode;
|
|
50
50
|
/** Non-null while a deferred `plan -> build` waits for `agent_end`. */
|
|
51
51
|
pendingAgentMode?: AgentMode | null;
|
|
52
|
+
/**
|
|
53
|
+
* Live subagent counts (team-subagents §6.2), or absent when no dispatch is
|
|
54
|
+
* running — an ordinary session's bar is then unchanged.
|
|
55
|
+
*
|
|
56
|
+
* LEFT CLUSTER, and it degrades (D-20 / P2-1). The left cluster is
|
|
57
|
+
* `flexShrink={0}`, so every column it takes comes out of the context gauge
|
|
58
|
+
* and the cost readout opposite it; on an 80-column terminal the full
|
|
59
|
+
* `agents 3/5` visibly squeezes them, and the gauge is how a user notices they
|
|
60
|
+
* are about to run out of context. Below `TEAM_LIMITS.statusCompactCols` this
|
|
61
|
+
* therefore renders as `[3]` — the running count alone.
|
|
62
|
+
*
|
|
63
|
+
* This does NOT contradict the "no `cols >= N` breakpoint" rule above: that
|
|
64
|
+
* rule is written about `agentMode`, whose reason is that the mode must never
|
|
65
|
+
* be unreportable. The team counter has a second, fuller home in `TeamPanel`
|
|
66
|
+
* and a third in `TeamCard`; it is the one readout on this bar that can afford
|
|
67
|
+
* to degrade.
|
|
68
|
+
*/
|
|
69
|
+
teamActive?: {
|
|
70
|
+
running: number;
|
|
71
|
+
total: number;
|
|
72
|
+
};
|
|
52
73
|
}
|
|
53
74
|
export declare function StatusBar(props: StatusBarProps): React.ReactElement;
|
|
54
75
|
export {};
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -4,6 +4,7 @@ import { pickGlyphs } from './glyphs.js';
|
|
|
4
4
|
import { formatCost, formatTokens, formatDuration } from '../agent/usage.js';
|
|
5
5
|
import { buildGauge } from './gauge.js';
|
|
6
6
|
import { MODE_LABEL } from '../agent/agent-mode.js';
|
|
7
|
+
import { TEAM_LIMITS } from '../team/limits.js';
|
|
7
8
|
/**
|
|
8
9
|
* I-5 — DO NOT "CLEAN THIS UP". Ink short-circuits identical output at TWO
|
|
9
10
|
* gates (`ink.js:132` and `log-update.js:13`), so a fixed frame whose React
|
|
@@ -23,7 +24,7 @@ function redrawChar(nonce) {
|
|
|
23
24
|
return nonce % 2 === 0 ? ' ' : NBSP;
|
|
24
25
|
}
|
|
25
26
|
export function StatusBar(props) {
|
|
26
|
-
const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, } = props;
|
|
27
|
+
const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, } = props;
|
|
27
28
|
const { stdout } = useStdout();
|
|
28
29
|
const cols = stdout?.columns ?? 80;
|
|
29
30
|
const running = status === 'running';
|
|
@@ -47,5 +48,7 @@ export function StatusBar(props) {
|
|
|
47
48
|
// and "? hel" rather than truncating cleanly at one end. Pinning the left
|
|
48
49
|
// cluster keeps the run status and model intact and makes the degradation
|
|
49
50
|
// land in one predictable place.
|
|
50
|
-
_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [redrawChar(redrawNonce), statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })),
|
|
51
|
+
_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [redrawChar(redrawNonce), statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), teamActive && (_jsxs(Text, { color: theme.toolRunning, bold: true, children: [' ', cols >= TEAM_LIMITS.statusCompactCols
|
|
52
|
+
? `agents ${teamActive.running}/${teamActive.total}`
|
|
53
|
+
: `[${teamActive.running}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
|
|
51
54
|
}
|