@aragon-agent/cli 0.5.2 → 0.5.4
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 +127 -0
- package/README.md +193 -3
- 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 +98 -7
- package/dist/commands/builtins.d.ts +2 -2
- package/dist/commands/builtins.js +133 -3
- 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 +83 -6
- package/dist/config/env.d.ts +3 -2
- package/dist/config/env.js +44 -6
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.js +107 -6
- 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 +176 -18
- package/dist/config/schema.js +162 -8
- package/dist/config/store.js +40 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -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 +22 -1
- package/dist/ui/App.js +144 -18
- 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 +10 -0
- package/dist/ui/glyphs.js +6 -0
- package/dist/ui/layout/AppShell.d.ts +11 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/overlays/HelpOverlay.js +13 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
- package/dist/ui/overlays/SettingsScreen.js +38 -1
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +1 -1
- package/dist/ui/use-startup-notices.js +2 -1
- 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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import type { AgentController } from '../agent/controller.js';
|
|
9
9
|
import type { ConfirmRequest } from '../tools/index.js';
|
|
10
|
-
import { type Overlay } from '../agent/reducer.js';
|
|
10
|
+
import { type Overlay, type ToastLevel } from '../agent/reducer.js';
|
|
11
11
|
import { type RenderMode } from './layout/frame.js';
|
|
12
12
|
import { type FrameGeometry } from './layout/regions.js';
|
|
13
13
|
import type { MouseSource } from '../input/stdin-mouse-filter.js';
|
|
@@ -15,6 +15,27 @@ import type { HumanInputBridge } from '../tools/human-input.js';
|
|
|
15
15
|
export interface ConfirmBridge {
|
|
16
16
|
handler: ((req: ConfirmRequest) => Promise<boolean>) | null;
|
|
17
17
|
}
|
|
18
|
+
/** What the settings screen's `Max tokens` field resolved to. */
|
|
19
|
+
export interface SettingsMaxTokens {
|
|
20
|
+
/** Pushed into the live controller. `undefined` is AUTO. */
|
|
21
|
+
applied: number | undefined;
|
|
22
|
+
/** Written to the config file, or `'skip'` to leave the stored value alone. */
|
|
23
|
+
persist: number | null | 'skip';
|
|
24
|
+
/** Replaces the generic "Settings saved." acknowledgement when present. */
|
|
25
|
+
toast?: [ToastLevel, string];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
|
|
29
|
+
* discard the rest of the form.
|
|
30
|
+
*
|
|
31
|
+
* The three outcomes are genuinely different and must stay distinguishable:
|
|
32
|
+
* `auto` is a setting, a clamped number is a setting the user should be told
|
|
33
|
+
* about, and nonsense is not a setting at all — it keeps the previous value and
|
|
34
|
+
* says so, rather than silently resolving to a default the user never chose.
|
|
35
|
+
*
|
|
36
|
+
* Exported for the tests; there is no other caller.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveSettingsMaxTokens(raw: string, previous: number | undefined): SettingsMaxTokens;
|
|
18
39
|
export interface AppProps {
|
|
19
40
|
controller: AgentController;
|
|
20
41
|
version: string;
|
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 { parseMaxTokensInput, 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,6 +28,7 @@ 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';
|
|
@@ -46,6 +50,41 @@ import { CommandRegistry, runSlashInput } from '../commands/registry.js';
|
|
|
46
50
|
import { registerBuiltinCommands } from '../commands/builtins.js';
|
|
47
51
|
import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
|
|
48
52
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
53
|
+
/**
|
|
54
|
+
* Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
|
|
55
|
+
* discard the rest of the form.
|
|
56
|
+
*
|
|
57
|
+
* The three outcomes are genuinely different and must stay distinguishable:
|
|
58
|
+
* `auto` is a setting, a clamped number is a setting the user should be told
|
|
59
|
+
* about, and nonsense is not a setting at all — it keeps the previous value and
|
|
60
|
+
* says so, rather than silently resolving to a default the user never chose.
|
|
61
|
+
*
|
|
62
|
+
* Exported for the tests; there is no other caller.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveSettingsMaxTokens(raw, previous) {
|
|
65
|
+
const parsed = parseMaxTokensInput(raw);
|
|
66
|
+
if (parsed.kind === 'auto') {
|
|
67
|
+
return {
|
|
68
|
+
applied: undefined,
|
|
69
|
+
persist: null,
|
|
70
|
+
toast: ['info', 'Max tokens: auto (per-model ceiling).'],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (parsed.kind === 'value') {
|
|
74
|
+
return {
|
|
75
|
+
applied: parsed.value,
|
|
76
|
+
persist: parsed.value,
|
|
77
|
+
...(parsed.clamped
|
|
78
|
+
? { toast: ['warn', `Max tokens clamped to ${parsed.value}.`] }
|
|
79
|
+
: {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
applied: previous,
|
|
84
|
+
persist: 'skip',
|
|
85
|
+
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
49
88
|
/** Merge pending delta actions ~33 ms and flush the coalescer buffer. */
|
|
50
89
|
const COALESCE_MS = 33;
|
|
51
90
|
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }) {
|
|
@@ -58,7 +97,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
58
97
|
const [confirmState, setConfirmState] = useState(null);
|
|
59
98
|
/** The request the question / plan overlay is currently rendering. */
|
|
60
99
|
const [humanRequest, setHumanRequest] = useState(null);
|
|
61
|
-
|
|
100
|
+
// Seeded from the history file, not from `CliConfig`: the list is no longer
|
|
101
|
+
// part of the resolved config, so every other `loadConfig()` caller is spared
|
|
102
|
+
// reading it (config-state-separation D-4).
|
|
103
|
+
const [promptHistory, setPromptHistory] = useState(() => loadPromptHistory());
|
|
62
104
|
// Scroll plumbing: intent goes down, one derived display number comes back.
|
|
63
105
|
const [scrollIntent, setScrollIntent] = useState();
|
|
64
106
|
/** One prompt-history step per wheel notch over the composer band (§4.7). */
|
|
@@ -194,6 +236,62 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
194
236
|
unsubscribe();
|
|
195
237
|
};
|
|
196
238
|
}, [controller, flushPending, cancelPendingHuman]);
|
|
239
|
+
// --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
|
|
240
|
+
//
|
|
241
|
+
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
|
|
242
|
+
// deliberately not a member of core's `AgentEvent` union (D-10 / I-5), so it
|
|
243
|
+
// cannot arrive through `controller.subscribe`.
|
|
244
|
+
//
|
|
245
|
+
// These actions are NOT routed through the streaming coalescer. That buffer
|
|
246
|
+
// exists to merge text/thinking deltas; `agent_update` is already coalesced at
|
|
247
|
+
// the source at 120 ms per child (§5.2), and pushing it through a second
|
|
248
|
+
// buffer would only delay the roster.
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const unsubscribeTeam = controller.subscribeTeam((event) => {
|
|
251
|
+
switch (event.type) {
|
|
252
|
+
case 'dispatch_start':
|
|
253
|
+
dispatch({
|
|
254
|
+
type: 'teamStart',
|
|
255
|
+
dispatchId: event.dispatchId,
|
|
256
|
+
requested: event.requested,
|
|
257
|
+
specs: event.specs,
|
|
258
|
+
});
|
|
259
|
+
break;
|
|
260
|
+
case 'agent_update': {
|
|
261
|
+
const snapshot = controller.getTeamSnapshot();
|
|
262
|
+
if (snapshot)
|
|
263
|
+
dispatch({ type: 'teamUpdate', snapshot });
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case 'message': {
|
|
267
|
+
const snapshot = controller.getTeamSnapshot();
|
|
268
|
+
if (snapshot)
|
|
269
|
+
dispatch({ type: 'teamUpdate', snapshot });
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case 'usage':
|
|
273
|
+
// Child spend is REAL SPEND and must reach the status bar, or the
|
|
274
|
+
// session cost readout under-reports by however much the team consumed
|
|
275
|
+
// — the single most misleading possible failure of this feature (R-5).
|
|
276
|
+
dispatch({
|
|
277
|
+
type: 'teamUsage',
|
|
278
|
+
usage: event.usage,
|
|
279
|
+
costDelta: computeCost(event.usage, controller.getModelInfo().cost),
|
|
280
|
+
});
|
|
281
|
+
break;
|
|
282
|
+
case 'dispatch_end':
|
|
283
|
+
dispatch({ type: 'teamEnd', outcome: event.outcome });
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
return () => {
|
|
288
|
+
unsubscribeTeam();
|
|
289
|
+
// Nothing else disposes the runtime on an inline-mode exit or a crash
|
|
290
|
+
// that unmounts the tree, and an orphaned child holds the event loop open
|
|
291
|
+
// (R-15). `cli.tsx` calls this too; `dispose()` is idempotent.
|
|
292
|
+
controller.dispose();
|
|
293
|
+
};
|
|
294
|
+
}, [controller]);
|
|
197
295
|
// --- Elapsed timer + tokens/sec baseline while running. ----------------
|
|
198
296
|
useEffect(() => {
|
|
199
297
|
if (state.status !== 'running') {
|
|
@@ -241,7 +339,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
241
339
|
// the events must never be shown advice about a mode not in effect (§6.1).
|
|
242
340
|
useStartupNotices(dispatch, {
|
|
243
341
|
enabled: !!mouseSource,
|
|
244
|
-
|
|
342
|
+
// `state.json`, not `config.json`: whether a one-shot notice has been shown
|
|
343
|
+
// is bookkeeping, and the comment on the old persisted key said so.
|
|
344
|
+
onSeen: () => setMouseNoticeSeen(true),
|
|
245
345
|
});
|
|
246
346
|
// --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
|
|
247
347
|
useEffect(() => {
|
|
@@ -399,15 +499,21 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
399
499
|
refreshSkills: () => setSkillsNonce((n) => n + 1),
|
|
400
500
|
applyAgentMode: applyMode,
|
|
401
501
|
});
|
|
502
|
+
/**
|
|
503
|
+
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
504
|
+
* line to the history file and bumps one counter in `state.json`.
|
|
505
|
+
*
|
|
506
|
+
* `setPromptHistory` IS NOT OPTIONAL, and deleting it is the single easiest
|
|
507
|
+
* mistake to make here: `appendPrompt` keeps its own module-level array, but
|
|
508
|
+
* the `history` prop the composer recalls from is THIS React state. Without
|
|
509
|
+
* this line `↑` still works after a restart (the file is correct) and fails
|
|
510
|
+
* only for prompts submitted in the current session.
|
|
511
|
+
*/
|
|
402
512
|
const recordPrompt = (text) => {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
// and never enters React state — nothing re-renders on its account.
|
|
408
|
-
const nextCount = (controller.getConfig().submitCount ?? 0) + 1;
|
|
409
|
-
controller.setSubmitCount(nextCount);
|
|
410
|
-
persistConfig({ promptHistory: next, submitCount: nextCount });
|
|
513
|
+
setPromptHistory(appendPrompt(text));
|
|
514
|
+
// Read back through `controller.getConfig()` and never entering React
|
|
515
|
+
// state, exactly as before — nothing re-renders on the counter's account.
|
|
516
|
+
controller.setSubmitCount(bumpSubmitCount());
|
|
411
517
|
};
|
|
412
518
|
/** `repeat` folds a coalesced wheel burst; the keyboard always sends one. */
|
|
413
519
|
const scrollBy = useCallback((kind, repeat = 1) => {
|
|
@@ -447,10 +553,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
447
553
|
submitMessage(raw.startsWith('//') ? raw.slice(1) : raw);
|
|
448
554
|
};
|
|
449
555
|
const handleSettingsSave = (values) => {
|
|
450
|
-
|
|
556
|
+
// An unusable cap must NOT discard the rest of the form: the same Enter
|
|
557
|
+
// press is often carrying a freshly pasted API key, and losing that to a
|
|
558
|
+
// typo in an unrelated field is the worst outcome available here.
|
|
559
|
+
const cap = resolveSettingsMaxTokens(values.maxTokens, controller.getConfig().maxTokens);
|
|
451
560
|
controller.setModel(values.provider, values.model, values.baseUrl || undefined);
|
|
452
561
|
controller.setThinkingLevel(values.thinkingLevel);
|
|
453
|
-
controller.setMaxTokens(
|
|
562
|
+
controller.setMaxTokens(cap.applied);
|
|
454
563
|
const key = values.apiKey.trim();
|
|
455
564
|
if (key.length > 0) {
|
|
456
565
|
// Registration site 4 (§4.4.3), and the one that matters most: this is how
|
|
@@ -465,7 +574,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
465
574
|
model: values.model,
|
|
466
575
|
baseUrl: values.baseUrl.trim() ? values.baseUrl.trim() : null,
|
|
467
576
|
thinkingLevel: values.thinkingLevel,
|
|
468
|
-
|
|
577
|
+
// Omitted entirely when the field was unusable, so the previous value
|
|
578
|
+
// survives instead of being overwritten with a guess.
|
|
579
|
+
...(cap.persist !== 'skip' ? { maxTokens: cap.persist } : {}),
|
|
469
580
|
};
|
|
470
581
|
if (key.length > 0)
|
|
471
582
|
patch.apiKeys = { [values.provider]: key };
|
|
@@ -476,7 +587,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
476
587
|
persistConfig(patch);
|
|
477
588
|
getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
|
|
478
589
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
479
|
-
toast('success', 'Settings saved.');
|
|
590
|
+
toast(cap.toast?.[0] ?? 'success', cap.toast?.[1] ?? 'Settings saved.');
|
|
480
591
|
};
|
|
481
592
|
const handleModelSelect = (provider, model) => {
|
|
482
593
|
controller.setModel(provider, model);
|
|
@@ -641,7 +752,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
641
752
|
return;
|
|
642
753
|
}
|
|
643
754
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
644
|
-
|
|
755
|
+
// `team` joins `tool` here (team-subagents §6.3): a dispatch card collapses
|
|
756
|
+
// its per-agent summaries behind the same key, and `toggleExpand` is keyed
|
|
757
|
+
// on entry id so it needs no change.
|
|
758
|
+
const last = [...stateRef.current.entries]
|
|
759
|
+
.reverse()
|
|
760
|
+
.find((e) => e.kind === 'tool' || e.kind === 'team');
|
|
645
761
|
if (last)
|
|
646
762
|
dispatch({ type: 'toggleExpand', id: last.id });
|
|
647
763
|
else
|
|
@@ -691,7 +807,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
691
807
|
model: cfg.model,
|
|
692
808
|
baseUrl: cfg.baseUrl ?? '',
|
|
693
809
|
thinkingLevel: cfg.thinkingLevel,
|
|
694
|
-
|
|
810
|
+
// `auto`, not an empty string: an empty field reads as "unset", which
|
|
811
|
+
// is indistinguishable from AUTO and is not a state this can be in.
|
|
812
|
+
maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : 'auto',
|
|
695
813
|
apiKey: '',
|
|
696
814
|
logLevel: cfg.log.level,
|
|
697
815
|
}, 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;
|
|
@@ -724,5 +842,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
724
842
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
725
843
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
726
844
|
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' }) }) }));
|
|
727
|
-
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, geometryRef: geometryRef, 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
|
|
845
|
+
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
|
|
846
|
+
? {
|
|
847
|
+
teamActive: {
|
|
848
|
+
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
849
|
+
r.phase !== 'failed' && r.phase !== 'aborted').length,
|
|
850
|
+
total: state.team.runs.length,
|
|
851
|
+
},
|
|
852
|
+
}
|
|
853
|
+
: {}) }) }));
|
|
728
854
|
}
|
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
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TeamPanel — the live subagent roster (team-subagents §6.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`; a literal here bypasses the
|
|
5
|
+
* capability probe and shows mojibake on a legacy console (`glyphs.ts` header).
|
|
6
|
+
*
|
|
7
|
+
* RENDERED ONLY WHILE A DISPATCH IS RUNNING (D-11). The requirement says "(dang
|
|
8
|
+
* you de shi hou)" — when there is one — and a permanent zero-row region would
|
|
9
|
+
* be furniture. `App` passes `null` the rest of the time and this component is
|
|
10
|
+
* not mounted at all, so a session that never delegates has zero rows and zero
|
|
11
|
+
* layout shift.
|
|
12
|
+
*
|
|
13
|
+
* IT MUST BE MOUNTED INSIDE `AppShell`'s MEASURED BOTTOM BOX (I-10). The wheel
|
|
14
|
+
* router reads the composer band boundary from `measureElement(bottomRef)`; a
|
|
15
|
+
* panel rendered outside it makes that boundary stale the moment a dispatch
|
|
16
|
+
* starts, and the wheel begins scrolling prompt history over the transcript.
|
|
17
|
+
*/
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import type { Theme } from './theme.js';
|
|
20
|
+
import type { TermCapabilities } from './capabilities.js';
|
|
21
|
+
import type { SubagentRun, TeamSnapshot } from '../team/types.js';
|
|
22
|
+
export interface TeamPanelProps {
|
|
23
|
+
snapshot: TeamSnapshot;
|
|
24
|
+
/** Terminal rows, for the one-line collapse below `panelCollapseRows`. */
|
|
25
|
+
rows: number;
|
|
26
|
+
/**
|
|
27
|
+
* Terminal columns. REQUIRED, not optional (D-13): an optional width
|
|
28
|
+
* defaulting to narrow would silently produce the degraded form forever if a
|
|
29
|
+
* call site forgot it, where a required prop fails at compile time.
|
|
30
|
+
*/
|
|
31
|
+
cols: number;
|
|
32
|
+
reducedMotion: boolean;
|
|
33
|
+
theme: Theme;
|
|
34
|
+
caps: TermCapabilities;
|
|
35
|
+
/** Injectable clock so the panel test does not depend on wall time. */
|
|
36
|
+
now?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function activityBudget(cols: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* The middle column: what this child is doing right now, in a few words.
|
|
41
|
+
*
|
|
42
|
+
* Two width policies, answering different questions. `wide` decides WHICH FACT
|
|
43
|
+
* to show - a whole path or its basename, a whole command line or its program
|
|
44
|
+
* name - mirroring `StatusBar`'s `statusCompactCols` degradation rather than
|
|
45
|
+
* inventing a second threshold. `activityBudget(cols)` decides HOW MUCH OF IT
|
|
46
|
+
* FITS, and it is a continuous function of width rather than a threshold,
|
|
47
|
+
* because the failure it prevents - a row wider than the terminal - is
|
|
48
|
+
* continuous.
|
|
49
|
+
*
|
|
50
|
+
* Exported for the same reason `buildSubagentTools` is: the width bound is the
|
|
51
|
+
* property this round has to keep, and asserting it through a rendered frame
|
|
52
|
+
* cannot distinguish "the arithmetic clamped it" from "Ink truncated it" - which
|
|
53
|
+
* is precisely the confusion that let v1's fixed-width column look fine at 100
|
|
54
|
+
* columns and break at 80.
|
|
55
|
+
*/
|
|
56
|
+
export declare function activityLine(run: SubagentRun, cols: number): string;
|
|
57
|
+
export declare function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, }: TeamPanelProps): React.ReactElement;
|