@aragon-agent/cli 0.6.34 → 0.6.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3071 -3007
- package/dist/agent/controller.d.ts +29 -0
- package/dist/agent/controller.js +42 -1
- package/dist/agent/kit-apply.d.ts +56 -0
- package/dist/agent/kit-apply.js +209 -0
- package/dist/agent/model-profile-settings.d.ts +28 -0
- package/dist/agent/model-profile-settings.js +142 -29
- package/dist/agent/reducer.d.ts +1 -1
- package/dist/commands/builtins.js +4 -0
- package/dist/commands/kit-command.d.ts +20 -0
- package/dist/commands/kit-command.js +131 -0
- package/dist/config/kit-resolution.d.ts +93 -0
- package/dist/config/kit-resolution.js +226 -0
- package/dist/config/model-kits.d.ts +224 -0
- package/dist/config/model-kits.js +302 -0
- package/dist/config/model-profile-store.d.ts +25 -0
- package/dist/config/model-profiles.d.ts +41 -3
- package/dist/config/model-profiles.js +46 -5
- package/dist/config/schema.d.ts +16 -0
- package/dist/input/keymap.js +18 -4
- package/dist/ui/App.js +108 -12
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +8 -2
- package/dist/ui/PromptInput.js +1 -1
- package/dist/ui/ScrollbackShell.d.ts +18 -3
- package/dist/ui/ScrollbackShell.js +1 -1
- package/dist/ui/StatusBar.d.ts +13 -0
- package/dist/ui/StatusBar.js +10 -3
- package/dist/ui/TeamPanel.d.ts +12 -0
- package/dist/ui/TeamPanel.js +10 -31
- package/dist/ui/Transcript.js +22 -2
- package/dist/ui/entries/TeamCard.d.ts +18 -2
- package/dist/ui/entries/TeamCard.js +47 -13
- package/dist/ui/layout/header-hints.d.ts +11 -1
- package/dist/ui/layout/header-hints.js +35 -3
- package/dist/ui/layout/kit-chip.d.ts +50 -0
- package/dist/ui/layout/kit-chip.js +81 -0
- package/dist/ui/layout/virtual-window.js +9 -0
- package/dist/ui/overlays/HelpOverlay.js +7 -0
- package/dist/ui/overlays/KitEditor.d.ts +23 -0
- package/dist/ui/overlays/KitEditor.js +101 -0
- package/dist/ui/overlays/KitManager.d.ts +23 -0
- package/dist/ui/overlays/KitManager.js +104 -0
- package/dist/ui/overlays/KitPicker.d.ts +49 -0
- package/dist/ui/overlays/KitPicker.js +58 -0
- package/dist/ui/overlays/ModelProfilePicker.js +16 -6
- package/dist/ui/overlays/SettingsScreen.js +139 -16
- package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
- package/dist/ui/overlays/kit-picker-state.js +136 -0
- package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
- package/dist/ui/overlays/kit-settings-state.js +267 -0
- package/dist/ui/overlays/model-profile-state.d.ts +26 -3
- package/dist/ui/overlays/model-profile-state.js +59 -6
- package/dist/ui/scrollback/LiveTail.d.ts +2 -2
- package/dist/ui/scrollback/LiveTail.js +7 -2
- package/dist/ui/scrollback/live-preview.js +10 -3
- package/dist/ui/scrollback/tail-window.d.ts +12 -0
- package/dist/ui/scrollback/tail-window.js +17 -2
- package/dist/ui/scrollback/use-scrollback-view.js +7 -0
- package/dist/ui/team-status.d.ts +141 -0
- package/dist/ui/team-status.js +224 -0
- package/dist/ui/transcript-text.js +13 -4
- package/dist/ui/use-wheel-routing.js +2 -2
- package/package.json +2 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type ModelProfile, type ModelProfilesConfig, type ModelRole } from '../../config/model-profiles.js';
|
|
2
|
+
import type { AgentKit } from '../../config/model-kits.js';
|
|
2
3
|
import type { ModelSettingsDraft } from '../../config/model-profile-store.js';
|
|
3
4
|
import type { SubagentModelMode } from '../../team/model.js';
|
|
4
|
-
|
|
5
|
+
import type { KitDraft } from './kit-settings-state.js';
|
|
6
|
+
export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' | 'delete-confirm' | 'discard-confirm' | 'kits' | 'kit-edit' | 'kit-slot' | 'kit-delete';
|
|
5
7
|
/**
|
|
6
8
|
* Which role a PROFILE PICKER page is choosing for.
|
|
7
9
|
*
|
|
@@ -9,8 +11,13 @@ export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' |
|
|
|
9
11
|
* `'main' | 'fast'` because it is the type basis of the config layer's
|
|
10
12
|
* `${role}Id` template keys - widening it would invent a `subagentId` library
|
|
11
13
|
* key nothing reads. The subagent picker writes `draft.patch.team` instead.
|
|
14
|
+
*
|
|
15
|
+
* `'binding'` IS A KIT SLOT CHOOSER (agent-model-kits §4.5): a plain profile
|
|
16
|
+
* list with no `Current custom` row, no built-in rows and one `Create profile`
|
|
17
|
+
* action. A kit slot can only ever name a profile, so offering "no profile" as a
|
|
18
|
+
* choice there would be offering an invalid kit.
|
|
12
19
|
*/
|
|
13
|
-
export type ProfilePickerRole = ModelRole | 'subagent';
|
|
20
|
+
export type ProfilePickerRole = ModelRole | 'subagent' | 'binding';
|
|
14
21
|
/** The picker's built-in `Fast tier model` row; never a profile id on disk. */
|
|
15
22
|
export declare const SUBAGENT_FAST_ROW_ID = "__fast__";
|
|
16
23
|
export interface ProfileEditorDraft {
|
|
@@ -30,13 +37,27 @@ export interface ModelProfileState {
|
|
|
30
37
|
page: ProfilePage;
|
|
31
38
|
role: ProfilePickerRole;
|
|
32
39
|
editor?: ProfileEditorDraft;
|
|
33
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Where the editor and the delete confirmation hand control back.
|
|
42
|
+
*
|
|
43
|
+
* `'kit-slot'` JOINS `'picker'` (agent-model-kits §4.5): the kit slot chooser
|
|
44
|
+
* is a profile picker too, and its `Create profile` action must return to the
|
|
45
|
+
* slot it was creating for rather than to the profile manager.
|
|
46
|
+
*/
|
|
47
|
+
returnPage: 'picker' | 'manager' | 'kit-slot';
|
|
34
48
|
bindNew: boolean;
|
|
35
49
|
dirty: boolean;
|
|
36
50
|
error?: string;
|
|
37
51
|
deleteId?: string;
|
|
38
52
|
/** Who referenced the entry being deleted ('Main + Subagent', or ''). */
|
|
39
53
|
deleteUsers?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The kit editor's own draft (agent-model-kits §4.5), owned by `kitReducer`.
|
|
56
|
+
*
|
|
57
|
+
* ONE FIELD, NOT A SECOND STATE OBJECT: the page axis stays shared, so `Save`
|
|
58
|
+
* and `Esc` mean the same thing on every page of this screen.
|
|
59
|
+
*/
|
|
60
|
+
kitDraft?: KitDraft;
|
|
40
61
|
}
|
|
41
62
|
export type ModelProfileAction = {
|
|
42
63
|
type: 'page';
|
|
@@ -90,6 +111,8 @@ export declare function finishProfileEditor(editor: ProfileEditorDraft): {
|
|
|
90
111
|
};
|
|
91
112
|
/** Make an isolated root draft; opening the screen never mutates live config. */
|
|
92
113
|
export declare function createModelProfileState(draft: ModelSettingsDraft): ModelProfileState;
|
|
114
|
+
/** The kits whose three slots name `id`, for the delete confirmation (D-9). */
|
|
115
|
+
export declare function kitsUsingProfile(profiles: ModelProfilesConfig | undefined, id: string): AgentKit[];
|
|
93
116
|
/** Describe references using text so ASCII and no-color modes remain usable. */
|
|
94
117
|
export declare function profileUsers(profiles: ModelProfilesConfig | undefined, id: string, subagentProfileId?: string | null): string;
|
|
95
118
|
/**
|
|
@@ -41,10 +41,38 @@ export function createModelProfileState(draft) {
|
|
|
41
41
|
return { draft: copy, page: 'root', role: 'main', returnPage: 'manager',
|
|
42
42
|
bindNew: false, dirty: false };
|
|
43
43
|
}
|
|
44
|
+
/** The kits whose three slots name `id`, for the delete confirmation (D-9). */
|
|
45
|
+
export function kitsUsingProfile(profiles, id) {
|
|
46
|
+
return (profiles?.kits ?? []).filter((kit) => kit.mainProfileId === id
|
|
47
|
+
|| kit.fastProfileId === id
|
|
48
|
+
|| (kit.subagent.model === 'profile' && kit.subagent.modelProfileId === id));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Drop `slot` from the session kit (§4.3 row 2 / AC-38).
|
|
52
|
+
*
|
|
53
|
+
* THE DRAFT IS NARROWED HERE, NOT ONLY AT SAVE TIME. `releasedSlots` already
|
|
54
|
+
* strips the same slot from the transaction before it commits, but the ROOT PAGE
|
|
55
|
+
* paints the kit's effective value for every HELD slot - so without this line a
|
|
56
|
+
* user picks a profile and the row keeps showing the one they just replaced,
|
|
57
|
+
* which is the most confusing thing a settings screen can do. Releasing the last
|
|
58
|
+
* slot ends the kit: `held: []` is not a state any surface can render.
|
|
59
|
+
*/
|
|
60
|
+
function releaseHeldSlot(draft, slot) {
|
|
61
|
+
const current = draft.sessionKit;
|
|
62
|
+
if (!current || !current.held.includes(slot))
|
|
63
|
+
return draft;
|
|
64
|
+
const held = current.held.filter((item) => item !== slot);
|
|
65
|
+
return { ...draft, sessionKit: held.length ? { id: current.id, held } : null };
|
|
66
|
+
}
|
|
44
67
|
/** Describe references using text so ASCII and no-color modes remain usable. */
|
|
45
68
|
export function profileUsers(profiles, id, subagentProfileId) {
|
|
69
|
+
// THE KITS ARE REFERENCES TOO (D-9 / RV-P2-6): "Main + kit "Deep work"" is what
|
|
70
|
+
// makes the confirmation honest, because deleting this entry also deletes that
|
|
71
|
+
// kit. The empty case must stay empty - a leading ` + ` is what a naive
|
|
72
|
+
// `join` after an unconditional push produces.
|
|
46
73
|
return [profiles?.mainId === id ? 'Main' : '', profiles?.fastId === id ? 'Fast' : '',
|
|
47
|
-
subagentProfileId === id ? 'Subagent' : ''
|
|
74
|
+
subagentProfileId === id ? 'Subagent' : '',
|
|
75
|
+
...kitsUsingProfile(profiles, id).map((kit) => `kit "${kit.name}"`)]
|
|
48
76
|
.filter(Boolean).join(' + ');
|
|
49
77
|
}
|
|
50
78
|
/**
|
|
@@ -155,14 +183,27 @@ export function modelProfileReducer(state, action) {
|
|
|
155
183
|
? { model: 'fast', modelProfileId: null }
|
|
156
184
|
: { model: 'profile', modelProfileId: action.id };
|
|
157
185
|
return { ...state, page: 'root', dirty: true, error: undefined,
|
|
158
|
-
draft: { ...state.draft,
|
|
186
|
+
draft: releaseHeldSlot({ ...state.draft,
|
|
187
|
+
patch: { ...state.draft.patch, team } }, 'subagent') };
|
|
159
188
|
}
|
|
189
|
+
// `'binding'` NEVER REACHES THIS REDUCER: a kit slot's choice is applied
|
|
190
|
+
// by `kitReducer` (`kit-main` / `kit-fast`), because the value belongs to
|
|
191
|
+
// an editor draft rather than to a `${role}Id` library key. Returning the
|
|
192
|
+
// state unchanged is the honest answer to an impossible action.
|
|
193
|
+
if (action.role === 'binding')
|
|
194
|
+
return state;
|
|
160
195
|
return { ...state, page: 'root', dirty: true, error: undefined,
|
|
161
|
-
draft: { ...state.draft,
|
|
162
|
-
|
|
196
|
+
draft: releaseHeldSlot({ ...state.draft,
|
|
197
|
+
profiles: { ...library, [`${action.role}Id`]: action.id },
|
|
198
|
+
activateRoles: [...new Set([...state.draft.activateRoles, action.role])] }, action.role) };
|
|
163
199
|
}
|
|
164
200
|
case 'edit': return { ...state, editor: createProfileEditor(action.profile), page: 'editor',
|
|
165
|
-
|
|
201
|
+
// A PROFILE BORN IN A KIT SLOT RETURNS TO THAT SLOT (§4.5): the picker's
|
|
202
|
+
// `Create profile` action is the only way to invent one and it is offered on
|
|
203
|
+
// the kit's slot chooser - landing in the profile MANAGER there would
|
|
204
|
+
// abandon the half-built kit behind a page the user never asked for.
|
|
205
|
+
returnPage: state.page === 'picker' || state.page === 'kit-slot'
|
|
206
|
+
? state.page : 'manager',
|
|
166
207
|
bindNew: action.bindNew ?? false, error: undefined };
|
|
167
208
|
case 'editor-field': return editField(state, action);
|
|
168
209
|
case 'retain-key': return state.editor?.original.apiKey ? { ...state,
|
|
@@ -206,17 +247,29 @@ export function modelProfileReducer(state, action) {
|
|
|
206
247
|
// when it named THIS entry. `removeModelProfile`'s throw stays as the
|
|
207
248
|
// bottom-line defense - the UI has already unbound, so it cannot fire
|
|
208
249
|
// on this path.
|
|
250
|
+
//
|
|
251
|
+
// AND THE KITS GO WITH IT (agent-model-kits D-9 / I-8): a kit naming a
|
|
252
|
+
// deleted profile can neither be applied nor repaired from any screen, so
|
|
253
|
+
// leaving it behind would be leaving a permanent error. The confirmation
|
|
254
|
+
// page has already named them, so this is the action the user agreed to.
|
|
255
|
+
const doomed = kitsUsingProfile(library, id);
|
|
256
|
+
const defaultKitKept = doomed.some((kit) => kit.id === (library.defaultKitId ?? null))
|
|
257
|
+
? null : library.defaultKitId;
|
|
258
|
+
const sessionReleased = doomed.some((kit) => kit.id === state.draft.sessionKit?.id)
|
|
259
|
+
? null : state.draft.sessionKit;
|
|
209
260
|
const profiles = {
|
|
210
261
|
...library,
|
|
211
262
|
mainId: library.mainId === id ? null : library.mainId,
|
|
212
263
|
fastId: library.fastId === id ? null : library.fastId,
|
|
213
264
|
entries: library.entries.filter((entry) => entry.id !== id),
|
|
265
|
+
kits: (library.kits ?? []).filter((kit) => !doomed.includes(kit)),
|
|
266
|
+
defaultKitId: defaultKitKept,
|
|
214
267
|
};
|
|
215
268
|
const teamReset = draftSubagentProfileId(state.draft) === id
|
|
216
269
|
? { team: { model: 'main', modelProfileId: null } }
|
|
217
270
|
: {};
|
|
218
271
|
return { ...state, page: 'manager', dirty: true,
|
|
219
|
-
draft: { ...state.draft, profiles,
|
|
272
|
+
draft: { ...state.draft, profiles, sessionKit: sessionReleased,
|
|
220
273
|
patch: { ...state.draft.patch, ...teamReset } },
|
|
221
274
|
deleteId: undefined, deleteUsers: undefined,
|
|
222
275
|
// No orphan editor for a deleted entry (3.7).
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* WHY THIS FILE EXISTS. Native scroll mode cannot print an entry until it settles, so
|
|
6
6
|
* the live region used to show `buildLivePreview`'s plain-text projection - and for a
|
|
7
|
-
* running dispatch that projection
|
|
8
|
-
*
|
|
7
|
+
* running dispatch that projection was four grey rows saying `Team: 2/4 running`. It
|
|
8
|
+
* was the "simple look" the feature was opened for. A text-only region can never fix
|
|
9
9
|
* it, which is why the region's content type changed from `readonly string[]` to a
|
|
10
10
|
* fixed-height box that can hold components.
|
|
11
11
|
*
|
|
@@ -3,7 +3,7 @@ import { Box } from 'ink';
|
|
|
3
3
|
import { EntryView, nowSecFor } from '../Transcript.js';
|
|
4
4
|
import { estimateEntryRows } from '../layout/virtual-window.js';
|
|
5
5
|
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
6
|
-
import {
|
|
6
|
+
import { isSlotless } from './tail-window.js';
|
|
7
7
|
import { resolveTailAnchor } from './tail-anchor.js';
|
|
8
8
|
/**
|
|
9
9
|
* The last `maxEntries` printable entries, each with its predecessor.
|
|
@@ -13,7 +13,12 @@ import { resolveTailAnchor } from './tail-anchor.js';
|
|
|
13
13
|
* separation row the transcript shows above the same card.
|
|
14
14
|
*/
|
|
15
15
|
export function selectTailEntries(entries, maxEntries) {
|
|
16
|
-
|
|
16
|
+
// A LIVE DISPATCH CARD TAKES NO SLOT (tui-subagent-double-roster A-2): the roster
|
|
17
|
+
// draws it and `EntryView` returns null for it, so the window must not spend one of
|
|
18
|
+
// its slots on an entry that is not on screen. `isSlotless` is `tail-window.ts`'s
|
|
19
|
+
// predicate - the one `selectTailWindow` and the print pump apply - so the box and
|
|
20
|
+
// `planOverflowPrint` cannot drift into different windows.
|
|
21
|
+
const printable = entries.filter((entry) => !isSlotless(entry));
|
|
17
22
|
const limit = Number.isFinite(maxEntries) ? Math.max(0, Math.floor(maxEntries)) : 0;
|
|
18
23
|
if (limit === 0)
|
|
19
24
|
return [];
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { clampLiveText } from '../live-clamp.js';
|
|
21
21
|
import { wrapToRows } from '../layout/wrap-rows.js';
|
|
22
22
|
import { isExcluded } from './settled-boundary.js';
|
|
23
|
+
import { isLiveTeamCard, outcomePhrase, rosterCounts } from '../team-status.js';
|
|
23
24
|
import { clipLine } from './writer-shapes.js';
|
|
24
25
|
const MAX_TEAM_RUN_ROWS = 3;
|
|
25
26
|
const TOOL_LIVE_ROWS = 6;
|
|
@@ -56,8 +57,10 @@ function toolRows(entry, cols) {
|
|
|
56
57
|
return out;
|
|
57
58
|
}
|
|
58
59
|
function teamRows(entry, cols) {
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// ONLY A SETTLED CARD REACHES HERE (the live one projects nothing, below), so the
|
|
61
|
+
// outcome phrase - `team-status.ts`'s own sentence - replaces an always-zero fraction.
|
|
62
|
+
const counts = rosterCounts(entry.runs);
|
|
63
|
+
const out = [fit(`Team: ${outcomePhrase(counts)}`, cols)];
|
|
61
64
|
for (const run of entry.runs.slice(0, MAX_TEAM_RUN_ROWS)) {
|
|
62
65
|
out.push(fit(` ${run.label} ${run.phase}`, cols));
|
|
63
66
|
}
|
|
@@ -71,7 +74,11 @@ function entryRows(entry, input) {
|
|
|
71
74
|
case 'tool':
|
|
72
75
|
return toolRows(entry, cols);
|
|
73
76
|
case 'team':
|
|
74
|
-
|
|
77
|
+
// THE LIVE DISPATCH HAS ONE SURFACE (tui-subagent-double-roster A-2): this
|
|
78
|
+
// projection is the text fallback for the same slot `LiveTail` fills, so a
|
|
79
|
+
// `Team: n/m running` line here would be the second live block again, one level
|
|
80
|
+
// down. A settled card still projects normally.
|
|
81
|
+
return isLiveTeamCard(entry) ? [] : teamRows(entry, cols);
|
|
75
82
|
case 'todo':
|
|
76
83
|
return [fit(`Todo: ${entry.doneCount}/${entry.total}`, cols)];
|
|
77
84
|
case 'compaction':
|
|
@@ -27,6 +27,18 @@
|
|
|
27
27
|
* closes, and `/perf`'s `window=<start>/<n> head=<rows>` is how it stays observable.
|
|
28
28
|
*/
|
|
29
29
|
import type { Entry } from '../../agent/reducer.js';
|
|
30
|
+
/**
|
|
31
|
+
* Entries that take NO slot in the window: they neither print nor take a row.
|
|
32
|
+
*
|
|
33
|
+
* A LIVE DISPATCH CARD JOINS THE NON-TERMINAL SERVICE CARD HERE
|
|
34
|
+
* (tui-subagent-double-roster A-2'): the roster draws the live dispatch and the
|
|
35
|
+
* transcript draws nothing, so the window must not spend one of its slots on an
|
|
36
|
+
* entry that is not on screen. THE PRINT PUMP AND THE BOX MUST AGREE ABOUT SLOTS:
|
|
37
|
+
* so both window functions below skip it, and `LiveTail.selectTailEntries` - the
|
|
38
|
+
* window function the box renders - imports this one instead of spelling the
|
|
39
|
+
* composition out a second time.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isSlotless(entry: Entry): boolean;
|
|
30
42
|
export interface TailWindow {
|
|
31
43
|
/** First index of the window inside the full entry list. */
|
|
32
44
|
start: number;
|
|
@@ -27,6 +27,21 @@
|
|
|
27
27
|
* closes, and `/perf`'s `window=<start>/<n> head=<rows>` is how it stays observable.
|
|
28
28
|
*/
|
|
29
29
|
import { isExcluded, settledCount } from './settled-boundary.js';
|
|
30
|
+
import { isLiveTeamCard } from '../team-status.js';
|
|
31
|
+
/**
|
|
32
|
+
* Entries that take NO slot in the window: they neither print nor take a row.
|
|
33
|
+
*
|
|
34
|
+
* A LIVE DISPATCH CARD JOINS THE NON-TERMINAL SERVICE CARD HERE
|
|
35
|
+
* (tui-subagent-double-roster A-2'): the roster draws the live dispatch and the
|
|
36
|
+
* transcript draws nothing, so the window must not spend one of its slots on an
|
|
37
|
+
* entry that is not on screen. THE PRINT PUMP AND THE BOX MUST AGREE ABOUT SLOTS:
|
|
38
|
+
* so both window functions below skip it, and `LiveTail.selectTailEntries` - the
|
|
39
|
+
* window function the box renders - imports this one instead of spelling the
|
|
40
|
+
* composition out a second time.
|
|
41
|
+
*/
|
|
42
|
+
export function isSlotless(entry) {
|
|
43
|
+
return isExcluded(entry) || isLiveTeamCard(entry);
|
|
44
|
+
}
|
|
30
45
|
/**
|
|
31
46
|
* The index of the first entry that is still on screen: count backwards up to
|
|
32
47
|
* `maxEntries` PRINTABLE entries. Everything before it has left the region and is the
|
|
@@ -42,7 +57,7 @@ export function tailWindowStart(entries, maxEntries) {
|
|
|
42
57
|
return entries.length;
|
|
43
58
|
let seen = 0;
|
|
44
59
|
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
45
|
-
if (
|
|
60
|
+
if (isSlotless(entries[i]))
|
|
46
61
|
continue;
|
|
47
62
|
seen += 1;
|
|
48
63
|
if (seen === limit)
|
|
@@ -54,7 +69,7 @@ export function tailWindowStart(entries, maxEntries) {
|
|
|
54
69
|
/** `entries.slice(start)` with the entries that never print dropped. */
|
|
55
70
|
export function selectTailWindow(entries, maxEntries) {
|
|
56
71
|
const start = tailWindowStart(entries, maxEntries);
|
|
57
|
-
return { start, entries: entries.slice(start).filter((entry) => !
|
|
72
|
+
return { start, entries: entries.slice(start).filter((entry) => !isSlotless(entry)) };
|
|
58
73
|
}
|
|
59
74
|
/**
|
|
60
75
|
* The print boundary: never print what the region is still showing.
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
*/
|
|
53
53
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef } from 'react';
|
|
54
54
|
import { estimateEntryRows } from '../layout/virtual-window.js';
|
|
55
|
+
import { isLiveTeamCard } from '../team-status.js';
|
|
55
56
|
import { buildScrollbackBudget } from './budget.js';
|
|
56
57
|
import { buildLivePreview } from './live-preview.js';
|
|
57
58
|
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
@@ -554,6 +555,12 @@ export function useScrollbackView(args) {
|
|
|
554
555
|
if (index < 0)
|
|
555
556
|
return false;
|
|
556
557
|
const entry = list[index];
|
|
558
|
+
// A LIVE DISPATCH CARD IS REFUSED, NOT PRINTED (tui-subagent-double-roster A-2').
|
|
559
|
+
// `Ctrl+O` arrives mid-dispatch, `EntryView` suppresses this card, and a copy
|
|
560
|
+
// printed here would be a FROZEN SNAPSHOT of a card the roster is still rewriting -
|
|
561
|
+
// permanently in the scrollback, because printed is forever.
|
|
562
|
+
if (isLiveTeamCard(entry))
|
|
563
|
+
return false;
|
|
557
564
|
// THE COPY IS RENDERED IN A MICROTASK, NOT IN THE KEY HANDLER THAT ASKED FOR IT.
|
|
558
565
|
// `Ctrl+O` arrives on the stdin stream, and an offscreen render issued from there is
|
|
559
566
|
// SCHEDULED rather than flushed - the reconciler sees a pending update, the frame the
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team status — the ONE authority that answers "what state is this child in, and
|
|
3
|
+
* how is that drawn / counted / timed" (tui-subagent-double-roster A-1).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. The phase -> glyph / colour / count question used to be
|
|
6
|
+
* answered by nine copies in seven files, and they disagreed. The transcript card
|
|
7
|
+
* asked `isOk(run) ? toolDone : toolError`, so six of the eight phases - every
|
|
8
|
+
* child that was still working - were painted with the FAILURE glyph, and an
|
|
9
|
+
* aborted child was counted as a failed one forever. That is not a palette
|
|
10
|
+
* mistake, it is a judgement implemented in the wrong number of places.
|
|
11
|
+
*
|
|
12
|
+
* THE VOCABULARY IS THE DOMAIN'S (analysis A-1' item 3). `ChildStatus` reuses
|
|
13
|
+
* `SubagentPhase`'s own words - `queued` / `running` / `done` / `failed` /
|
|
14
|
+
* `aborted` - and never invents a second dictionary (`pending` was proposed and
|
|
15
|
+
* rejected: `SubagentPhase` does not have it, and two words for one state is how
|
|
16
|
+
* the tenth copy gets written).
|
|
17
|
+
*
|
|
18
|
+
* FIVE STATES, NOT FOUR (A-1' item 2). `aborted` is NOT a failure. The runtime
|
|
19
|
+
* distinguishes them on purpose (`Runtime.settlePhase`: aborted first, then the
|
|
20
|
+
* timeout, then the error), and a user pressing Esc must not read their own
|
|
21
|
+
* action back as "3 failed". So `aborted` takes `glyphs.warn` and
|
|
22
|
+
* `theme.noticeWarn`, which collide with neither the done tick nor the failed
|
|
23
|
+
* cross in either glyph tier.
|
|
24
|
+
*
|
|
25
|
+
* ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts`'s static scan, so every
|
|
26
|
+
* symbol here is injected through `pickGlyphs(caps)`.
|
|
27
|
+
*/
|
|
28
|
+
import type { Entry } from '../agent/reducer.js';
|
|
29
|
+
import type { SubagentRun } from '../team/types.js';
|
|
30
|
+
import type { Glyphs } from './glyphs.js';
|
|
31
|
+
import type { Theme } from './theme.js';
|
|
32
|
+
/**
|
|
33
|
+
* The five readings a child can have on screen. A TOTAL mapping of
|
|
34
|
+
* `SubagentPhase`, which is why `childStatus` needs no guard clauses: `starting`,
|
|
35
|
+
* `thinking`, `tool` and `waiting` are all "this child is working right now", and
|
|
36
|
+
* the per-row activity column is what tells them apart.
|
|
37
|
+
*/
|
|
38
|
+
export type ChildStatus = 'queued' | 'running' | 'done' | 'failed' | 'aborted';
|
|
39
|
+
/**
|
|
40
|
+
* The status of one child.
|
|
41
|
+
*
|
|
42
|
+
* A LOOKUP TABLE RATHER THAN A SWITCH, because the mapping is the whole content
|
|
43
|
+
* of this function: a `Record` keyed by the phase union is exhaustive at compile
|
|
44
|
+
* time (a new phase is a missing-key error) and cannot drift from the type.
|
|
45
|
+
*/
|
|
46
|
+
export declare function childStatus(run: SubagentRun): ChildStatus;
|
|
47
|
+
/** Per-status totals of a roster. `total` is the roster size, not the sum. */
|
|
48
|
+
export interface RosterCounts {
|
|
49
|
+
queued: number;
|
|
50
|
+
running: number;
|
|
51
|
+
done: number;
|
|
52
|
+
failed: number;
|
|
53
|
+
aborted: number;
|
|
54
|
+
total: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Count a roster once, by status.
|
|
58
|
+
*
|
|
59
|
+
* THE HEADER, THE CARD HEADLINE AND THE STATUS CHIP ALL READ THIS. Before it
|
|
60
|
+
* existed the header filtered `phase !== 'queued' && ...` for "running" and the
|
|
61
|
+
* card subtracted `runs.length - isOk(runs)`, so the same dispatch reported
|
|
62
|
+
* `0 running` above two rows reading `queued` and `1 failed` for a child that had
|
|
63
|
+
* never started.
|
|
64
|
+
*/
|
|
65
|
+
export declare function rosterCounts(runs: readonly SubagentRun[]): RosterCounts;
|
|
66
|
+
/** True while the child is doing something - the only state that animates. */
|
|
67
|
+
export declare function isRunningStatus(status: ChildStatus): boolean;
|
|
68
|
+
/** True once the child's outcome is final, whatever it is. */
|
|
69
|
+
export declare function isSettledStatus(status: ChildStatus): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The static marker for a status.
|
|
72
|
+
*
|
|
73
|
+
* `running` gets `spinnerStill` because the animated `<Spinner>` is the caller's
|
|
74
|
+
* job: it needs `reducedMotion` and `caps.unicode`, and a component that owns its
|
|
75
|
+
* own `ink-spinner` is what `spinner-census.test.ts` counts. The caller swaps this
|
|
76
|
+
* glyph for the spinner when it may animate.
|
|
77
|
+
*/
|
|
78
|
+
export declare function statusGlyph(status: ChildStatus, glyphs: Glyphs): string;
|
|
79
|
+
/**
|
|
80
|
+
* The marker padded to the tier's widest one, for the leading cell of a row.
|
|
81
|
+
*
|
|
82
|
+
* THE PADDING IS NOT COSMETIC (A-1'a): the ASCII tier spells `[ok]` (4), `[x]` (3)
|
|
83
|
+
* and `!` (1), so an unpadded marker cell starts every label at a different
|
|
84
|
+
* column and the roster stops reading as a table. `TodoPanel` pads its marker cell
|
|
85
|
+
* for the same reason. The Unicode tier's markers are all one cell wide, so the
|
|
86
|
+
* padding is a no-op there.
|
|
87
|
+
*/
|
|
88
|
+
export declare function statusMarkerCell(status: ChildStatus, glyphs: Glyphs): string;
|
|
89
|
+
/**
|
|
90
|
+
* The colour of a status. One reading for the roster row, the card row and the
|
|
91
|
+
* card's rail marker.
|
|
92
|
+
*
|
|
93
|
+
* `aborted` deliberately does NOT share `toolError` with `failed`: an abort is the
|
|
94
|
+
* user's own Esc, and the analysis found that painting it red made a cancelled
|
|
95
|
+
* dispatch permanently unreadable in the history.
|
|
96
|
+
*/
|
|
97
|
+
export declare function statusColor(status: ChildStatus, theme: Theme): string | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* How long this child has been (or was) working, in ms.
|
|
100
|
+
*
|
|
101
|
+
* `endedAt ?? now`, NOT `endedAt ?? 0`. The transcript card used the latter, which
|
|
102
|
+
* made every RUNNING child read `0ms` while the roster beside it read `1m05s` for
|
|
103
|
+
* the same child - the pair of contradictory readouts the reporter screenshotted.
|
|
104
|
+
*/
|
|
105
|
+
export declare function childElapsedMs(run: SubagentRun, now: number): number;
|
|
106
|
+
/**
|
|
107
|
+
* The roster header's count section, e.g. `2 running · 0 done · 1 queued`.
|
|
108
|
+
*
|
|
109
|
+
* `queued` IS COUNTED (the old `isRunning` excluded it, so a fresh dispatch
|
|
110
|
+
* announced `0 running · 0 done` above five rows that each said `queued`), and a
|
|
111
|
+
* non-zero bucket is always named even when nothing is running - a roster whose
|
|
112
|
+
* children are all `aborted` must say so rather than `0 running · 0 done`.
|
|
113
|
+
*
|
|
114
|
+
* WIDTH LADDER: narrow terminals drop the least urgent clause first, in the order
|
|
115
|
+
* `done`, `aborted`, `failed`, `queued`; `running` is never dropped. The order is
|
|
116
|
+
* a product decision, not a detail: while a dispatch is live, what is happening
|
|
117
|
+
* now outranks what has already finished, and the per-row activity column carries
|
|
118
|
+
* the finished children's own facts anyway.
|
|
119
|
+
*/
|
|
120
|
+
export declare function rosterHeadline(counts: RosterCounts, glyphs: Glyphs, cols: number): string;
|
|
121
|
+
/**
|
|
122
|
+
* The settled card's outcome buckets, e.g. `2 ok, 1 aborted, 1 failed`.
|
|
123
|
+
*
|
|
124
|
+
* `ok` is always shown (it is the denominator a reader compares against) and a
|
|
125
|
+
* zero bucket is omitted, so a clean dispatch still reads `3 ok` and a failed one
|
|
126
|
+
* no longer claims that a cancelled child failed.
|
|
127
|
+
*/
|
|
128
|
+
export declare function outcomePhrase(counts: RosterCounts): string;
|
|
129
|
+
/**
|
|
130
|
+
* Whether this entry is the transcript card of a dispatch that is STILL RUNNING,
|
|
131
|
+
* i.e. the second live surface the reporter saw beside the roster.
|
|
132
|
+
*
|
|
133
|
+
* ONE PREDICATE, FOUR CONSUMERS (analysis A-2'): the transcript's `case 'team'`
|
|
134
|
+
* and `estimateEntryRows` (so the virtual window and the frame agree), the tail
|
|
135
|
+
* window and `LiveTail` (so the pump's print plan and the box agree), and the
|
|
136
|
+
* `Ctrl+O` finder plus `reprintExpanded` (so a keypress cannot print a stale
|
|
137
|
+
* snapshot of a card that is still being rewritten). `state.entries` is NEVER
|
|
138
|
+
* filtered: the entry's lifecycle and `session/persist.ts`'s `interrupted`
|
|
139
|
+
* normalisation depend on it staying there.
|
|
140
|
+
*/
|
|
141
|
+
export declare function isLiveTeamCard(entry: Entry): boolean;
|