@aragon-agent/cli 0.6.39 → 0.6.40
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 +2589 -2523
- package/README.md +3706 -3614
- package/dist/agent/controller.d.ts +103 -9
- package/dist/agent/controller.js +196 -19
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +29 -1
- package/dist/agent/reducer.js +34 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +14 -2
- package/dist/agent/system-prompt.js +7 -4
- package/dist/cli.js +21 -5
- package/dist/commands/builtins.js +12 -3
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/load.js +50 -3
- package/dist/config/schema.d.ts +82 -6
- package/dist/config/schema.js +107 -9
- package/dist/config/store.js +7 -2
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/ui/App.js +14 -1
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +18 -1
- package/dist/ui/overlays/SettingsScreen.js +29 -5
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/package.json +2 -2
package/dist/tools/plan-tools.js
CHANGED
|
@@ -25,18 +25,25 @@ const NO_INTERACTIVE_USER = 'No interactive user is attached (-p/--print mode).
|
|
|
25
25
|
'state your assumptions explicitly and continue.';
|
|
26
26
|
const NO_INTERACTIVE_REVIEWER = 'No interactive reviewer is attached. Emit the full plan as your final answer ' +
|
|
27
27
|
'in markdown instead.';
|
|
28
|
-
function roundCapRefusal(used) {
|
|
29
|
-
return (`You have already asked ${used} rounds of questions (the configured maximum). ` +
|
|
30
|
-
'Proceed with your best assumptions and state them in the plan.');
|
|
31
|
-
}
|
|
32
28
|
// ---------------------------------------------------------------------------
|
|
33
29
|
// ask_user
|
|
34
30
|
// ---------------------------------------------------------------------------
|
|
35
31
|
const ASK_USER_DESCRIPTION = 'Ask the user 1-5 multiple-choice questions when a decision would change the work. ' +
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
32
|
+
'There is no limit on how many rounds you ask: call this as many times as the work ' +
|
|
33
|
+
'needs, one batch at a time. Give each question a stable id, a header of at most 12 ' +
|
|
34
|
+
'characters, and 2-4 options; exactly one option must set recommended:true, with the ' +
|
|
35
|
+
'reason in its description. Extra questions or options beyond the per-call limits are ' +
|
|
36
|
+
'dropped, so keep each round inside them. Do not ask anything you could answer by ' +
|
|
37
|
+
'reading the workspace, and never re-ask a question this turn already answered.';
|
|
38
|
+
/**
|
|
39
|
+
* The de-duplication notice, as a NON-error result.
|
|
40
|
+
*
|
|
41
|
+
* `textResult`, not `errorResult`: a tool failure invites the model to retry the
|
|
42
|
+
* same call, and its only possible outcome is the same refusal. This is the same
|
|
43
|
+
* reasoning as the cancellation path below.
|
|
44
|
+
*/
|
|
45
|
+
const DUPLICATE_ROUND_RESULT = 'You already asked this exact set of questions in this turn and the user answered. ' +
|
|
46
|
+
'Use those answers; if you need something else, ask a different question.';
|
|
40
47
|
function makeAskUser(opts) {
|
|
41
48
|
return {
|
|
42
49
|
name: 'ask_user',
|
|
@@ -91,9 +98,10 @@ function makeAskUser(opts) {
|
|
|
91
98
|
if (questions.length === 0) {
|
|
92
99
|
return errorResult('No usable questions: each needs 2-4 distinct options.');
|
|
93
100
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
// BEFORE the wait: a set the user has already answered must not open an
|
|
102
|
+
// overlay a second time. Read-only — nothing is recorded here.
|
|
103
|
+
if (opts.hasAnsweredQuestions(questions))
|
|
104
|
+
return textResult(DUPLICATE_ROUND_RESULT);
|
|
97
105
|
const response = await opts.withHumanWait(() => opts.gate.request({ kind: 'questions', questions }, ctx.signal));
|
|
98
106
|
// `null` covers cancel, unmount, abort AND timeout. From the model's point
|
|
99
107
|
// of view an unanswered question and an abandoned one are the same
|
|
@@ -102,12 +110,11 @@ function makeAskUser(opts) {
|
|
|
102
110
|
// invites a retry loop.
|
|
103
111
|
const cancelled = response === null || response.kind !== 'answers' || response.cancelled;
|
|
104
112
|
const answers = response !== null && response.kind === 'answers' ? response.answers : [];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}, questions.length));
|
|
113
|
+
// AFTER the answer, and only for a real answer. A cancelled round leaves
|
|
114
|
+
// the set askable: mis-touch must not cost the user information.
|
|
115
|
+
if (!cancelled)
|
|
116
|
+
opts.noteAnsweredQuestions(questions);
|
|
117
|
+
return textResult(formatAnswersResult({ answers, cancelled }, questions.length));
|
|
111
118
|
},
|
|
112
119
|
};
|
|
113
120
|
}
|
package/dist/ui/App.js
CHANGED
|
@@ -1014,6 +1014,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1014
1014
|
// correct value (P0-1).
|
|
1015
1015
|
dispatch({ type: 'compactionEnd', record: event.record });
|
|
1016
1016
|
break;
|
|
1017
|
+
case 'phase':
|
|
1018
|
+
// THE STAGE LINE ON THE LIVE CARD (context-compaction-never-fires C).
|
|
1019
|
+
// Dispatch is unconditional; the reducer is what ignores a phase that
|
|
1020
|
+
// arrives for a card the settle has already closed.
|
|
1021
|
+
dispatch({ type: 'compactionPhase', index: event.index, phase: event.phase });
|
|
1022
|
+
break;
|
|
1017
1023
|
case 'usage': {
|
|
1018
1024
|
// Compaction spend is REAL SPEND at the SUMMARIZER's price (§6.4 /
|
|
1019
1025
|
// AC-15), which may be neither the lead's nor the fast tier's — so the
|
|
@@ -3153,7 +3159,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
3153
3159
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
3154
3160
|
: {}), ...(acceptanceActive ? { acceptanceActive } : {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
|
|
3155
3161
|
// Manual compaction starts before its next snapshot is published.
|
|
3156
|
-
|
|
3162
|
+
// THE AUTO-PAUSED CHIP RIDES HERE AND NOWHERE ELSE (never-fires C /
|
|
3163
|
+
// analysis 3.3.1): `selfDisabled` used to live only in `/compact status`,
|
|
3164
|
+
// a command nobody types while the gauge sits at 95 %. `StatusBar` plans
|
|
3165
|
+
// the primary row from its own props, so a second copy of the flag on
|
|
3166
|
+
// `statusInput` would be a field nothing renders - and a fact with two
|
|
3167
|
+
// sources is a fact that drifts.
|
|
3168
|
+
compactionActive: { inFlight: statusInput.compacting ?? false,
|
|
3169
|
+
selfDisabled: state.compaction?.selfDisabled === true }, ...(controller.isCompactionEnabled()
|
|
3157
3170
|
? {
|
|
3158
3171
|
gaugeMarks: {
|
|
3159
3172
|
warn: cfg.compaction.warnThreshold * 100,
|
package/dist/ui/StatusBar.d.ts
CHANGED
package/dist/ui/StatusBar.js
CHANGED
|
@@ -39,6 +39,7 @@ export function StatusBar(props) {
|
|
|
39
39
|
interruptPhase: props.interruptPhase,
|
|
40
40
|
waitingForConfirmation: props.waitingForConfirmation, stopping: props.stopping,
|
|
41
41
|
compacting: props.compactionActive?.inFlight, retryActive: props.retryActive,
|
|
42
|
+
compactionPaused: props.compactionActive?.selfDisabled === true,
|
|
42
43
|
mode: props.agentMode, pendingMode: props.pendingAgentMode, servicesActive: props.servicesActive,
|
|
43
44
|
teamActive: props.teamActive, todoActive: props.todoActive,
|
|
44
45
|
...(props.acceptanceActive ? { acceptanceActive: props.acceptanceActive } : {}),
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -137,7 +137,7 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
137
137
|
case 'fast':
|
|
138
138
|
return frame(entry.status === 'failed' ? glyphs.toolError : glyphs.teamAgent, fastCardColor(entry.status, theme), _jsx(FastCard, { reviewIndex: entry.reviewIndex, model: entry.model, status: entry.status, text: entry.text, detail: entry.detail, turn: entry.turn, durationMs: entry.durationMs, live: entry.live, reducedMotion: reducedMotion, theme: theme, caps: caps }));
|
|
139
139
|
case 'compaction':
|
|
140
|
-
return frame(entry.applied || entry.live ? glyphs.compaction : glyphs.toolError, compactionCardColor(entry, theme), _jsx(CompactionCard, { index: entry.index, decision: entry.decision, memoryVersion: entry.memoryVersion, trigger: entry.trigger, mode: entry.mode, applied: entry.applied, reason: entry.reason, messagesBefore: entry.messagesBefore, messagesAfter: entry.messagesAfter, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter, summary: entry.summary, model: entry.model, durationMs: entry.durationMs, tailRelief: entry.tailRelief,
|
|
140
|
+
return frame(entry.applied || entry.live ? glyphs.compaction : glyphs.toolError, compactionCardColor(entry, theme), _jsx(CompactionCard, { index: entry.index, decision: entry.decision, memoryVersion: entry.memoryVersion, trigger: entry.trigger, mode: entry.mode, applied: entry.applied, reason: entry.reason, messagesBefore: entry.messagesBefore, messagesAfter: entry.messagesAfter, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter, summary: entry.summary, model: entry.model, durationMs: entry.durationMs, tailRelief: entry.tailRelief, phase: entry.phase, keepRelaxed: entry.keepRelaxed, retainedTurns: entry.retainedTurns, retainedMessages: entry.retainedMessages, keepRecentTurnsUsed: entry.keepRecentTurnsUsed, elidedMessages: entry.elidedMessages,
|
|
141
141
|
// ONLY WHILE LIVE, and only when the clock has ticked at least once:
|
|
142
142
|
// `nowSec` is `undefined` until then, and a settled card must keep
|
|
143
143
|
// rendering `durationMs` rather than a figure that would drift.
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import React from 'react';
|
|
24
24
|
import type { Theme } from '../theme.js';
|
|
25
25
|
import type { TermCapabilities } from '../capabilities.js';
|
|
26
|
-
import type { CompactionDecision, CompactionMode, CompactionUiTrigger } from '../../compaction/types.js';
|
|
26
|
+
import type { CompactionDecision, CompactionMode, CompactionPhase, CompactionUiTrigger } from '../../compaction/types.js';
|
|
27
27
|
export interface CompactionCardProps {
|
|
28
28
|
decision?: CompactionDecision;
|
|
29
29
|
memoryVersion?: 2;
|
|
@@ -39,6 +39,16 @@ export interface CompactionCardProps {
|
|
|
39
39
|
summary?: string;
|
|
40
40
|
model: string;
|
|
41
41
|
durationMs?: number;
|
|
42
|
+
/** The live card's stage, so "compacting" also says WHAT (never-fires C). */
|
|
43
|
+
phase?: CompactionPhase;
|
|
44
|
+
/** Set when this cut existed only because `keepRecentTurns` was relaxed (B). */
|
|
45
|
+
keepRelaxed?: boolean;
|
|
46
|
+
retainedTurns?: number;
|
|
47
|
+
/** Messages the relaxed cut left verbatim, which is not the same as turns. */
|
|
48
|
+
retainedMessages?: number;
|
|
49
|
+
keepRecentTurnsUsed?: number;
|
|
50
|
+
/** Messages the digest left out and therefore cannot be quoted (A1). */
|
|
51
|
+
elidedMessages?: number;
|
|
42
52
|
/**
|
|
43
53
|
* Wall-clock since the card opened, LIVE ONLY
|
|
44
54
|
* (context-auto-compaction-hardening §3.6 / W5).
|
|
@@ -28,6 +28,7 @@ import { pickGlyphs } from '../glyphs.js';
|
|
|
28
28
|
import { railBorderProps } from '../layout/Gutter.js';
|
|
29
29
|
import { formatDuration, formatTokens } from '../../agent/usage.js';
|
|
30
30
|
import { COMPACTION_LIMITS } from '../../compaction/limits.js';
|
|
31
|
+
import { compactionPhaseLabel, compactionReasonLine } from '../../compaction/reason-text.js';
|
|
31
32
|
/**
|
|
32
33
|
* The card's colour.
|
|
33
34
|
*
|
|
@@ -56,12 +57,19 @@ function headline(props, arrow, dot) {
|
|
|
56
57
|
// second 45 this was the only changing information on the card, and there was
|
|
57
58
|
// none.
|
|
58
59
|
const elapsed = props.elapsedMs === undefined ? '' : `${dot}${formatDuration(props.elapsedMs)}`;
|
|
60
|
+
// THE STAGE, NOT JUST "COMPACTING" (never-fires C). Planning and digesting run
|
|
61
|
+
// before any model call, and a user watching a card for twenty seconds cannot
|
|
62
|
+
// tell "thinking hard" from "stuck in a gate" without it.
|
|
63
|
+
const stage = props.phase === undefined ? '' : `${dot}${compactionPhaseLabel(props.phase)}`;
|
|
59
64
|
return props.model
|
|
60
|
-
? `compacting context #${index}${dot}${props.model}${elapsed}`
|
|
61
|
-
: `compacting context #${index}${elapsed}`;
|
|
65
|
+
? `compacting context #${index}${dot}${props.model}${stage}${elapsed}`
|
|
66
|
+
: `compacting context #${index}${stage}${elapsed}`;
|
|
62
67
|
}
|
|
63
68
|
if (!props.applied || props.tokensAfter >= props.tokensBefore) {
|
|
64
|
-
|
|
69
|
+
// THE REASON IN WORDS (never-fires C). `digest_budget_exceeded: $.digest` was
|
|
70
|
+
// a code the user could not act on; `compactionReasonLine` renders the same
|
|
71
|
+
// fact as a clause plus, where there is one, the remedy.
|
|
72
|
+
const why = props.reason ? `: ${compactionReasonLine(props.reason)}` : '';
|
|
65
73
|
return `context not compacted #${index}${why}`;
|
|
66
74
|
}
|
|
67
75
|
const messages = `${props.messagesBefore} ${arrow} ${props.messagesAfter} messages`;
|
|
@@ -87,8 +95,15 @@ function headline(props, arrow, dot) {
|
|
|
87
95
|
*/
|
|
88
96
|
function detailLine(props) {
|
|
89
97
|
if (props.live) {
|
|
98
|
+
// THE ESCAPE, NOT THE STAGE (never-fires C, as corrected in review): the live
|
|
99
|
+
// headline already carries the stage, and a two-line card that names it twice
|
|
100
|
+
// is the one-fact-one-surface rule this file keeps having to restore. With no
|
|
101
|
+
// model resolved there is no call to cancel, so the whole line goes - the same
|
|
102
|
+
// rule the settled branch applies.
|
|
90
103
|
if (!props.model)
|
|
91
104
|
return '';
|
|
105
|
+
if (props.phase !== undefined)
|
|
106
|
+
return 'esc to cancel';
|
|
92
107
|
return `summarizing with ${props.model} - esc to cancel`;
|
|
93
108
|
}
|
|
94
109
|
if (!props.applied || props.tokensAfter >= props.tokensBefore)
|
|
@@ -104,6 +119,50 @@ function detailLine(props) {
|
|
|
104
119
|
const via = props.trigger === 'overflow' ? ' after a context overflow' : '';
|
|
105
120
|
return `summarized ${dropped} messages with ${props.model}${via}`;
|
|
106
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The relaxation disclosure (never-fires B).
|
|
124
|
+
*
|
|
125
|
+
* `planCompaction`'s second pass trades "the last N user turns are verbatim" for
|
|
126
|
+
* "a cut exists at all". That trade must be readable, because the difference
|
|
127
|
+
* between the two compactions is exactly what the user asked for: the tail is the
|
|
128
|
+
* part they care most about keeping word for word.
|
|
129
|
+
*/
|
|
130
|
+
function relaxedLine(props) {
|
|
131
|
+
if (props.live || !props.applied || props.keepRelaxed !== true)
|
|
132
|
+
return '';
|
|
133
|
+
const turns = props.retainedTurns ?? 0;
|
|
134
|
+
const wanted = props.keepRecentTurnsUsed;
|
|
135
|
+
const messages = props.retainedMessages;
|
|
136
|
+
// THE TAIL IS MESSAGES, NOT TURNS (review). The dominant relaxed shape is one
|
|
137
|
+
// prompt plus a hundred tool calls, so `retainedTurns` is 0 while forty messages
|
|
138
|
+
// were kept verbatim: a line reading "kept 0 recent turns" would report a total
|
|
139
|
+
// loss that did not happen. Whenever the count is available it leads, and the
|
|
140
|
+
// turn arithmetic follows as the explanation rather than as the headline.
|
|
141
|
+
if (messages !== undefined && wanted !== undefined) {
|
|
142
|
+
return `kept the last ${messages} messages - ${turns} of the ${wanted} requested turns, ` +
|
|
143
|
+
'since this run had too few user turns';
|
|
144
|
+
}
|
|
145
|
+
if (messages !== undefined)
|
|
146
|
+
return `kept the last ${messages} messages verbatim`;
|
|
147
|
+
return wanted === undefined
|
|
148
|
+
? `kept the last ${turns} turns - this run had too few user turns to keep more`
|
|
149
|
+
: `kept ${turns} of the ${wanted} requested turns - this run had too few user turns`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The digest-elision disclosure (never-fires A1).
|
|
153
|
+
*
|
|
154
|
+
* The digest never rejects for length any more; it keeps the first and last
|
|
155
|
+
* entries verbatim and elides the middle. The elided messages cannot be quoted by
|
|
156
|
+
* the summarizer, so the card says so rather than presenting a summary as if it
|
|
157
|
+
* had seen everything.
|
|
158
|
+
*/
|
|
159
|
+
function elidedLine(props) {
|
|
160
|
+
const n = props.elidedMessages ?? 0;
|
|
161
|
+
if (props.live || n <= 0)
|
|
162
|
+
return '';
|
|
163
|
+
return `elided ${n} ${n === 1 ? 'message' : 'messages'} from the summary input - ` +
|
|
164
|
+
'the middle is summarized without them';
|
|
165
|
+
}
|
|
107
166
|
/**
|
|
108
167
|
* The relief disclosure, on a settled card that ALSO summarized.
|
|
109
168
|
*
|
|
@@ -139,6 +198,8 @@ function CompactionCardImpl(props) {
|
|
|
139
198
|
const head = headline(props, glyphs.arrowRight, dot);
|
|
140
199
|
const detail = detailLine(props);
|
|
141
200
|
const relief = reliefLine(props);
|
|
201
|
+
const relaxed = relaxedLine(props);
|
|
202
|
+
const elided = elidedLine(props);
|
|
142
203
|
const showMemory = props.memoryVersion !== 2 || expanded === true;
|
|
143
204
|
const rows = summary && showMemory ? summaryRows(summary, expanded === true) : [];
|
|
144
205
|
const totalRows = summary ? summary.split('\n').filter((l) => l.trim().length > 0).length : 0;
|
|
@@ -150,7 +211,7 @@ function CompactionCardImpl(props) {
|
|
|
150
211
|
// the whole run, losing the status colour along with the motion.
|
|
151
212
|
const animate = live && reducedMotion !== true && caps.unicode;
|
|
152
213
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.accent, bold: true, children: props.trigger === 'manual' ? 'Manual compaction' : props.trigger === 'overflow'
|
|
153
|
-
? 'Overflow: threshold reached' : 'Automatic: threshold reached' }), _jsxs(Text, { color: live ? color : theme.muted, wrap: caps.unicode ? 'truncate' : 'wrap', children: [animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), props.memoryVersion === 2 && (_jsxs(Text, { color: theme.muted, children: ["Task memory v2", expanded ? '' : ' (ctrl+o to expand)'] })), detail.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: caps.unicode ? 'truncate' : 'wrap', children: detail }) })), relief.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.noticeWarn, children: relief }) })), rows.length > 0 && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [rows.map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))), hidden > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " +", hidden, " more lines (ctrl+o to expand)"] })), hidden === 0 && expanded === true && totalRows > COMPACTION_LIMITS.cardTextRows && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " ctrl+o to collapse"] }))] }))] }));
|
|
214
|
+
? 'Overflow: threshold reached' : 'Automatic: threshold reached' }), _jsxs(Text, { color: live ? color : theme.muted, wrap: caps.unicode ? 'truncate' : 'wrap', children: [animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), props.memoryVersion === 2 && (_jsxs(Text, { color: theme.muted, children: ["Task memory v2", expanded ? '' : ' (ctrl+o to expand)'] })), detail.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: caps.unicode ? 'truncate' : 'wrap', children: detail }) })), relief.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.noticeWarn, children: relief }) })), relaxed.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.muted, children: relaxed }) })), elided.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.muted, children: elided }) })), rows.length > 0 && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [rows.map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))), hidden > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " +", hidden, " more lines (ctrl+o to expand)"] })), hidden === 0 && expanded === true && totalRows > COMPACTION_LIMITS.cardTextRows && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " ctrl+o to collapse"] }))] }))] }));
|
|
154
215
|
}
|
|
155
216
|
/**
|
|
156
217
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
@@ -31,6 +31,16 @@ export interface StatusLayoutInput {
|
|
|
31
31
|
spinner?: string;
|
|
32
32
|
waitingForConfirmation?: boolean;
|
|
33
33
|
stopping?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Auto-compaction paused itself after repeated no-progress attempts
|
|
36
|
+
* (context-compaction-never-fires C).
|
|
37
|
+
*
|
|
38
|
+
* A SEPARATE FIELD FROM `compacting`, and it renders as a SUFFIX on the context
|
|
39
|
+
* gauge rather than as a phase word: the fact belongs next to the percentage it
|
|
40
|
+
* explains ("Context 95% auto-paused"), and the phase slot must keep answering
|
|
41
|
+
* "is the agent running".
|
|
42
|
+
*/
|
|
43
|
+
compactionPaused?: boolean;
|
|
34
44
|
compacting?: boolean;
|
|
35
45
|
retryActive?: {
|
|
36
46
|
attempt: number;
|
|
@@ -53,6 +53,22 @@ function contextPercent(context) {
|
|
|
53
53
|
const mark = context.source === 'estimate' || context.deltaTokens > 0 ? '~' : '';
|
|
54
54
|
return mark + (context.pct > 999 ? '>999' : Math.round(context.pct)) + '%';
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The words that ride on the gauge when auto-compaction has given up (C).
|
|
58
|
+
*
|
|
59
|
+
* IT RIDES ON THE GAUGE RATHER THAN OCCUPYING ITS OWN FIELD. "Auto-paused" is the
|
|
60
|
+
* only fact that explains a stuck percentage, so the two belong in one field for
|
|
61
|
+
* the same reason `/context` prints them on adjacent lines. A second field would
|
|
62
|
+
* also be the first thing the width ladder drops, and this is precisely the state
|
|
63
|
+
* that must survive a narrow terminal.
|
|
64
|
+
*
|
|
65
|
+
* `paused` at the tight rungs: the full phrase costs twelve cells the row does
|
|
66
|
+
* not have at rung 3 and beyond, and dropping the fact entirely would be worse
|
|
67
|
+
* than abbreviating it.
|
|
68
|
+
*/
|
|
69
|
+
function pausedSuffix(full) {
|
|
70
|
+
return full ? ' auto-paused' : ' paused';
|
|
71
|
+
}
|
|
56
72
|
export function shortDuration(ms) {
|
|
57
73
|
if (!Number.isFinite(ms) || ms <= 0)
|
|
58
74
|
return '0s';
|
|
@@ -208,7 +224,8 @@ export function planPrimaryStatusFields(input) {
|
|
|
208
224
|
statusField('phase', glyph + (rung >= 5 ? '' : ' ') + label, phaseTone),
|
|
209
225
|
...(notice ? [statusField('feedback', notice, feedbackTone)] : []),
|
|
210
226
|
...(modeText ? [statusField('mode', modeText, 'accent')] : []),
|
|
211
|
-
statusField('context', (full ? 'Context ' : 'C') + pct
|
|
227
|
+
statusField('context', (full ? 'Context ' : 'C') + pct
|
|
228
|
+
+ (input.compactionPaused === true ? pausedSuffix(full) : '')),
|
|
212
229
|
statusField('thinking', full ? 'Think ' + (thinkingCodes[input.thinkingLevel] ? input.thinkingLevel : '?')
|
|
213
230
|
: (rung >= 2 ? '' : 'Th:') + code),
|
|
214
231
|
statusField('speed', speed + (full ? ' tok/s' : rung >= 3 ? '/s' : 't/s')),
|
|
@@ -27,6 +27,8 @@ import { canProbe, editorProbeTarget, visibleProbeViews } from './model-probe-vi
|
|
|
27
27
|
import { createModelProfileState, createCustomProfile, modelProfileReducer, profileUsers, draftTeam, draftSubagentProfileId, subagentModelSummary, subagentSelectedId, SUBAGENT_FAST_ROW_ID } from './model-profile-state.js';
|
|
28
28
|
import { KitEditor } from './KitEditor.js';
|
|
29
29
|
import { KitManager } from './KitManager.js';
|
|
30
|
+
import { TeamFanoutPage } from './TeamFanoutPage.js';
|
|
31
|
+
import { fanoutRows, fanoutSummary, teamFanoutValues } from './team-fanout-state.js';
|
|
30
32
|
import { heldSlots, kitManagerRows, kitPackRow, kitReducer } from './kit-settings-state.js';
|
|
31
33
|
/**
|
|
32
34
|
* THE ROOT PAGE'S ROW INDEXES, AS NAMES (subagent-model V-5).
|
|
@@ -40,10 +42,15 @@ import { heldSlots, kitManagerRows, kitPackRow, kitReducer } from './kit-setting
|
|
|
40
42
|
* fifth of these and the "five offsets agreed by accident" defect does not need
|
|
41
43
|
* a sequel. Order here IS the render order.
|
|
42
44
|
*/
|
|
43
|
-
const ROOT_ROWS = ['main', 'fast', 'subagent', 'kit', 'manage'];
|
|
45
|
+
const ROOT_ROWS = ['main', 'fast', 'subagent', 'team', 'kit', 'manage'];
|
|
44
46
|
const ROOT_MAIN = ROOT_ROWS.indexOf('main');
|
|
45
47
|
const ROOT_FAST = ROOT_ROWS.indexOf('fast');
|
|
46
48
|
const ROOT_SUBAGENT = ROOT_ROWS.indexOf('subagent');
|
|
49
|
+
// The fan-out page (subagent-fanout D-5). It is FOURTH, directly under the model
|
|
50
|
+
// row it complements: "which model children run on" and "how many children" are
|
|
51
|
+
// one decision, and splitting them across the page would make the pair harder to
|
|
52
|
+
// keep consistent than the pair of numbers inside it.
|
|
53
|
+
const ROOT_TEAM = ROOT_ROWS.indexOf('team');
|
|
47
54
|
const ROOT_KIT = ROOT_ROWS.indexOf('kit');
|
|
48
55
|
const ROOT_MANAGE = ROOT_ROWS.indexOf('manage');
|
|
49
56
|
const ROOT_FIRST_FIELD = ROOT_ROWS.length;
|
|
@@ -583,9 +590,9 @@ function useProfileSettingsState(props) {
|
|
|
583
590
|
&& ['provider', 'model', 'baseUrl', 'contextWindow', 'apiKey'].includes(item.key)),
|
|
584
591
|
...FIELDS.filter((item) => library.fastId === null
|
|
585
592
|
&& ['fastModel', 'fastProvider'].includes(item.key)), ...PROFILE_FIELDS];
|
|
586
|
-
//
|
|
587
|
-
// around the `fields` block - derived from ROOT_ROWS so
|
|
588
|
-
// offsets cannot drift apart (see those constants).
|
|
593
|
+
// EIGHT FIXED ROWS now (Main, Fast, Subagent model, Subagent fan-out, Kit pack,
|
|
594
|
+
// Manage, Save, Reload) around the `fields` block - derived from ROOT_ROWS so
|
|
595
|
+
// the count and the offsets cannot drift apart (see those constants).
|
|
589
596
|
const count = fields.length + ROOT_ROWS.length + 2;
|
|
590
597
|
const field = fields[index - ROOT_FIRST_FIELD];
|
|
591
598
|
const readError = state.draft.readError;
|
|
@@ -679,8 +686,15 @@ function activateRootSetting(props, view) {
|
|
|
679
686
|
// `draft.patch.team` rather than a `${role}Id` library key.
|
|
680
687
|
dispatch({ type: 'page', page: 'picker', role: 'subagent' });
|
|
681
688
|
}
|
|
689
|
+
else if (index === ROOT_TEAM) {
|
|
690
|
+
// MUST STAY ABOVE THE FALLBACK `else` BELOW, WHICH IS `saveProfileSettings`
|
|
691
|
+
// (R-P1-8). A missing or mis-derived constant here does not fail to compile:
|
|
692
|
+
// pressing Enter on this row would silently COMMIT the whole draft, so
|
|
693
|
+
// `team-fanout-page.test.tsx` asserts that Enter on this row saves nothing.
|
|
694
|
+
dispatch({ type: 'page', page: 'team' });
|
|
695
|
+
}
|
|
682
696
|
else if (index === ROOT_KIT) {
|
|
683
|
-
// The
|
|
697
|
+
// The fifth root row opens the manager, not a picker: kits are data the user
|
|
684
698
|
// owns here, and `ctrl+K` is where they are CONSUMED (agent-model-kits §4.5).
|
|
685
699
|
dispatch({ type: 'page', page: 'kits' });
|
|
686
700
|
}
|
|
@@ -836,6 +850,11 @@ function fastTierUnavailable(props, view) {
|
|
|
836
850
|
}
|
|
837
851
|
function renderProfileSubpage(props, view) {
|
|
838
852
|
const { state, library, dispatch, setConfirmIndex } = view;
|
|
853
|
+
// THE FAN-OUT PAGE (subagent-fanout 3.4.4). Placed before the picker branch
|
|
854
|
+
// because that branch is the unconditional fallback; the page name is unique,
|
|
855
|
+
// so its position among the others is arbitrary.
|
|
856
|
+
if (state.page === 'team')
|
|
857
|
+
return _jsx(TeamFanoutPage, { ...props, view: { rows: fanoutRows(teamFanoutValues(state.draft)), error: state.error }, dispatch: dispatch, onBack: () => dispatch({ type: 'page', page: 'root' }) });
|
|
839
858
|
// THE KIT PAGES (agent-model-kits §4.5). They are dispatched BEFORE the profile
|
|
840
859
|
// pages because `'kit-slot'` RENDERS a profile picker while belonging to the
|
|
841
860
|
// kit editor - the page name, not the component, decides where the user is.
|
|
@@ -945,9 +964,14 @@ function buildRootSettingsRows(props, view) {
|
|
|
945
964
|
profiles: state.draft.profiles,
|
|
946
965
|
hasKey: (id) => (props.apiKeys[id] ?? '').length > 0 })}${kitTag}`
|
|
947
966
|
: subagentLine;
|
|
967
|
+
// THE FAN-OUT SUMMARY (subagent-fanout 3.4.4): a DERIVED row, like the three
|
|
968
|
+
// above it, and deliberately not a control - the numbers are adjusted on the
|
|
969
|
+
// page Enter opens, so the root row cannot change a value by accident.
|
|
970
|
+
const fanout = fanoutSummary(teamFanoutValues(state.draft));
|
|
948
971
|
const lines = [`Main profile ${mainValue}`,
|
|
949
972
|
`Fast profile ${fastValue}`,
|
|
950
973
|
`Subagent model ${subagentValue}`,
|
|
974
|
+
`Subagent fan-out ${fanout}`,
|
|
951
975
|
`Kit pack ${kitPackRow(state)}`,
|
|
952
976
|
`Manage profiles ${library.entries.length} saved`,
|
|
953
977
|
...fields.map((item) => `${item.label.padEnd(18)}${item.key === 'apiKey'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings > Team fan-out: three arrows-adjustable rows (subagent-fanout 3.4.3).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* THE PAGE OWNS ONE PIECE OF STATE: its cursor. Every value comes from
|
|
7
|
+
* `props.view.rows`, which `SettingsScreen` derives from the reducer state - so
|
|
8
|
+
* this page cannot hold a second copy of the draft and then disagree with the
|
|
9
|
+
* root page about what is about to be saved (the discipline
|
|
10
|
+
* `model-profile-state.ts` records at its own head).
|
|
11
|
+
*
|
|
12
|
+
* IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13).
|
|
13
|
+
* `useRootSettingsInput` gates its `useInput` on `state.page === 'root'`, so a
|
|
14
|
+
* subpage has no save listener at all - the kit page included. A footer
|
|
15
|
+
* promising "Ctrl+S saves here" would therefore be a lie the user acts on. The
|
|
16
|
+
* honest instruction is the one already in their fingers: Esc back, Ctrl+S on the
|
|
17
|
+
* main page.
|
|
18
|
+
*/
|
|
19
|
+
import React from 'react';
|
|
20
|
+
import { type ProfileFrameProps } from './ModelProfilePicker.js';
|
|
21
|
+
import type { TeamFanoutField, TeamFanoutView } from './team-fanout-state.js';
|
|
22
|
+
/** What the page may ask the screen to change. */
|
|
23
|
+
export interface TeamFanoutAction {
|
|
24
|
+
type: 'team-fanout';
|
|
25
|
+
field: TeamFanoutField;
|
|
26
|
+
delta: -1 | 1;
|
|
27
|
+
}
|
|
28
|
+
interface TeamFanoutPageProps extends ProfileFrameProps {
|
|
29
|
+
view: TeamFanoutView;
|
|
30
|
+
dispatch: (action: TeamFanoutAction) => void;
|
|
31
|
+
/** Esc: back to the settings root. The draft is NOT discarded here. */
|
|
32
|
+
onBack: () => void;
|
|
33
|
+
}
|
|
34
|
+
export declare function TeamFanoutPage(props: TeamFanoutPageProps): React.ReactElement;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Settings > Team fan-out: three arrows-adjustable rows (subagent-fanout 3.4.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* THE PAGE OWNS ONE PIECE OF STATE: its cursor. Every value comes from
|
|
8
|
+
* `props.view.rows`, which `SettingsScreen` derives from the reducer state - so
|
|
9
|
+
* this page cannot hold a second copy of the draft and then disagree with the
|
|
10
|
+
* root page about what is about to be saved (the discipline
|
|
11
|
+
* `model-profile-state.ts` records at its own head).
|
|
12
|
+
*
|
|
13
|
+
* IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13).
|
|
14
|
+
* `useRootSettingsInput` gates its `useInput` on `state.page === 'root'`, so a
|
|
15
|
+
* subpage has no save listener at all - the kit page included. A footer
|
|
16
|
+
* promising "Ctrl+S saves here" would therefore be a lie the user acts on. The
|
|
17
|
+
* honest instruction is the one already in their fingers: Esc back, Ctrl+S on the
|
|
18
|
+
* main page.
|
|
19
|
+
*/
|
|
20
|
+
import { useState } from 'react';
|
|
21
|
+
import { Text, useInput } from 'ink';
|
|
22
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
23
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
24
|
+
import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
|
|
25
|
+
/**
|
|
26
|
+
* Label column, matching the root page's `padEnd(18)` rows above and below this
|
|
27
|
+
* one. `Subagent fan-out` is 16 characters, so two spaces of padding are all it
|
|
28
|
+
* needs to line up with `Subagent model` - measured, not guessed.
|
|
29
|
+
*/
|
|
30
|
+
const LABEL_PAD = 18;
|
|
31
|
+
export function TeamFanoutPage(props) {
|
|
32
|
+
const [index, setIndex] = useState(0);
|
|
33
|
+
const rows = props.view.rows;
|
|
34
|
+
const count = Math.max(1, rows.length);
|
|
35
|
+
const cursor = Math.min(index, count - 1);
|
|
36
|
+
const focused = rows[cursor];
|
|
37
|
+
const offset = useProfileScroll(props, cursor);
|
|
38
|
+
const nudge = (delta) => {
|
|
39
|
+
if (!focused)
|
|
40
|
+
return;
|
|
41
|
+
props.dispatch({ type: 'team-fanout', field: focused.field, delta });
|
|
42
|
+
};
|
|
43
|
+
useInput((_input, key) => {
|
|
44
|
+
if (props.isActive === false)
|
|
45
|
+
return;
|
|
46
|
+
if (key.escape) {
|
|
47
|
+
props.onBack();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
51
|
+
setIndex((cursor - 1 + count) % count);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.downArrow || key.tab) {
|
|
55
|
+
setIndex((cursor + 1) % count);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.leftArrow || key.rightArrow) {
|
|
59
|
+
nudge(key.leftArrow ? -1 : 1);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// `Enter` toggles the Supervisor row and does NOTHING on a numeric row:
|
|
63
|
+
// a stray Return that silently changed a number is indistinguishable from a
|
|
64
|
+
// bug, and the arrow keys are already the obvious control.
|
|
65
|
+
if (key.return && focused?.field === 'overseer')
|
|
66
|
+
nudge(1);
|
|
67
|
+
});
|
|
68
|
+
const lines = rows.map((row) => `${row.label.padEnd(LABEL_PAD)}${row.value}`);
|
|
69
|
+
const glyphs = pickGlyphs(props.caps);
|
|
70
|
+
const body = lines.map((line, at) => (_jsx(Text, { wrap: "truncate", color: at === cursor ? props.theme.accent : props.theme.muted, children: `${at === cursor ? glyphs.caret : ' '} ${clipProfileText(line, props.cols - 8)}` }, at)));
|
|
71
|
+
// The hint explains the FOCUSED row rather than decorating the frame: three
|
|
72
|
+
// numbers whose meaning is not obvious ("per dispatch" versus "at once") are
|
|
73
|
+
// the reason this page exists at all.
|
|
74
|
+
if (focused) {
|
|
75
|
+
body.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: clipProfileText(focused.hint, props.cols - 8) }, "hint"));
|
|
76
|
+
}
|
|
77
|
+
return _jsx(OverlayFrame, { ...props, rows: body, scrollOffset: offset, title: props.view.error ?? 'Team fan-out', hint: "Arrows adjust. Esc goes back; Ctrl+S on the main page saves." });
|
|
78
|
+
}
|
|
@@ -3,7 +3,14 @@ import type { AgentKit } from '../../config/model-kits.js';
|
|
|
3
3
|
import type { ModelSettingsDraft } from '../../config/model-profile-store.js';
|
|
4
4
|
import type { SubagentModelMode } from '../../team/model.js';
|
|
5
5
|
import type { KitDraft } from './kit-settings-state.js';
|
|
6
|
-
|
|
6
|
+
import { type TeamFanoutField } from './team-fanout-state.js';
|
|
7
|
+
export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' | 'delete-confirm' | 'discard-confirm' | 'kits' | 'kit-edit' | 'kit-slot' | 'kit-delete' | 'team';
|
|
8
|
+
/**
|
|
9
|
+
* `'team'` is the fan-out page (subagent-fanout D-5), and it is a PAGE rather
|
|
10
|
+
* than three more fields on the root page for a measurable reason: the root page
|
|
11
|
+
* already scrolls on a 24-row terminal, and three arrows-adjustable rows would
|
|
12
|
+
* push `Save` and `Reload` off the first screen on every machine.
|
|
13
|
+
*/
|
|
7
14
|
/**
|
|
8
15
|
* Which role a PROFILE PICKER page is choosing for.
|
|
9
16
|
*
|
|
@@ -100,6 +107,17 @@ export type ModelProfileAction = {
|
|
|
100
107
|
} | {
|
|
101
108
|
type: 'reload';
|
|
102
109
|
draft: ModelSettingsDraft;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* One arrow press on the fan-out page (subagent-fanout 3.4.2).
|
|
113
|
+
*
|
|
114
|
+
* The reducer, not the page, owns the write: the page holds a cursor and
|
|
115
|
+
* nothing else, so there is exactly one place a draft can change.
|
|
116
|
+
*/
|
|
117
|
+
| {
|
|
118
|
+
type: 'team-fanout';
|
|
119
|
+
field: TeamFanoutField;
|
|
120
|
+
delta: -1 | 1;
|
|
103
121
|
};
|
|
104
122
|
/** Keep the original secret separate from the empty replacement field. */
|
|
105
123
|
export declare function createProfileEditor(profile: ModelProfile): ProfileEditorDraft;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { emptyModelProfiles, duplicateModelProfile, normalizeModelProfiles, validateModelProfiles } from '../../config/model-profiles.js';
|
|
3
3
|
import { parseContextWindowInput } from '../../config/schema.js';
|
|
4
|
+
import { stepFanoutValue, teamFanoutValues } from './team-fanout-state.js';
|
|
4
5
|
/** The picker's built-in `Fast tier model` row; never a profile id on disk. */
|
|
5
6
|
export const SUBAGENT_FAST_ROW_ID = '__fast__';
|
|
6
7
|
/** Keep the original secret separate from the empty replacement field. */
|
|
@@ -221,6 +222,17 @@ export function modelProfileReducer(state, action) {
|
|
|
221
222
|
}
|
|
222
223
|
case 'cancel-editor': return { ...state, editor: undefined, page: state.returnPage,
|
|
223
224
|
error: undefined };
|
|
225
|
+
case 'team-fanout': {
|
|
226
|
+
// THE FRAGMENT IS THE ONLY THING WRITTEN (D-17 / R-P0-1). `stepFanoutValue`
|
|
227
|
+
// returns `Partial<TeamFanoutPatch>`, so `model` and `modelProfileId` - the
|
|
228
|
+
// two keys `releasedSlots` reads as "the user re-decided the subagent slot" -
|
|
229
|
+
// cannot enter the patch from here. `teamFanoutValues` is the CLAMPED view
|
|
230
|
+
// (R-P0-2), so a step starts from the number the user is looking at.
|
|
231
|
+
const step = stepFanoutValue(teamFanoutValues(state.draft), action.field, action.delta);
|
|
232
|
+
return { ...state, dirty: true, error: undefined,
|
|
233
|
+
draft: { ...state.draft,
|
|
234
|
+
patch: { ...state.draft.patch, team: { ...state.draft.patch.team, ...step } } } };
|
|
235
|
+
}
|
|
224
236
|
case 'duplicate': {
|
|
225
237
|
if (library.entries.length >= 200)
|
|
226
238
|
return { ...state,
|