@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
package/dist/ui/TeamPanel.js
CHANGED
|
@@ -2,26 +2,11 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
4
|
import { pickGlyphs } from './glyphs.js';
|
|
5
|
+
import { childElapsedMs, childStatus, isRunningStatus, rosterCounts, rosterHeadline, statusColor, statusMarkerCell, } from './team-status.js';
|
|
5
6
|
import { formatDuration } from '../agent/usage.js';
|
|
6
7
|
import { describeToolActivity, sanitizeActivity } from '../team/activity.js';
|
|
7
8
|
import { TEAM_LIMITS } from '../team/limits.js';
|
|
8
9
|
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
9
|
-
function isRunning(run) {
|
|
10
|
-
return run.phase === 'starting' || run.phase === 'thinking' || run.phase === 'tool' ||
|
|
11
|
-
run.phase === 'waiting';
|
|
12
|
-
}
|
|
13
|
-
function isSettled(run) {
|
|
14
|
-
return run.phase === 'done' || run.phase === 'failed' || run.phase === 'aborted';
|
|
15
|
-
}
|
|
16
|
-
function statusColor(run, theme) {
|
|
17
|
-
if (run.phase === 'failed' || run.phase === 'aborted')
|
|
18
|
-
return theme.toolError;
|
|
19
|
-
if (run.phase === 'done')
|
|
20
|
-
return theme.toolDone;
|
|
21
|
-
if (isRunning(run))
|
|
22
|
-
return theme.toolRunning;
|
|
23
|
-
return theme.muted;
|
|
24
|
-
}
|
|
25
10
|
/**
|
|
26
11
|
* How many characters the activity column may spend at this width (P0-1).
|
|
27
12
|
*
|
|
@@ -172,10 +157,12 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
|
|
|
172
157
|
return null;
|
|
173
158
|
const glyphs = pickGlyphs(caps);
|
|
174
159
|
const at = now ?? Date.now();
|
|
175
|
-
|
|
176
|
-
|
|
160
|
+
// ONE COUNTING AUTHORITY (A-1): `queued` used to fall between `isRunning` and
|
|
161
|
+
// `isSettled`, so a fresh dispatch read `0 running 0 done` above rows that each
|
|
162
|
+
// said `queued`.
|
|
163
|
+
const counts = rosterCounts(snapshot.runs);
|
|
177
164
|
const elapsed = formatDuration(Math.max(0, at - snapshot.startedAt));
|
|
178
|
-
const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [
|
|
165
|
+
const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [rosterHeadline(counts, glyphs, cols), " ", glyphs.midDot, " ", elapsed] })] }), snapshot.messageCount > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.teamMail, " ", snapshot.messageCount] }))] }));
|
|
179
166
|
// Short terminals or a long draft leave only the shared header projection,
|
|
180
167
|
// which still answers whether anything is running and for how long.
|
|
181
168
|
if (projection.collapsed) {
|
|
@@ -187,13 +174,14 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
|
|
|
187
174
|
// the requirement permits.
|
|
188
175
|
const { visible, hiddenTotal, hiddenRunning } = projection;
|
|
189
176
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [header, visible.map((run) => {
|
|
190
|
-
const
|
|
177
|
+
const status = childStatus(run);
|
|
178
|
+
const color = statusColor(status, theme);
|
|
191
179
|
// One spinner per RUNNING row, and the row cap is 5 precisely because
|
|
192
180
|
// five is the documented maximum number of simultaneous spinners. Both
|
|
193
181
|
// `reducedMotion` and a non-Unicode terminal fall back to the same
|
|
194
182
|
// static marker, exactly as `AssistantEntry` and `ToolCard` already do.
|
|
195
|
-
const animate =
|
|
196
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) :
|
|
183
|
+
const animate = isRunningStatus(status) && !reducedMotion && caps.unicode;
|
|
184
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : statusMarkerCell(status, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityWithBadge(run, cols, at)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(childElapsedMs(run, at)) : ''] })] }, run.label));
|
|
197
185
|
}), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), (() => {
|
|
198
186
|
// The panel's supervisor line (AC-5): one row, only when the
|
|
199
187
|
// supervisor has actually acted - the per-row badges above already
|
|
@@ -202,12 +190,3 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
|
|
|
202
190
|
return line ? (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', line] })) : null;
|
|
203
191
|
})(), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
|
|
204
192
|
}
|
|
205
|
-
function marker(run, glyphs) {
|
|
206
|
-
if (run.phase === 'done')
|
|
207
|
-
return glyphs.toolDone;
|
|
208
|
-
if (run.phase === 'failed' || run.phase === 'aborted')
|
|
209
|
-
return glyphs.toolError;
|
|
210
|
-
if (run.phase === 'queued')
|
|
211
|
-
return glyphs.toolPending;
|
|
212
|
-
return glyphs.spinnerStill;
|
|
213
|
-
}
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -12,6 +12,7 @@ import { QueuedEntry } from './entries/QueuedEntry.js';
|
|
|
12
12
|
import { AssistantEntry } from './entries/AssistantEntry.js';
|
|
13
13
|
import { ToolCard, statusColor, toolEntryGlyph } from './entries/ToolCard.js';
|
|
14
14
|
import { TeamCard, teamCardColor } from './entries/TeamCard.js';
|
|
15
|
+
import { isLiveTeamCard, rosterCounts } from './team-status.js';
|
|
15
16
|
import { TodoCard, todoCardColor } from './entries/TodoCard.js';
|
|
16
17
|
import { RetryCard, retryCardColor } from './entries/RetryCard.js';
|
|
17
18
|
import { FastCard, fastCardColor } from './entries/FastCard.js';
|
|
@@ -42,6 +43,12 @@ export function nowSecFor(entry, nowSec) {
|
|
|
42
43
|
// function protects is unchanged for every other card, forever.
|
|
43
44
|
if (entry.kind === 'compaction' && entry.live)
|
|
44
45
|
return nowSec;
|
|
46
|
+
// THE FOURTH CONSUMER IS A LIVE DISPATCH CARD (tui-subagent-double-roster A-1):
|
|
47
|
+
// a child's elapsed time must advance while it works, and `TeamCard` reads this
|
|
48
|
+
// same 1 Hz clock instead of a second `Date.now()` per render. A suppressed card
|
|
49
|
+
// never mounts, so this only keeps the reading honest for a directly mounted one.
|
|
50
|
+
if (entry.kind === 'team' && entry.active)
|
|
51
|
+
return nowSec;
|
|
45
52
|
// THE THIRD CONSUMER IS A LIVE JOB (long-task-jobs D-8), and it is the reason
|
|
46
53
|
// this stays a single 1 Hz clock: a task's elapsed time and its `no output`
|
|
47
54
|
// badge both have to move, and a per-card `setInterval` would be one timer per
|
|
@@ -96,8 +103,21 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
96
103
|
// comparator sees a fresh `[]` as a changed prop, which is why that
|
|
97
104
|
// file already keeps a shared `EMPTY_LINES`.
|
|
98
105
|
live: entry.live, lastOutputAt: entry.lastOutputAt, nowSec: nowSec, theme: theme, caps: caps }));
|
|
99
|
-
case 'team':
|
|
100
|
-
|
|
106
|
+
case 'team': {
|
|
107
|
+
// THE LIVE DISPATCH HAS EXACTLY ONE SURFACE (tui-subagent-double-roster A-2):
|
|
108
|
+
// while `active`, the roster (`TeamPanel`) is the only live subagent block, so
|
|
109
|
+
// the transcript draws nothing for this entry and the card appears - once - as
|
|
110
|
+
// history the moment the dispatch settles. Returning BEFORE `frame(...)` draws
|
|
111
|
+
// neither the rail marker nor the separation row.
|
|
112
|
+
//
|
|
113
|
+
// `renderer.tsx`'s offscreen measurement goes through THIS function too, which is
|
|
114
|
+
// what makes the native print pump's `measureWindow` agree with the frame for
|
|
115
|
+
// free. `estimateEntryRows` carries the same predicate (0 rows), so no other
|
|
116
|
+
// arithmetic has to know about the suppression.
|
|
117
|
+
if (isLiveTeamCard(entry))
|
|
118
|
+
return null;
|
|
119
|
+
return frame(glyphs.teamAgent, teamCardColor(rosterCounts(entry.runs), entry.active, entry.aborted, theme), _jsx(TeamCard, { requested: entry.requested, runs: entry.runs, aborted: entry.aborted, durationMs: entry.durationMs, active: entry.active, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps, nowSec: nowSecFor(entry, nowSec) }));
|
|
120
|
+
}
|
|
101
121
|
case 'todo':
|
|
102
122
|
return frame(toolGlyph(glyphs, 'todo_write'), todoCardColor(entry.doneCount, entry.total, entry.live, theme), _jsx(TodoCard, { items: entry.items, doneCount: entry.doneCount, total: entry.total, live: entry.live, interrupted: entry.interrupted, theme: theme, caps: caps }));
|
|
103
123
|
case 'retry':
|
|
@@ -15,6 +15,7 @@ import React from 'react';
|
|
|
15
15
|
import type { Theme } from '../theme.js';
|
|
16
16
|
import type { TermCapabilities } from '../capabilities.js';
|
|
17
17
|
import type { SubagentRun } from '../../team/types.js';
|
|
18
|
+
import { type RosterCounts } from '../team-status.js';
|
|
18
19
|
export interface TeamCardProps {
|
|
19
20
|
requested: number;
|
|
20
21
|
runs: SubagentRun[];
|
|
@@ -25,9 +26,24 @@ export interface TeamCardProps {
|
|
|
25
26
|
reducedMotion?: boolean;
|
|
26
27
|
theme: Theme;
|
|
27
28
|
caps: TermCapabilities;
|
|
29
|
+
/**
|
|
30
|
+
* Wall-clock seconds, set only while the dispatch is live (`nowSecFor`). WITHOUT
|
|
31
|
+
* IT a running child's elapsed time came from `(endedAt ?? 0) - startedAt` - a
|
|
32
|
+
* negative number clamped to `0ms`, the readout that contradicted the roster
|
|
33
|
+
* beside it (A-1 / E4).
|
|
34
|
+
*/
|
|
35
|
+
nowSec?: number;
|
|
28
36
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The card's aggregate colour.
|
|
39
|
+
*
|
|
40
|
+
* FAILURES DOMINATE, THEN A CANCELLATION, THEN A DISPATCH THAT NEVER SETTLED.
|
|
41
|
+
* The old version asked `runs.some((r) => !isOk(r))`, so an entry whose children
|
|
42
|
+
* were still working - and one the user had cancelled - was painted `toolError`
|
|
43
|
+
* (A-1 / E8). `RosterCounts` is passed IN rather than recomputed, so this colour
|
|
44
|
+
* and the headline cannot disagree inside one card.
|
|
45
|
+
*/
|
|
46
|
+
export declare function teamCardColor(counts: RosterCounts, active: boolean, aborted: boolean, theme: Theme): string | undefined;
|
|
31
47
|
declare function TeamCardImpl(props: TeamCardProps): React.ReactElement;
|
|
32
48
|
/**
|
|
33
49
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
@@ -18,37 +18,71 @@ import Spinner from 'ink-spinner';
|
|
|
18
18
|
import { pickGlyphs } from '../glyphs.js';
|
|
19
19
|
import { railBorderProps } from '../layout/Gutter.js';
|
|
20
20
|
import { formatDuration } from '../../agent/usage.js';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
import { childElapsedMs, childStatus, outcomePhrase, rosterCounts, statusColor, statusMarkerCell, } from '../team-status.js';
|
|
22
|
+
/**
|
|
23
|
+
* The card's aggregate colour.
|
|
24
|
+
*
|
|
25
|
+
* FAILURES DOMINATE, THEN A CANCELLATION, THEN A DISPATCH THAT NEVER SETTLED.
|
|
26
|
+
* The old version asked `runs.some((r) => !isOk(r))`, so an entry whose children
|
|
27
|
+
* were still working - and one the user had cancelled - was painted `toolError`
|
|
28
|
+
* (A-1 / E8). `RosterCounts` is passed IN rather than recomputed, so this colour
|
|
29
|
+
* and the headline cannot disagree inside one card.
|
|
30
|
+
*/
|
|
31
|
+
export function teamCardColor(counts, active, aborted, theme) {
|
|
26
32
|
if (active)
|
|
27
33
|
return theme.toolRunning;
|
|
28
|
-
if (
|
|
34
|
+
if (counts.failed > 0)
|
|
29
35
|
return theme.toolError;
|
|
36
|
+
if (aborted || counts.aborted > 0 || counts.queued > 0 || counts.running > 0) {
|
|
37
|
+
return theme.noticeWarn;
|
|
38
|
+
}
|
|
30
39
|
return theme.toolDone;
|
|
31
40
|
}
|
|
41
|
+
/** `nowSec` is seconds (`App`'s 1 Hz clock); a card measures in milliseconds. */
|
|
42
|
+
function liveClockMs(nowSec) {
|
|
43
|
+
return nowSec === undefined ? Date.now() : nowSec * 1000;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The elapsed column for one child: a MEASUREMENT, or an empty cell.
|
|
47
|
+
*
|
|
48
|
+
* `liveNow` is defined only while the dispatch is live, and that distinction is the
|
|
49
|
+
* whole point. A child restored by `/resume` keeps its `startedAt` and has no
|
|
50
|
+
* `endedAt` - `session/persist.ts` settles the ENTRY, it does not invent an end time
|
|
51
|
+
* for the run - so a figure computed against `Date.now()` would claim hours of work
|
|
52
|
+
* that never happened, next to an `interrupted (session resumed)` headline that says
|
|
53
|
+
* the opposite. An unmeasurable duration is left blank instead.
|
|
54
|
+
*/
|
|
55
|
+
function elapsedCell(run, liveNow) {
|
|
56
|
+
const end = run.endedAt ?? liveNow;
|
|
57
|
+
if (run.startedAt === undefined || end === undefined)
|
|
58
|
+
return '';
|
|
59
|
+
return formatDuration(childElapsedMs(run, end));
|
|
60
|
+
}
|
|
32
61
|
function TeamCardImpl(props) {
|
|
33
62
|
const { requested, runs, aborted, durationMs, active, expanded, reducedMotion, theme, caps } = props;
|
|
34
63
|
const glyphs = pickGlyphs(caps);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
64
|
+
// ONE COUNTING AUTHORITY (A-1): `ok`/`failed` used to be `isOk(...)` and
|
|
65
|
+
// `runs.length - ok`, which counted a cancelled child - and a still-working one -
|
|
66
|
+
// as a failed one (E3 / E8).
|
|
67
|
+
const counts = rosterCounts(runs);
|
|
68
|
+
const color = teamCardColor(counts, active, aborted, theme);
|
|
69
|
+
// A CLOCK ONLY WHILE THE DISPATCH IS LIVE: a settled card has nothing left to time,
|
|
70
|
+
// and a clock in the settled path is how a resumed dispatch came to read like a run
|
|
71
|
+
// that is still going (`elapsedCell`).
|
|
72
|
+
const liveNow = active ? liveClockMs(props.nowSec) : undefined;
|
|
38
73
|
// A dispatch entry restored from `/resume` is settled and aborted but has no
|
|
39
74
|
// duration: nothing measured it, because the process it belonged to is gone.
|
|
40
75
|
// Saying so plainly beats rendering `0.0s (0 ok, 3 failed)`, which reads like
|
|
41
76
|
// a dispatch that ran and lost (§5.3 / P1-5).
|
|
42
77
|
const interrupted = !active && aborted && durationMs === undefined;
|
|
43
|
-
const settled = runs.filter((r) => r.phase === 'done' || r.phase === 'failed' || r.phase === 'aborted').length;
|
|
44
78
|
const headline = interrupted
|
|
45
79
|
? 'interrupted (session resumed)'
|
|
46
80
|
: active
|
|
47
|
-
? `${
|
|
48
|
-
: `${formatDuration(durationMs ?? 0)} (${
|
|
81
|
+
? `${counts.queued + counts.running} running`
|
|
82
|
+
: `${formatDuration(durationMs ?? 0)} (${outcomePhrase(counts)})`;
|
|
49
83
|
const count = `${runs.length} ${runs.length === 1 ? 'subagent' : 'subagents'}`;
|
|
50
84
|
const dropped = requested > runs.length ? ` (${runs.length} of ${requested} requested)` : '';
|
|
51
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color:
|
|
85
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: statusColor(childStatus(run), theme), children: statusMarkerCell(childStatus(run), glyphs) }), _jsxs(Text, { color: theme.primary, children: [' ', run.label.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', elapsedCell(run, liveNow), ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] }), run.retry && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', "retry ", run.retry.attempt, "/", run.retry.maxRetries] }))] }, run.label))), expanded &&
|
|
52
86
|
runs.map((run) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: [run.label, " ", glyphs.midDot, " ", run.description] }), _jsx(Text, { color: theme.muted, children: run.summary ?? '(no summary)' })] }, `s-${run.label}`))), !expanded && !active && runs.length > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", runs.length, " ", runs.length === 1 ? 'summary' : 'summaries', " (Ctrl+O)"] })), expanded && _jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" })] })] }));
|
|
53
87
|
}
|
|
54
88
|
/**
|
|
@@ -46,7 +46,7 @@ export declare const HEADER_HINT_LIMITS: {
|
|
|
46
46
|
*/
|
|
47
47
|
readonly separatorCells: 2;
|
|
48
48
|
};
|
|
49
|
-
export type HeaderHintId = 'branch' | 'index' | 'detail';
|
|
49
|
+
export type HeaderHintId = 'branch' | 'index' | 'kit' | 'detail';
|
|
50
50
|
export interface HeaderHintClause {
|
|
51
51
|
id: HeaderHintId;
|
|
52
52
|
text: string;
|
|
@@ -74,4 +74,14 @@ export declare function planHeaderHints(input: {
|
|
|
74
74
|
statusExpanded: boolean;
|
|
75
75
|
overlayOpen: boolean;
|
|
76
76
|
scrolledLines: number;
|
|
77
|
+
/**
|
|
78
|
+
* Whether the `^K switch kit` clause exists at all (default `true`).
|
|
79
|
+
*
|
|
80
|
+
* `App` ALWAYS PASSES TRUE, including for a user with no kits: the whole point
|
|
81
|
+
* of the requirement is discoverability of `ctrl+K`, and an empty library is
|
|
82
|
+
* exactly the state that needs it (the picker's own `No kits yet` row tells
|
|
83
|
+
* them where to create one). `false` exists for tests and for the future
|
|
84
|
+
* switch, and it degrades the six expanded rungs back into today's four.
|
|
85
|
+
*/
|
|
86
|
+
kitHint?: boolean;
|
|
77
87
|
}): HeaderHintPlan;
|
|
@@ -49,6 +49,15 @@ export const HEADER_HINT_LIMITS = {
|
|
|
49
49
|
/** The verbatim clause text (spec 3.8, appendix A). */
|
|
50
50
|
const BRANCH_CLAUSE = { collapsed: '^L branch', expanded: '^L new branch' };
|
|
51
51
|
const INDEX_CLAUSE = { collapsed: '^I Index', expanded: '^I build index' };
|
|
52
|
+
/**
|
|
53
|
+
* THE KIT CLAUSE HAS NO COLLAPSED FORM (agent-model-kits D-11).
|
|
54
|
+
*
|
|
55
|
+
* The requirement says the `ctrl+k` hint appears AFTER `ctrl+G`; the collapsed
|
|
56
|
+
* row is the narrowest budget in the header, and `^K` is a low-frequency action.
|
|
57
|
+
* Spending 13 columns of the everyday row on it would push `provider:model`
|
|
58
|
+
* leftward on every launch for a key pressed twice a session.
|
|
59
|
+
*/
|
|
60
|
+
const KIT_CLAUSE = { expanded: '^K switch kit' };
|
|
52
61
|
const DETAIL_CLAUSE = { collapsed: '^G more', expanded: '^G less' };
|
|
53
62
|
/** L3 gives the key name and nothing else: discoverability at the last width. */
|
|
54
63
|
const BRANCH_KEY_ONLY = '^L';
|
|
@@ -77,7 +86,7 @@ export function planHeaderHints(input) {
|
|
|
77
86
|
const detail = input.statusExpanded ? DETAILS_ON : DETAILS_OFF;
|
|
78
87
|
return buildPlan([{ id: 'detail', text: detail }], scrollCells, true);
|
|
79
88
|
}
|
|
80
|
-
const ladder = ladderFor(input.statusExpanded);
|
|
89
|
+
const ladder = ladderFor(input.statusExpanded, input.kitHint !== false);
|
|
81
90
|
// THE SEPARATOR IS SPENT OUT OF THE SAME ROW (defect 12): left half + gap + this
|
|
82
91
|
// column have to add up to `columns`, so the gap comes off the budget instead of
|
|
83
92
|
// being assumed free.
|
|
@@ -94,8 +103,18 @@ export function planHeaderHints(input) {
|
|
|
94
103
|
// shrinks (to zero), and `overflow="hidden"` keeps that from overflowing.
|
|
95
104
|
return buildPlan(ladder[ladder.length - 1] ?? [], scrollCells, false);
|
|
96
105
|
}
|
|
97
|
-
/**
|
|
98
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The rungs, widest first. Built per call because the widths are text-derived.
|
|
108
|
+
*
|
|
109
|
+
* THE EXPANDED ORDER `branch -> index -> kit -> detail` IS A REGRESSION
|
|
110
|
+
* CONSTRAINT, NOT A PREFERENCE (RV-P1-2). With `kit` AFTER `index`, rungs E2,
|
|
111
|
+
* E4, E5 and E6 are exactly today's four expanded rungs, so every existing
|
|
112
|
+
* width assertion for 48-78 columns keeps its old answer and `^I` never
|
|
113
|
+
* disappears from a row where it used to fit. Putting `^K` first would have cost
|
|
114
|
+
* `^I` everything from 64 to 78 columns - a permanent loss, paid for a
|
|
115
|
+
* low-frequency key.
|
|
116
|
+
*/
|
|
117
|
+
function ladderFor(statusExpanded, kitHint) {
|
|
99
118
|
const branch = {
|
|
100
119
|
id: 'branch',
|
|
101
120
|
text: statusExpanded ? BRANCH_CLAUSE.expanded : BRANCH_CLAUSE.collapsed,
|
|
@@ -108,8 +127,21 @@ function ladderFor(statusExpanded) {
|
|
|
108
127
|
id: 'detail',
|
|
109
128
|
text: statusExpanded ? DETAIL_CLAUSE.expanded : DETAIL_CLAUSE.collapsed,
|
|
110
129
|
};
|
|
130
|
+
const base = [
|
|
131
|
+
[branch, index, detail],
|
|
132
|
+
[branch, detail],
|
|
133
|
+
[{ id: 'branch', text: BRANCH_KEY_ONLY }, detail],
|
|
134
|
+
[detail],
|
|
135
|
+
];
|
|
136
|
+
// THE COLLAPSED LADDER IS UNTOUCHED (D-11): `^K` has no collapsed form, so a
|
|
137
|
+
// user who never presses `ctrl+G` sees the row they always saw (I-9).
|
|
138
|
+
if (!statusExpanded || !kitHint)
|
|
139
|
+
return base;
|
|
140
|
+
const kit = { id: 'kit', text: KIT_CLAUSE.expanded };
|
|
111
141
|
return [
|
|
142
|
+
[branch, index, kit, detail],
|
|
112
143
|
[branch, index, detail],
|
|
144
|
+
[branch, kit, detail],
|
|
113
145
|
[branch, detail],
|
|
114
146
|
[{ id: 'branch', text: BRANCH_KEY_ONLY }, detail],
|
|
115
147
|
[detail],
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The status bar's kit chip: pure width planning, no JSX
|
|
3
|
+
* (agent-model-kits §4.8 / D-12).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHY A PLANNER, AND WHY IT IS THE SAME SHAPE AS `session-chip.ts`: the status
|
|
8
|
+
* row is measured cell by cell, and anything appended "just to the right" eats
|
|
9
|
+
* `phase` - the one field a user needs while a run is in flight. The chip
|
|
10
|
+
* therefore competes for the SAME budget as every other field, and the rung that
|
|
11
|
+
* survives is computed rather than hand-tuned.
|
|
12
|
+
*
|
|
13
|
+
* ONE COMPARISON, NOT A THRESHOLD TABLE: `columns - (cells + gap) >=
|
|
14
|
+
* minStatusCells`. The width table in the spec is what that comparison WORKS
|
|
15
|
+
* OUT TO. `minStatusCells` is deliberately the same 34 as the session chip's -
|
|
16
|
+
* both answer "how much room must the left half keep", and two numbers for one
|
|
17
|
+
* question is how a "fit" and a "don't fit" end up disagreeing.
|
|
18
|
+
*/
|
|
19
|
+
export declare const KIT_CHIP_LIMITS: {
|
|
20
|
+
/** Blank cells between the chip and the field run. */
|
|
21
|
+
readonly gapCells: 2;
|
|
22
|
+
/** The floor the left half must keep; asserted equal to the session chip's. */
|
|
23
|
+
readonly minStatusCells: 34;
|
|
24
|
+
/** Below this the `kit` label is dropped before the name is shortened. */
|
|
25
|
+
readonly labelMinCols: 72;
|
|
26
|
+
/** The shortest unique prefix worth showing; below it the chip hides. */
|
|
27
|
+
readonly minNameChars: 4;
|
|
28
|
+
};
|
|
29
|
+
export interface KitChipPlan {
|
|
30
|
+
visible: boolean;
|
|
31
|
+
text: string;
|
|
32
|
+
cells: number;
|
|
33
|
+
gap: number;
|
|
34
|
+
}
|
|
35
|
+
export interface KitChipInput {
|
|
36
|
+
columns: number;
|
|
37
|
+
/**
|
|
38
|
+
* The kit in effect, or absent/null when there is none.
|
|
39
|
+
*
|
|
40
|
+
* ABSENT IS NOT EMPTY: with no kit the plan is hidden and the row's output is
|
|
41
|
+
* byte-identical to the pre-feature build (I-9).
|
|
42
|
+
*/
|
|
43
|
+
kit?: {
|
|
44
|
+
name: string;
|
|
45
|
+
modified: boolean;
|
|
46
|
+
otherNames: readonly string[];
|
|
47
|
+
} | null | undefined;
|
|
48
|
+
}
|
|
49
|
+
/** Plan the chip for `columns` and this session's kit. */
|
|
50
|
+
export declare function planKitChip(input: KitChipInput): KitChipPlan;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The status bar's kit chip: pure width planning, no JSX
|
|
3
|
+
* (agent-model-kits §4.8 / D-12).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHY A PLANNER, AND WHY IT IS THE SAME SHAPE AS `session-chip.ts`: the status
|
|
8
|
+
* row is measured cell by cell, and anything appended "just to the right" eats
|
|
9
|
+
* `phase` - the one field a user needs while a run is in flight. The chip
|
|
10
|
+
* therefore competes for the SAME budget as every other field, and the rung that
|
|
11
|
+
* survives is computed rather than hand-tuned.
|
|
12
|
+
*
|
|
13
|
+
* ONE COMPARISON, NOT A THRESHOLD TABLE: `columns - (cells + gap) >=
|
|
14
|
+
* minStatusCells`. The width table in the spec is what that comparison WORKS
|
|
15
|
+
* OUT TO. `minStatusCells` is deliberately the same 34 as the session chip's -
|
|
16
|
+
* both answer "how much room must the left half keep", and two numbers for one
|
|
17
|
+
* question is how a "fit" and a "don't fit" end up disagreeing.
|
|
18
|
+
*/
|
|
19
|
+
import stringWidth from 'string-width';
|
|
20
|
+
export const KIT_CHIP_LIMITS = {
|
|
21
|
+
/** Blank cells between the chip and the field run. */
|
|
22
|
+
gapCells: 2,
|
|
23
|
+
/** The floor the left half must keep; asserted equal to the session chip's. */
|
|
24
|
+
minStatusCells: 34,
|
|
25
|
+
/** Below this the `kit` label is dropped before the name is shortened. */
|
|
26
|
+
labelMinCols: 72,
|
|
27
|
+
/** The shortest unique prefix worth showing; below it the chip hides. */
|
|
28
|
+
minNameChars: 4,
|
|
29
|
+
};
|
|
30
|
+
const HIDDEN = { visible: false, text: '', cells: 0, gap: 0 };
|
|
31
|
+
/** The shortest prefix of `name` that no sibling name also starts with. */
|
|
32
|
+
function uniqueLength(name, others) {
|
|
33
|
+
const points = [...name];
|
|
34
|
+
const start = Math.min(KIT_CHIP_LIMITS.minNameChars, points.length);
|
|
35
|
+
const shared = (length) => {
|
|
36
|
+
const prefix = points.slice(0, length).join('');
|
|
37
|
+
return others.some((other) => [...other].slice(0, length).join('') === prefix);
|
|
38
|
+
};
|
|
39
|
+
let length = start;
|
|
40
|
+
while (length < points.length && shared(length))
|
|
41
|
+
length += 1;
|
|
42
|
+
return Math.max(length, start);
|
|
43
|
+
}
|
|
44
|
+
/** Plan the chip for `columns` and this session's kit. */
|
|
45
|
+
export function planKitChip(input) {
|
|
46
|
+
const kit = input.kit;
|
|
47
|
+
if (!kit)
|
|
48
|
+
return HIDDEN;
|
|
49
|
+
const name = kit.name.trim();
|
|
50
|
+
if (!name)
|
|
51
|
+
return HIDDEN;
|
|
52
|
+
const columns = Number.isFinite(input.columns) ? Math.floor(input.columns) : 80;
|
|
53
|
+
const gap = KIT_CHIP_LIMITS.gapCells;
|
|
54
|
+
const fits = (text) => columns - (stringWidth(text) + gap) >= KIT_CHIP_LIMITS.minStatusCells;
|
|
55
|
+
const mark = kit.modified ? '~' : '';
|
|
56
|
+
const plan = (text) => ({ visible: true, text, cells: stringWidth(text), gap });
|
|
57
|
+
// Widest rung first, then drop the label, then shorten the name, then hide.
|
|
58
|
+
// THE ORDER IS THE DECISION: a reader cares WHICH kit is in effect before they
|
|
59
|
+
// care that it is a kit, so the name is the last thing to go.
|
|
60
|
+
//
|
|
61
|
+
// `KIT_CHIP_LIMITS.labelMinCols` NEEDS NO BRANCH: a labelled rung is 45 cells
|
|
62
|
+
// for a 40-character name, so `fits` already rejects it below ~81 columns. The
|
|
63
|
+
// constant is the documented intent (labels belong to wide rows), measured
|
|
64
|
+
// rather than hard-coded into the ladder.
|
|
65
|
+
const points = [...name];
|
|
66
|
+
const shortest = uniqueLength(name, kit.otherNames);
|
|
67
|
+
const rungs = [];
|
|
68
|
+
if (kit.modified)
|
|
69
|
+
rungs.push(`kit ${name}~`);
|
|
70
|
+
rungs.push(`kit ${name}`);
|
|
71
|
+
if (kit.modified)
|
|
72
|
+
rungs.push(`${name}~`);
|
|
73
|
+
rungs.push(name);
|
|
74
|
+
for (let length = points.length - 1; length >= shortest; length -= 1) {
|
|
75
|
+
rungs.push(`${points.slice(0, length).join('')}${mark}`);
|
|
76
|
+
}
|
|
77
|
+
for (const text of rungs)
|
|
78
|
+
if (fits(text))
|
|
79
|
+
return plan(text);
|
|
80
|
+
return HIDDEN;
|
|
81
|
+
}
|
|
@@ -38,6 +38,7 @@ import { COMPACTION_LIMITS } from '../../compaction/limits.js';
|
|
|
38
38
|
import { GUTTER_WIDTH } from './Gutter.js';
|
|
39
39
|
import { expandTabs } from '../display-tab.js';
|
|
40
40
|
import { USER_ENTRY_MAX_ROWS } from '../composer-limits.js';
|
|
41
|
+
import { isLiveTeamCard } from '../team-status.js';
|
|
41
42
|
export const VIRTUAL_LIMITS = {
|
|
42
43
|
/**
|
|
43
44
|
* Entries kept mounted on each side of the visible band.
|
|
@@ -315,6 +316,14 @@ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisibl
|
|
|
315
316
|
return separation + 1 + shown + 1;
|
|
316
317
|
}
|
|
317
318
|
case 'team':
|
|
319
|
+
// A LIVE DISPATCH CARD IS NOT DRAWN AT ALL (tui-subagent-double-roster A-2):
|
|
320
|
+
// the roster is the only live subagent surface, so this entry owes the window
|
|
321
|
+
// ZERO rows. Estimating it at its real height while `EntryView` renders nothing
|
|
322
|
+
// would make `selectWindow` reserve four rows for a card that is not on screen -
|
|
323
|
+
// blank space at the bottom of every dispatch frame, and a window boundary that
|
|
324
|
+
// disagrees with what the printer measures.
|
|
325
|
+
if (isLiveTeamCard(entry))
|
|
326
|
+
return 0;
|
|
318
327
|
return separation + 1 + entry.runs.length + (expanded ? entry.runs.length * 3 + 1 : 1);
|
|
319
328
|
case 'todo':
|
|
320
329
|
return separation + 1 + entry.items.length;
|
|
@@ -46,6 +46,12 @@ function allKeyRows(times) {
|
|
|
46
46
|
['Ctrl+R', 'Redraw the frame'],
|
|
47
47
|
['Ctrl+T', 'Show/hide thinking (off by default)'],
|
|
48
48
|
['Ctrl+I', 'Build the project index (bundled skill; asks first; needs keyboard enhancement)'],
|
|
49
|
+
// THE KIT KEYS (agent-model-kits D-11 / D-15): `Ctrl+K` moved to the picker
|
|
50
|
+
// and `killToEnd` moved to `Alt+K`. BOTH rows are listed, because an existing
|
|
51
|
+
// user will press the old key by muscle memory and needs to find out where it
|
|
52
|
+
// went - and because `^K` is only advertised in the expanded header row.
|
|
53
|
+
['Ctrl+K', 'Agent kits: pick a preset for this session (see /kit)'],
|
|
54
|
+
['Alt+K', 'Delete from the cursor to the end of the line (was Ctrl+K)'],
|
|
49
55
|
['Ctrl+G', 'Expand/collapse status details; disabled while an overlay is open'],
|
|
50
56
|
['PgUp / PgDn', 'Scroll a page (transcript, or this overlay)'],
|
|
51
57
|
['Shift+Up / Shift+Down', 'Scroll the transcript a line (full-screen mode)'],
|
|
@@ -85,6 +91,7 @@ const COMMANDS = [
|
|
|
85
91
|
['/max-tokens [n|auto]', 'Output token cap; no argument reports the effective one'],
|
|
86
92
|
['/team [on|off|max n]', 'Team subagents: status, switch, fan-out width'],
|
|
87
93
|
['/fast [on|off|model|review]', 'Fast model tier: status, switch, model, review cadence'],
|
|
94
|
+
['/kit [list|use <name>]', 'Agent kits: what is in effect, list them, use one for this session'],
|
|
88
95
|
[
|
|
89
96
|
'/todo [on|off|panel|follow|clear|continue]',
|
|
90
97
|
'Todo planning: status, switches, the rail, follow-through',
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings > Kit pack > editor (agent-model-kits §4.5).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* FOUR ROWS AND TWO ACTIONS, and every slot is an explicit decision (D-8): the
|
|
7
|
+
* Main slot is required, the Fast slot is a two-state choice (`off` is a
|
|
8
|
+
* decision, not an omission), and the Subagent slot reuses the existing
|
|
9
|
+
* `Same as main` / `Fast tier model` vocabulary so the summary sentence the root
|
|
10
|
+
* page already prints (`Fast tier (off - falls back to main)`) needs no second
|
|
11
|
+
* wording. Nothing is written until the ROOT page's Save.
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import type { ModelProfileState } from './model-profile-state.js';
|
|
15
|
+
import type { KitAction, KitDraft } from './kit-settings-state.js';
|
|
16
|
+
import { type ProfileFrameProps } from './ModelProfilePicker.js';
|
|
17
|
+
interface KitEditorProps extends ProfileFrameProps {
|
|
18
|
+
draft: KitDraft;
|
|
19
|
+
state: ModelProfileState;
|
|
20
|
+
dispatch: React.Dispatch<KitAction>;
|
|
21
|
+
}
|
|
22
|
+
export declare function KitEditor(props: KitEditorProps): React.ReactElement;
|
|
23
|
+
export {};
|