@aragon-agent/cli 0.6.33 → 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/CHANGELOG.md +2305 -2242
- package/README.md +119 -18
- 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/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +1 -1
- 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 +135 -15
- 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 +38 -13
- package/dist/ui/ScrollbackShell.js +4 -3
- 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 +44 -10
- package/dist/ui/scrollback/LiveTail.js +20 -4
- package/dist/ui/scrollback/limits.d.ts +16 -0
- package/dist/ui/scrollback/limits.js +16 -0
- package/dist/ui/scrollback/live-preview.js +10 -3
- package/dist/ui/scrollback/overflow-print.d.ts +108 -0
- package/dist/ui/scrollback/overflow-print.js +120 -0
- package/dist/ui/scrollback/renderer.d.ts +11 -1
- package/dist/ui/scrollback/renderer.js +53 -25
- package/dist/ui/scrollback/tail-anchor.d.ts +68 -0
- package/dist/ui/scrollback/tail-anchor.js +77 -0
- package/dist/ui/scrollback/tail-window.d.ts +21 -8
- package/dist/ui/scrollback/tail-window.js +26 -10
- package/dist/ui/scrollback/use-scrollback-view.d.ts +38 -10
- package/dist/ui/scrollback/use-scrollback-view.js +372 -85
- 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/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 {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Settings > Kit pack > editor (agent-model-kits §4.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* FOUR ROWS AND TWO ACTIONS, and every slot is an explicit decision (D-8): the
|
|
8
|
+
* Main slot is required, the Fast slot is a two-state choice (`off` is a
|
|
9
|
+
* decision, not an omission), and the Subagent slot reuses the existing
|
|
10
|
+
* `Same as main` / `Fast tier model` vocabulary so the summary sentence the root
|
|
11
|
+
* page already prints (`Fast tier (off - falls back to main)`) needs no second
|
|
12
|
+
* wording. Nothing is written until the ROOT page's Save.
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import { Text, useInput } from 'ink';
|
|
16
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
17
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
18
|
+
import { stripPasteFrames } from '../paste-frames.js';
|
|
19
|
+
import { stripEnterFrames } from '../enter-frames.js';
|
|
20
|
+
import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
|
|
21
|
+
const ROWS = ['name', 'main', 'subagent', 'fast', 'save', 'cancel'];
|
|
22
|
+
function slotLabel(draft, state) {
|
|
23
|
+
const entries = state.draft.profiles?.entries ?? [];
|
|
24
|
+
const name = (id) => {
|
|
25
|
+
const entry = id ? entries.find((item) => item.id === id) : undefined;
|
|
26
|
+
// `Profile missing` rather than a blank: a kit can be pointed at a profile
|
|
27
|
+
// that was removed on disk, and the editor is where that gets fixed.
|
|
28
|
+
return entry ? `${entry.provider}:${entry.model} (${entry.name})` : id ? 'Profile missing'
|
|
29
|
+
: '(not set)';
|
|
30
|
+
};
|
|
31
|
+
const subagent = draft.subagent.model === 'main' ? 'Same as main'
|
|
32
|
+
: draft.subagent.model === 'fast' ? 'Fast tier model'
|
|
33
|
+
: name(draft.subagent.modelProfileId);
|
|
34
|
+
return { main: name(draft.mainProfileId), fast: name(draft.fastProfileId),
|
|
35
|
+
subagent };
|
|
36
|
+
}
|
|
37
|
+
export function KitEditor(props) {
|
|
38
|
+
const glyphs = pickGlyphs(props.caps);
|
|
39
|
+
const { draft, dispatch } = props;
|
|
40
|
+
const [index, setIndex] = useState(0);
|
|
41
|
+
const row = ROWS[index];
|
|
42
|
+
const offset = useProfileScroll(props, index);
|
|
43
|
+
const labels = slotLabel(draft, props.state);
|
|
44
|
+
const filled = [draft.mainProfileId, draft.fastProfileId,
|
|
45
|
+
draft.subagent.model !== 'profile' || draft.subagent.modelProfileId]
|
|
46
|
+
.filter(Boolean).length;
|
|
47
|
+
function activate(target) {
|
|
48
|
+
if (target === 'main')
|
|
49
|
+
dispatch({ type: 'kit-slot', slot: 'main' });
|
|
50
|
+
if (target === 'fast')
|
|
51
|
+
dispatch({ type: 'kit-slot', slot: 'fast' });
|
|
52
|
+
if (target === 'subagent')
|
|
53
|
+
dispatch({ type: 'kit-slot', slot: 'subagent' });
|
|
54
|
+
if (target === 'save')
|
|
55
|
+
dispatch({ type: 'kit-save' });
|
|
56
|
+
if (target === 'cancel')
|
|
57
|
+
dispatch({ type: 'kit-cancel' });
|
|
58
|
+
}
|
|
59
|
+
useInput((input, key) => {
|
|
60
|
+
if (props.isActive === false)
|
|
61
|
+
return;
|
|
62
|
+
if (key.escape) {
|
|
63
|
+
dispatch({ type: 'kit-cancel' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
67
|
+
setIndex((index - 1 + ROWS.length) % ROWS.length);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (key.downArrow || key.tab) {
|
|
71
|
+
setIndex((index + 1) % ROWS.length);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (key.return) {
|
|
75
|
+
activate(row);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (row !== 'name' || key.ctrl || key.meta)
|
|
79
|
+
return;
|
|
80
|
+
// THE NAME IS THE ONLY TEXT FIELD, and control characters are stripped by the
|
|
81
|
+
// reducer rather than refused here - a pasted name with a newline should
|
|
82
|
+
// become a name, not an error the user cannot see.
|
|
83
|
+
const text = stripPasteFrames(stripEnterFrames(input));
|
|
84
|
+
if (key.backspace || key.delete) {
|
|
85
|
+
dispatch({ type: 'kit-name', value: [...draft.name].slice(0, -1).join('') });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (text)
|
|
89
|
+
dispatch({ type: 'kit-name', value: `${draft.name}${text}` });
|
|
90
|
+
});
|
|
91
|
+
const lines = [`Name ${draft.name}`,
|
|
92
|
+
`Main profile ${labels.main}`,
|
|
93
|
+
`Subagent ${labels.subagent}`,
|
|
94
|
+
`Fast tier ${draft.fastProfileId ? labels.fast : 'off (this kit turns it off)'}`,
|
|
95
|
+
`Save (${filled} slots set)`, 'Cancel'];
|
|
96
|
+
const rows = lines.map((line, at) => (_jsx(Text, { wrap: "truncate", color: at === index ? props.theme.accent : props.theme.muted, children: `${at === index ? glyphs.caret : ' '} ${clipProfileText(line, props.cols - 8)}` }, at)));
|
|
97
|
+
if (draft.error) {
|
|
98
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.noticeError, children: clipProfileText(draft.error, props.cols - 8) }, "error"));
|
|
99
|
+
}
|
|
100
|
+
return _jsx(OverlayFrame, { ...props, title: draft.id ? 'Edit kit' : 'New kit', hint: "Enter choose | type to rename | Esc back", rows: rows, scrollOffset: offset });
|
|
101
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings > Kit pack: the manager page (agent-model-kits §4.5 / D-9).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* CRUD LIVES HERE AND NOWHERE ELSE: `New kit` and `e` open the editor, `d` asks
|
|
7
|
+
* a confirmation that NAMES the consequences, `s` runs exit A (three slots plus
|
|
8
|
+
* the persisted pointer, because a default that only moved a pointer would not
|
|
9
|
+
* be in effect at the next launch - RV-P0-3), and `Enter` applies a kit to the
|
|
10
|
+
* transaction draft. Nothing on this page writes the config file.
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { type ProfileFrameProps } from './ModelProfilePicker.js';
|
|
14
|
+
import type { KitAction, KitManagerRow } from './kit-settings-state.js';
|
|
15
|
+
interface KitManagerProps extends ProfileFrameProps {
|
|
16
|
+
rows: KitManagerRow[];
|
|
17
|
+
error?: string;
|
|
18
|
+
dispatch: React.Dispatch<KitAction>;
|
|
19
|
+
/** Leave the kit pages entirely: `Esc` here means "back to Settings". */
|
|
20
|
+
onBack: () => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function KitManager(props: KitManagerProps): React.ReactElement;
|
|
23
|
+
export {};
|