@aragon-agent/cli 0.6.45 → 0.6.46
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 +51 -0
- package/dist/agent/invocation-card.d.ts +77 -0
- package/dist/agent/invocation-card.js +169 -0
- package/dist/agent/prompt-options.d.ts +6 -0
- package/dist/agent/queued-messages.d.ts +3 -0
- package/dist/agent/queued-messages.js +4 -1
- package/dist/agent/reducer.d.ts +5 -0
- package/dist/agent/reducer.js +12 -3
- package/dist/commands/builtins.js +12 -0
- package/dist/commands/governance-command.js +13 -1
- package/dist/commands/index-build.d.ts +8 -0
- package/dist/commands/index-build.js +11 -0
- package/dist/commands/registry.d.ts +23 -0
- package/dist/commands/registry.js +20 -7
- package/dist/commands/skills.js +36 -2
- package/dist/commands/subagent-command.js +3 -1
- package/dist/commands/team-command.d.ts +6 -0
- package/dist/commands/team-command.js +40 -3
- package/dist/session/persist.js +10 -0
- package/dist/session/validate-session.js +4 -2
- package/dist/team/forced-mode.d.ts +19 -0
- package/dist/team/forced-mode.js +42 -20
- package/dist/ui/App.js +25 -6
- package/dist/ui/Transcript.js +4 -1
- package/dist/ui/entries/InvocationCard.d.ts +20 -0
- package/dist/ui/entries/InvocationCard.js +32 -0
- package/dist/ui/layout/invocation-card.d.ts +41 -0
- package/dist/ui/layout/invocation-card.js +91 -0
- package/dist/ui/layout/virtual-window.js +11 -1
- package/dist/ui/transcript-text.js +4 -1
- package/package.json +2 -2
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* file therefore holds ONE delegation and one line of deprecation text, and
|
|
18
18
|
* `subagent-command.test.ts` asserts both (I-8).
|
|
19
19
|
*/
|
|
20
|
-
import { runTeamCommand } from './team-command.js';
|
|
20
|
+
import { runTeamCommand, teamRendersInvocationCard } from './team-command.js';
|
|
21
21
|
/**
|
|
22
22
|
* Run `/team`, having first said out loud that this is the same thing.
|
|
23
23
|
*
|
|
@@ -33,6 +33,8 @@ export function runSubagentCommand(ctx) {
|
|
|
33
33
|
export function registerSubagentCommand(registry) {
|
|
34
34
|
registry.register({
|
|
35
35
|
name: 'subagent',
|
|
36
|
+
// A fresh object inherits nothing from `/team`: reference the SAME predicate (RV-5).
|
|
37
|
+
rendersInvocationCard: teamRendersInvocationCard,
|
|
36
38
|
description: 'Deprecated alias of /team - same arguments',
|
|
37
39
|
run: (ctx) => { runSubagentCommand(ctx); },
|
|
38
40
|
});
|
|
@@ -43,6 +43,12 @@
|
|
|
43
43
|
* `Team 4` chip would wait for an unrelated state change before appearing.
|
|
44
44
|
*/
|
|
45
45
|
import type { CommandContext, CommandRegistry } from './registry.js';
|
|
46
|
+
/**
|
|
47
|
+
* Whether `/team <args>` ends in a card. The SAME function the command branches on
|
|
48
|
+
* (`readTeamIntent`), and shared by `/team` and the `/subagent` alias by reference: the
|
|
49
|
+
* alias builds a fresh object and inherits nothing (RV-5, I-8).
|
|
50
|
+
*/
|
|
51
|
+
export declare function teamRendersInvocationCard(args: string): boolean;
|
|
46
52
|
/** The command body; `registerTeamCommand` is the only registration site. */
|
|
47
53
|
export declare function runTeamCommand(ctx: CommandContext): void;
|
|
48
54
|
/** Registered from `builtins.ts` with one line, next to every other subsystem. */
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* `Team 4` chip would wait for an unrelated state change before appearing.
|
|
44
44
|
*/
|
|
45
45
|
import { buildForcedDirective } from '../team/forced-dispatch.js';
|
|
46
|
-
import { DEFAULT_FORCED_COUNT, planForced, planNotice, readTeamIntent, } from '../team/forced-mode.js';
|
|
46
|
+
import { DEFAULT_FORCED_COUNT, forcedCardNote, planForced, planNotice, readTeamIntent, } from '../team/forced-mode.js';
|
|
47
47
|
import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
|
|
48
48
|
/**
|
|
49
49
|
* The subagent model line, phrased by the same authority the settings root row
|
|
@@ -199,6 +199,34 @@ function enterForcedMode(ctx) {
|
|
|
199
199
|
ctx.notify('info', `Forced team mode on: ${adopted} subagents per turn. `
|
|
200
200
|
+ 'Your next message dispatches.');
|
|
201
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* The card's raw material for `/team <brief>` (tui-invocation-cards section 3.3). The
|
|
204
|
+
* command line is always spelled `/team`, even when the user came in through the
|
|
205
|
+
* deprecated `/subagent` alias: the card names the canonical command.
|
|
206
|
+
*/
|
|
207
|
+
function forcedCardInput(args, planned, note, ctx) {
|
|
208
|
+
const noun = planned.count === 1 ? 'subagent' : 'subagents';
|
|
209
|
+
const facts = [
|
|
210
|
+
planned.parts.length > 0 ? `parts ${planned.parts.join(', ')}` : `${planned.count} ${noun}`,
|
|
211
|
+
...(planned.readOnly ? ['read-only'] : []),
|
|
212
|
+
...(ctx.controller.getAgentMode() === 'plan' ? ['plan mode'] : []),
|
|
213
|
+
];
|
|
214
|
+
return {
|
|
215
|
+
variant: 'team',
|
|
216
|
+
commandLine: args === '' ? '/team' : `/team ${args}`,
|
|
217
|
+
input: planned.brief,
|
|
218
|
+
facts,
|
|
219
|
+
...(note !== undefined ? { note } : {}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Whether `/team <args>` ends in a card. The SAME function the command branches on
|
|
224
|
+
* (`readTeamIntent`), and shared by `/team` and the `/subagent` alias by reference: the
|
|
225
|
+
* alias builds a fresh object and inherits nothing (RV-5, I-8).
|
|
226
|
+
*/
|
|
227
|
+
export function teamRendersInvocationCard(args) {
|
|
228
|
+
return readTeamIntent(args).kind === 'brief';
|
|
229
|
+
}
|
|
202
230
|
/** `/team [-n n|--parts a,b] [--read-only] <brief>`: arm and submit in one act. */
|
|
203
231
|
function armAndSubmit(ctx, intent) {
|
|
204
232
|
const team = ctx.controller.getTeamConfig();
|
|
@@ -209,7 +237,11 @@ function armAndSubmit(ctx, intent) {
|
|
|
209
237
|
// directive names: the later turns reuse this count, so the first turn and the
|
|
210
238
|
// rest of the session cannot disagree about how wide the fan-out is (D-7).
|
|
211
239
|
ctx.controller.armForcedTeam(planned.count);
|
|
212
|
-
|
|
240
|
+
// With cards on, the disclosure rides on the card (`forcedCardNote`, same source as
|
|
241
|
+
// `planNotice`); with them off this is the pre-card notice, byte for byte.
|
|
242
|
+
const cards = ctx.invocationCards !== false;
|
|
243
|
+
if (!cards)
|
|
244
|
+
ctx.notify('info', planNotice(planned, team));
|
|
213
245
|
const directive = buildForcedDirective({
|
|
214
246
|
brief: planned.brief,
|
|
215
247
|
count: planned.count,
|
|
@@ -222,7 +254,11 @@ function armAndSubmit(ctx, intent) {
|
|
|
222
254
|
});
|
|
223
255
|
// `userInitiated: false`: a directive the user did not type belongs in neither
|
|
224
256
|
// the prompt history nor the hint-fade counter (todo-plan-followthrough C-12).
|
|
225
|
-
|
|
257
|
+
const note = forcedCardNote(planned, team);
|
|
258
|
+
ctx.submit(directive, {
|
|
259
|
+
userInitiated: false,
|
|
260
|
+
...(cards ? { invocation: forcedCardInput(ctx.args, planned, note, ctx) } : {}),
|
|
261
|
+
});
|
|
226
262
|
}
|
|
227
263
|
/** The command body; `registerTeamCommand` is the only registration site. */
|
|
228
264
|
export function runTeamCommand(ctx) {
|
|
@@ -253,6 +289,7 @@ export function runTeamCommand(ctx) {
|
|
|
253
289
|
export function registerTeamCommand(registry) {
|
|
254
290
|
registry.register({
|
|
255
291
|
name: 'team',
|
|
292
|
+
rendersInvocationCard: teamRendersInvocationCard,
|
|
256
293
|
description: 'Force team mode and start now: /team <brief>; /team [on|off|status]',
|
|
257
294
|
run: (ctx) => { runTeamCommand(ctx); },
|
|
258
295
|
});
|
package/dist/session/persist.js
CHANGED
|
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
9
9
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
10
10
|
import { getSessionsDir } from '../config/store.js';
|
|
11
11
|
import { stripEphemeralRuns } from '../team/settled.js';
|
|
12
|
+
import { sanitizeInvocationCard } from '../agent/invocation-card.js';
|
|
12
13
|
import { validateSession } from './validate-session.js';
|
|
13
14
|
import { validateCompactionIdentity, verifyCompactionIdentity, } from '../compaction/memory-identity.js';
|
|
14
15
|
import { getLogger } from '../logging/logger.js';
|
|
@@ -227,6 +228,15 @@ export function normalizeLoadedEntries(entries) {
|
|
|
227
228
|
// THE ID IS KEPT: `restoreEntries` splices these positions into the
|
|
228
229
|
// live transcript, and a fresh id here would collide with the next
|
|
229
230
|
// `e<n>` the reducer allocates.
|
|
231
|
+
if (entry.kind === 'user' && 'invocation' in entry) {
|
|
232
|
+
// THE EIGHTH CLAUSE (tui-invocation-cards I-7). A session file is user-editable
|
|
233
|
+
// and `shape()` ignores undeclared fields, so a card arrives unchecked. Repair
|
|
234
|
+
// what can be repaired; a card that cannot be is dropped and the entry degrades
|
|
235
|
+
// to a plain user message - a corrupt card must never cost the session.
|
|
236
|
+
const { invocation: stored, ...plain } = entry;
|
|
237
|
+
const card = sanitizeInvocationCard(stored);
|
|
238
|
+
return card ? { ...plain, invocation: card } : plain;
|
|
239
|
+
}
|
|
230
240
|
if (entry.kind === 'queued') {
|
|
231
241
|
const downgraded = {
|
|
232
242
|
id: entry.id,
|
|
@@ -145,14 +145,16 @@ const subagent = shape({
|
|
|
145
145
|
retry: optional(shape({ attempt: number, maxRetries: number })),
|
|
146
146
|
});
|
|
147
147
|
const ENTRY_FIELDS = {
|
|
148
|
-
|
|
148
|
+
// `invocation` is declared for self-description only: `shape()` ignores undeclared
|
|
149
|
+
// fields, and `normalizeLoadedEntries` repairs or drops a bad card (tui-invocation-cards).
|
|
150
|
+
user: { text: string, invocation: optional(() => { }) },
|
|
149
151
|
// A GOVERNANCE CARD IS HISTORY, so it is validated LENIENTLY (governance-mode 10.4):
|
|
150
152
|
// unknown fields are ignored (`shape` only reads the declared ones) and `card` is not
|
|
151
153
|
// shape-checked here at all - `sanitizeGovernanceEntries` below repairs a card it can
|
|
152
154
|
// and degrades one it cannot to a one-line notice, so a corrupt footnote never costs
|
|
153
155
|
// the session. `at` is advisory.
|
|
154
156
|
governance: { at: optional(number), card: () => { } },
|
|
155
|
-
queued: { text: string, queueId: optional(nonempty) },
|
|
157
|
+
queued: { text: string, queueId: optional(nonempty), invocation: optional(() => { }) },
|
|
156
158
|
notice: { text: string, level: enumeration('info', 'warn', 'error') },
|
|
157
159
|
assistant: {
|
|
158
160
|
text: string, thinking: optional(string), thinkingOpen: boolean, streaming: boolean,
|
|
@@ -106,6 +106,19 @@ export interface ForcedPlan {
|
|
|
106
106
|
* can apply, because only it can read the live config.
|
|
107
107
|
*/
|
|
108
108
|
export declare function planForced(args: SubagentCommandArgs, team: TeamConfig): ForcedPlan;
|
|
109
|
+
/**
|
|
110
|
+
* The parenthesised disclosure sentence of the plan notice - why the fan-out is not what
|
|
111
|
+
* the user typed - or `null` when nothing was narrowed.
|
|
112
|
+
*
|
|
113
|
+
* SHARED BY `planNotice` AND `forcedCardNote` (tui-invocation-cards RV-4): the notice and
|
|
114
|
+
* the transcript card must word the same fact the same way, so there is exactly one
|
|
115
|
+
* place that decides it.
|
|
116
|
+
*
|
|
117
|
+
* TWO CEILINGS, TWO SENTENCES. `forcedPartsMax` bounds what one argument may carry and
|
|
118
|
+
* `team.maxSubagents` bounds what may be dispatched; naming the wrong one sends the user
|
|
119
|
+
* to change a number that is not the reason.
|
|
120
|
+
*/
|
|
121
|
+
export declare function clampDisclosure(planned: ForcedPlan, team: TeamConfig): string | null;
|
|
109
122
|
/**
|
|
110
123
|
* The one line that says what the NEXT turn will do - a plan, not a result.
|
|
111
124
|
*
|
|
@@ -114,3 +127,9 @@ export declare function planForced(args: SubagentCommandArgs, team: TeamConfig):
|
|
|
114
127
|
* said "4 subagents started" would be a promise the command cannot keep.
|
|
115
128
|
*/
|
|
116
129
|
export declare function planNotice(planned: ForcedPlan, team: TeamConfig): string;
|
|
130
|
+
/**
|
|
131
|
+
* The card's `note`: the same disclosure, as a sentence. `undefined` when nothing was
|
|
132
|
+
* narrowed - the "Esc stops the turn" follow-up is an operating hint the status bar
|
|
133
|
+
* carries, not a fact about this invocation.
|
|
134
|
+
*/
|
|
135
|
+
export declare function forcedCardNote(planned: ForcedPlan, team: TeamConfig): string | undefined;
|
package/dist/team/forced-mode.js
CHANGED
|
@@ -123,34 +123,56 @@ export function planForced(args, team) {
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
* The
|
|
126
|
+
* The parenthesised disclosure sentence of the plan notice - why the fan-out is not what
|
|
127
|
+
* the user typed - or `null` when nothing was narrowed.
|
|
127
128
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
129
|
+
* SHARED BY `planNotice` AND `forcedCardNote` (tui-invocation-cards RV-4): the notice and
|
|
130
|
+
* the transcript card must word the same fact the same way, so there is exactly one
|
|
131
|
+
* place that decides it.
|
|
132
|
+
*
|
|
133
|
+
* TWO CEILINGS, TWO SENTENCES. `forcedPartsMax` bounds what one argument may carry and
|
|
134
|
+
* `team.maxSubagents` bounds what may be dispatched; naming the wrong one sends the user
|
|
135
|
+
* to change a number that is not the reason.
|
|
131
136
|
*/
|
|
132
|
-
export function
|
|
133
|
-
const noun = planned.count === 1 ? 'subagent' : 'subagents';
|
|
134
|
-
const head = `Forcing a fan-out of ${planned.count} ${noun}`;
|
|
135
|
-
const followUp = `${head} (max ${team.maxSubagents}) - the next turn dispatches them. `
|
|
136
|
-
+ 'Esc stops the turn.';
|
|
137
|
-
// TWO CEILINGS, TWO SENTENCES. `forcedPartsMax` bounds what one argument may
|
|
138
|
-
// carry and `team.maxSubagents` bounds what may be dispatched; naming the wrong
|
|
139
|
-
// one sends the user to change a number that is not the reason.
|
|
137
|
+
export function clampDisclosure(planned, team) {
|
|
140
138
|
if (planned.parts.length > 0) {
|
|
141
139
|
if (planned.parts.length < planned.partsAdmitted) {
|
|
142
|
-
return
|
|
143
|
-
+ `team.maxSubagents is ${team.maxSubagents}).`;
|
|
140
|
+
return `asked for ${planned.partsRequested} parts; team.maxSubagents is ${team.maxSubagents}`;
|
|
144
141
|
}
|
|
145
142
|
if (planned.partsAdmitted < planned.partsRequested) {
|
|
146
|
-
return
|
|
147
|
-
+ `the first ${planned.parts.length} are used).`;
|
|
143
|
+
return `asked for ${planned.partsRequested} parts, the first ${planned.parts.length} are used`;
|
|
148
144
|
}
|
|
149
|
-
return
|
|
145
|
+
return null;
|
|
150
146
|
}
|
|
151
147
|
if (planned.requested > planned.count) {
|
|
152
|
-
return
|
|
153
|
-
+ `team.maxSubagents is ${team.maxSubagents}).`;
|
|
148
|
+
return `asked for ${planned.requested}; team.maxSubagents is ${team.maxSubagents}`;
|
|
154
149
|
}
|
|
155
|
-
return
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The one line that says what the NEXT turn will do - a plan, not a result.
|
|
154
|
+
*
|
|
155
|
+
* IT IS PRINTED BEFORE THE TURN IS SUBMITTED and it never claims children
|
|
156
|
+
* started: forcing is instruction-level for the first turn, and a notice that
|
|
157
|
+
* said "4 subagents started" would be a promise the command cannot keep.
|
|
158
|
+
*/
|
|
159
|
+
export function planNotice(planned, team) {
|
|
160
|
+
const noun = planned.count === 1 ? 'subagent' : 'subagents';
|
|
161
|
+
const head = `Forcing a fan-out of ${planned.count} ${noun}`;
|
|
162
|
+
const disclosure = clampDisclosure(planned, team);
|
|
163
|
+
if (disclosure !== null)
|
|
164
|
+
return `${head} (${disclosure}).`;
|
|
165
|
+
return `${head} (max ${team.maxSubagents}) - the next turn dispatches them. `
|
|
166
|
+
+ 'Esc stops the turn.';
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The card's `note`: the same disclosure, as a sentence. `undefined` when nothing was
|
|
170
|
+
* narrowed - the "Esc stops the turn" follow-up is an operating hint the status bar
|
|
171
|
+
* carries, not a fact about this invocation.
|
|
172
|
+
*/
|
|
173
|
+
export function forcedCardNote(planned, team) {
|
|
174
|
+
const disclosure = clampDisclosure(planned, team);
|
|
175
|
+
if (disclosure === null)
|
|
176
|
+
return undefined;
|
|
177
|
+
return `${disclosure.charAt(0).toUpperCase()}${disclosure.slice(1)}.`;
|
|
156
178
|
}
|
package/dist/ui/App.js
CHANGED
|
@@ -92,7 +92,8 @@ import { SettingsScreen, compactionSettingsFrom, fastSettingsFrom, } from './ove
|
|
|
92
92
|
import { ConfirmDialog } from './overlays/ConfirmDialog.js';
|
|
93
93
|
import { QuestionOverlay } from './overlays/QuestionOverlay.js';
|
|
94
94
|
import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
|
|
95
|
-
import { CommandRegistry, runSlashInput } from '../commands/registry.js';
|
|
95
|
+
import { CommandRegistry, parseSlashInput, runSlashInput, } from '../commands/registry.js';
|
|
96
|
+
import { buildInvocationCard, invocationCardsEnabled } from '../agent/invocation-card.js';
|
|
96
97
|
import { registerBuiltinCommands } from '../commands/builtins.js';
|
|
97
98
|
import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
|
|
98
99
|
import { MODE_TOGGLE_KEYS } from '../agent/agent-mode.js';
|
|
@@ -1942,8 +1943,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1942
1943
|
dispatch({ type: 'taskSummaryReady', view: outcome.view });
|
|
1943
1944
|
});
|
|
1944
1945
|
};
|
|
1946
|
+
const invocationCardsOn = invocationCardsEnabled();
|
|
1945
1947
|
const makeCtx = (args) => ({
|
|
1946
1948
|
args,
|
|
1949
|
+
invocationCards: invocationCardsOn,
|
|
1947
1950
|
requestCopy: copyCoordinator.requestCopy,
|
|
1948
1951
|
controller,
|
|
1949
1952
|
state: stateRef.current,
|
|
@@ -2121,10 +2124,17 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2121
2124
|
// must be the one that runs, whichever branch it takes.
|
|
2122
2125
|
cancelFollowThrough();
|
|
2123
2126
|
cancelConfirmation();
|
|
2127
|
+
// THE ONE PLACE A CARD IS BUILT (tui-invocation-cards I-3 / I-6): redact -> normalise ->
|
|
2128
|
+
// limit, with the same redactor the `Command:` echo uses. `message` below is the model's
|
|
2129
|
+
// input and is never touched; the card only replaces what the TRANSCRIPT shows.
|
|
2130
|
+
const card = invocationCardsOn && opts.invocation
|
|
2131
|
+
? buildInvocationCard(opts.invocation, (text) => redactText(text, getSecrets()))
|
|
2132
|
+
: undefined;
|
|
2133
|
+
const shown = card?.commandLine ?? message;
|
|
2124
2134
|
if (interactionPhase.current === 'running' || controller.isCompactionBusy?.()) {
|
|
2125
2135
|
try {
|
|
2126
2136
|
const queueId = controller.queueUserMessage(message);
|
|
2127
|
-
dispatch({ type: 'steerQueued', queueId, text:
|
|
2137
|
+
dispatch({ type: 'steerQueued', queueId, text: shown, ...(card ? { invocation: card } : {}) });
|
|
2128
2138
|
return { accepted: true };
|
|
2129
2139
|
}
|
|
2130
2140
|
catch (error) {
|
|
@@ -2143,7 +2153,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2143
2153
|
// the user has learned, so suppressing the fade is correct rather than
|
|
2144
2154
|
// incidental — which is why the parameter is named for the CAUSE (P2-8).
|
|
2145
2155
|
if (opts.userInitiated !== false) {
|
|
2146
|
-
|
|
2156
|
+
// A card's command line was already recorded by `executeSlashInput`; the payload
|
|
2157
|
+
// behind it must never reach the up-arrow history (I-9: the gate is unchanged).
|
|
2158
|
+
if (!card)
|
|
2159
|
+
recordPrompt(message);
|
|
2147
2160
|
// THE ONE PLACE THE NUDGE BUDGET IS REFILLED (P0-2 / I-10). Same predicate
|
|
2148
2161
|
// as `recordPrompt`, and for the same reason: the question is "did a human
|
|
2149
2162
|
// ask for this?", and only a human may hand the gate another attempt. A
|
|
@@ -2154,7 +2167,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2154
2167
|
// Mirrors where the reducer clears its own copies, in the same action
|
|
2155
2168
|
// (C-11): a new turn's end reason must not inherit the previous one's.
|
|
2156
2169
|
endReasonRef.current = { aborted: false, errored: false };
|
|
2157
|
-
dispatch({ type: 'submit', text:
|
|
2170
|
+
dispatch({ type: 'submit', text: shown, ...(card ? { invocation: card } : {}) });
|
|
2158
2171
|
const pre = controller.preflight();
|
|
2159
2172
|
if (!pre.ok) {
|
|
2160
2173
|
dispatch({ type: 'runError' });
|
|
@@ -2174,7 +2187,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2174
2187
|
// model call that describes a turn nobody ran; and below, in the queueing
|
|
2175
2188
|
// branch, the message is steering rather than a new task.
|
|
2176
2189
|
if (opts.userInitiated !== false)
|
|
2177
|
-
ensureTaskSummary(
|
|
2190
|
+
ensureTaskSummary(shown);
|
|
2178
2191
|
void controller.prompt(message, { todoPolicy: opts.todoPolicy ?? 'new-task' }).then((outcome) => {
|
|
2179
2192
|
if (!mounted.current || sequence !== submissionSequence.current)
|
|
2180
2193
|
return;
|
|
@@ -2213,7 +2226,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2213
2226
|
// Retain a recoverable command before any asynchronous handler can fail.
|
|
2214
2227
|
const safe = redactText(raw, getSecrets());
|
|
2215
2228
|
recordPrompt(safe);
|
|
2216
|
-
|
|
2229
|
+
// The echo is suppressed BEFORE the command runs and never deleted afterwards: printed
|
|
2230
|
+
// scrollback rows cannot be rewritten (I-5). The predicate is the command's own (I-8).
|
|
2231
|
+
const parsed = parseSlashInput(raw);
|
|
2232
|
+
const rendersCard = invocationCardsOn && parsed !== null
|
|
2233
|
+
&& registry.get(parsed.name)?.rendersInvocationCard?.(parsed.args) === true;
|
|
2234
|
+
if (!rendersCard)
|
|
2235
|
+
notify('info', `Command: ${safe}`);
|
|
2217
2236
|
try {
|
|
2218
2237
|
await runSlashInput(registry, raw, makeCtx);
|
|
2219
2238
|
}
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -8,6 +8,7 @@ import { pickGlyphs, toolGlyph } from './glyphs.js';
|
|
|
8
8
|
import { separationRows } from './density.js';
|
|
9
9
|
import { EntryFrame } from './entries/EntryFrame.js';
|
|
10
10
|
import { UserEntry } from './entries/UserEntry.js';
|
|
11
|
+
import { InvocationCard } from './entries/InvocationCard.js';
|
|
11
12
|
import { QueuedEntry } from './entries/QueuedEntry.js';
|
|
12
13
|
import { AssistantEntry } from './entries/AssistantEntry.js';
|
|
13
14
|
import { ToolCard, statusColor, toolEntryGlyph } from './entries/ToolCard.js';
|
|
@@ -78,7 +79,9 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
78
79
|
const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
|
|
79
80
|
switch (entry.kind) {
|
|
80
81
|
case 'user':
|
|
81
|
-
return frame(glyphs.user, theme.user,
|
|
82
|
+
return frame(glyphs.user, theme.user, entry.invocation
|
|
83
|
+
? _jsx(InvocationCard, { card: entry.invocation, theme: theme, caps: caps })
|
|
84
|
+
: _jsx(UserEntry, { text: entry.text, theme: theme, caps: caps }));
|
|
82
85
|
case 'queued':
|
|
83
86
|
// `theme.muted`: quieter than a user message, louder than a notice --
|
|
84
87
|
// the row exists to be noticed UNTIL it is accepted, then rewritten in
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a `user` entry that carries an invocation card (tui-invocation-cards).
|
|
3
|
+
*
|
|
4
|
+
* Like `UserEntry`, this renders CONTENT only: the role glyph, rail and spacing belong
|
|
5
|
+
* to `EntryFrame`, so the card stays "the user's turn" for density, turn counting and
|
|
6
|
+
* the settled boundary. The header text comes from `invocationHeaderSegments` and is
|
|
7
|
+
* never assembled here (RV-10); the row count it implies is `invocationCardRows`.
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import type { Theme } from '../theme.js';
|
|
11
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
12
|
+
import type { InvocationCard as InvocationCardData } from '../../agent/invocation-card.js';
|
|
13
|
+
declare function InvocationCardImpl({ card, theme, caps, }: {
|
|
14
|
+
card: InvocationCardData;
|
|
15
|
+
theme: Theme;
|
|
16
|
+
caps: TermCapabilities;
|
|
17
|
+
}): React.ReactElement;
|
|
18
|
+
/** Default comparator: `mapEntry` preserves identity for untouched entries (L2 / R3). */
|
|
19
|
+
export declare const InvocationCard: React.MemoExoticComponent<typeof InvocationCardImpl>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Render a `user` entry that carries an invocation card (tui-invocation-cards).
|
|
4
|
+
*
|
|
5
|
+
* Like `UserEntry`, this renders CONTENT only: the role glyph, rail and spacing belong
|
|
6
|
+
* to `EntryFrame`, so the card stays "the user's turn" for density, turn counting and
|
|
7
|
+
* the settled boundary. The header text comes from `invocationHeaderSegments` and is
|
|
8
|
+
* never assembled here (RV-10); the row count it implies is `invocationCardRows`.
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { Box, Text } from 'ink';
|
|
12
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
13
|
+
import { invocationHeaderSegments, invocationHiddenText, invocationInputText, } from '../layout/invocation-card.js';
|
|
14
|
+
import { GUTTER_WIDTH } from '../layout/Gutter.js';
|
|
15
|
+
import { useTerminalSize } from '../layout/useTerminalSize.js';
|
|
16
|
+
import { fitCells } from '../layout/cells.js';
|
|
17
|
+
function toneColor(tone, theme) {
|
|
18
|
+
if (tone === 'label')
|
|
19
|
+
return theme.accent;
|
|
20
|
+
if (tone === 'name')
|
|
21
|
+
return theme.user;
|
|
22
|
+
return theme.muted;
|
|
23
|
+
}
|
|
24
|
+
function InvocationCardImpl({ card, theme, caps, }) {
|
|
25
|
+
const glyphs = pickGlyphs(caps);
|
|
26
|
+
const { cols } = useTerminalSize();
|
|
27
|
+
const usable = Math.max(1, cols - GUTTER_WIDTH);
|
|
28
|
+
const header = invocationHeaderSegments(card, glyphs, usable);
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "truncate", children: header.map((part, index) => (_jsx(Text, { color: toneColor(part.tone, theme), bold: part.bold, children: part.text }, index))) }), card.input !== '' && _jsx(Text, { color: theme.user, children: invocationInputText(card) }), card.inputHidden > 0 && (_jsx(Text, { color: theme.muted, wrap: "truncate", children: invocationHiddenText(card, glyphs) })), card.note !== undefined && card.note !== '' && (_jsx(Text, { color: theme.muted, wrap: "truncate", children: fitCells(card.note, usable, glyphs.ellipsis) }))] }));
|
|
30
|
+
}
|
|
31
|
+
/** Default comparator: `mapEntry` preserves identity for untouched entries (L2 / R3). */
|
|
32
|
+
export const InvocationCard = React.memo(InvocationCardImpl);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invocation card layout (tui-invocation-cards section 3.7): what a `user` entry that
|
|
3
|
+
* carries an `invocation` says, as pure data. No Ink, no React. ASCII ONLY.
|
|
4
|
+
*
|
|
5
|
+
* THE CARD'S HEIGHT IS A FUNCTION OF ITS DATA AND THE USABLE WIDTH (I-4).
|
|
6
|
+
* `InvocationCard.tsx` renders exactly the rows `invocationCardRows` counts, and both
|
|
7
|
+
* read `INVOCATION_LIMITS` plus the one `invocationInputText` below - an estimate that
|
|
8
|
+
* disagrees with the painter is permanent, because an over-estimated entry is never
|
|
9
|
+
* mounted and so never measured (see `virtual-window.ts`).
|
|
10
|
+
*/
|
|
11
|
+
import type { InvocationCard } from '../../agent/invocation-card.js';
|
|
12
|
+
import type { Glyphs } from '../glyphs.js';
|
|
13
|
+
/** Colour slots the component maps onto the theme; kept local so this file needs no `Theme`. */
|
|
14
|
+
export type InvocationTone = 'label' | 'name' | 'muted';
|
|
15
|
+
export interface InvocationSegment {
|
|
16
|
+
text: string;
|
|
17
|
+
tone: InvocationTone;
|
|
18
|
+
bold: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The header row, already fitted to `usable` columns (RV-10). Narrow terminals drop
|
|
22
|
+
* facts from the right WHOLE before anything else is cut; only when the identity alone
|
|
23
|
+
* is too wide is the name shortened. Ink's `wrap="truncate"` stays on the row as a
|
|
24
|
+
* backstop, but it is never what produces the visible ellipsis.
|
|
25
|
+
*/
|
|
26
|
+
export declare function invocationHeaderSegments(card: InvocationCard, glyphs: Glyphs, usable: number): InvocationSegment[];
|
|
27
|
+
/** The text painted for `input`: tabs expanded per line, the same rule `UserEntry` follows. */
|
|
28
|
+
export declare function invocationInputText(card: InvocationCard): string;
|
|
29
|
+
/** Singular/plural for the hidden-characters tail. */
|
|
30
|
+
export declare function invocationHiddenText(card: InvocationCard, glyphs: Glyphs): string;
|
|
31
|
+
/**
|
|
32
|
+
* Rows the card occupies at `usable` columns. `wrappedRows` is injected by the caller
|
|
33
|
+
* (`virtual-window.ts` owns the one wrapping rule) so this file stays import-cycle free.
|
|
34
|
+
*/
|
|
35
|
+
export declare function invocationCardRows(card: InvocationCard, usable: number, wrappedRows: (text: string, usable: number) => number): number;
|
|
36
|
+
/**
|
|
37
|
+
* The lines the exit replay / export prints UNDER the command line (section 2.4):
|
|
38
|
+
* a bracketed fact line, and the disclosure note when there is one. Plain ASCII
|
|
39
|
+
* separators - the replay is pasted into issues and must survive any terminal.
|
|
40
|
+
*/
|
|
41
|
+
export declare function invocationCardLines(card: InvocationCard): string[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invocation card layout (tui-invocation-cards section 3.7): what a `user` entry that
|
|
3
|
+
* carries an `invocation` says, as pure data. No Ink, no React. ASCII ONLY.
|
|
4
|
+
*
|
|
5
|
+
* THE CARD'S HEIGHT IS A FUNCTION OF ITS DATA AND THE USABLE WIDTH (I-4).
|
|
6
|
+
* `InvocationCard.tsx` renders exactly the rows `invocationCardRows` counts, and both
|
|
7
|
+
* read `INVOCATION_LIMITS` plus the one `invocationInputText` below - an estimate that
|
|
8
|
+
* disagrees with the painter is permanent, because an over-estimated entry is never
|
|
9
|
+
* mounted and so never measured (see `virtual-window.ts`).
|
|
10
|
+
*/
|
|
11
|
+
import { expandTabs } from '../display-tab.js';
|
|
12
|
+
import { cellWidth, fitCells } from './cells.js';
|
|
13
|
+
/** Two spaces between the identity (`skill . pdf-forms`) and the facts. */
|
|
14
|
+
const FACT_GAP = ' ';
|
|
15
|
+
function seg(text, tone, bold = false) {
|
|
16
|
+
return { text, tone, bold };
|
|
17
|
+
}
|
|
18
|
+
function segmentsWidth(segments) {
|
|
19
|
+
return segments.reduce((sum, part) => sum + cellWidth(part.text), 0);
|
|
20
|
+
}
|
|
21
|
+
function identitySegments(card, glyphs, name) {
|
|
22
|
+
const parts = [seg(card.variant, 'label', true)];
|
|
23
|
+
if (name !== '')
|
|
24
|
+
parts.push(seg(` ${glyphs.midDot} `, 'muted'), seg(name, 'name', true));
|
|
25
|
+
return parts;
|
|
26
|
+
}
|
|
27
|
+
function factSegments(facts, glyphs) {
|
|
28
|
+
if (facts.length === 0)
|
|
29
|
+
return [];
|
|
30
|
+
const parts = [seg(FACT_GAP, 'muted')];
|
|
31
|
+
facts.forEach((fact, index) => {
|
|
32
|
+
if (index > 0)
|
|
33
|
+
parts.push(seg(` ${glyphs.midDot} `, 'muted'));
|
|
34
|
+
parts.push(seg(fact, 'muted'));
|
|
35
|
+
});
|
|
36
|
+
return parts;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The header row, already fitted to `usable` columns (RV-10). Narrow terminals drop
|
|
40
|
+
* facts from the right WHOLE before anything else is cut; only when the identity alone
|
|
41
|
+
* is too wide is the name shortened. Ink's `wrap="truncate"` stays on the row as a
|
|
42
|
+
* backstop, but it is never what produces the visible ellipsis.
|
|
43
|
+
*/
|
|
44
|
+
export function invocationHeaderSegments(card, glyphs, usable) {
|
|
45
|
+
const width = Math.max(1, Math.floor(usable));
|
|
46
|
+
for (let kept = card.facts.length; kept >= 0; kept -= 1) {
|
|
47
|
+
const candidate = [
|
|
48
|
+
...identitySegments(card, glyphs, card.name),
|
|
49
|
+
...factSegments(card.facts.slice(0, kept), glyphs),
|
|
50
|
+
];
|
|
51
|
+
if (segmentsWidth(candidate) <= width)
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
const head = identitySegments(card, glyphs, '');
|
|
55
|
+
const room = width - segmentsWidth(head) - cellWidth(` ${glyphs.midDot} `);
|
|
56
|
+
if (card.name !== '' && room > 1) {
|
|
57
|
+
return identitySegments(card, glyphs, fitCells(card.name, room, glyphs.ellipsis));
|
|
58
|
+
}
|
|
59
|
+
return [seg(fitCells(card.variant, width, glyphs.ellipsis), 'label', true)];
|
|
60
|
+
}
|
|
61
|
+
/** The text painted for `input`: tabs expanded per line, the same rule `UserEntry` follows. */
|
|
62
|
+
export function invocationInputText(card) {
|
|
63
|
+
return card.input.split('\n').map((line) => expandTabs(line)).join('\n');
|
|
64
|
+
}
|
|
65
|
+
/** Singular/plural for the hidden-characters tail. */
|
|
66
|
+
export function invocationHiddenText(card, glyphs) {
|
|
67
|
+
const unit = card.inputHidden === 1 ? 'char' : 'chars';
|
|
68
|
+
return `${glyphs.ellipsis} +${card.inputHidden} ${unit} not shown`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Rows the card occupies at `usable` columns. `wrappedRows` is injected by the caller
|
|
72
|
+
* (`virtual-window.ts` owns the one wrapping rule) so this file stays import-cycle free.
|
|
73
|
+
*/
|
|
74
|
+
export function invocationCardRows(card, usable, wrappedRows) {
|
|
75
|
+
const inputRows = card.input === '' ? 0 : wrappedRows(invocationInputText(card), usable);
|
|
76
|
+
return 1 + inputRows + (card.inputHidden > 0 ? 1 : 0) + (card.note ? 1 : 0);
|
|
77
|
+
}
|
|
78
|
+
const REPLAY_INDENT = ' ';
|
|
79
|
+
/**
|
|
80
|
+
* The lines the exit replay / export prints UNDER the command line (section 2.4):
|
|
81
|
+
* a bracketed fact line, and the disclosure note when there is one. Plain ASCII
|
|
82
|
+
* separators - the replay is pasted into issues and must survive any terminal.
|
|
83
|
+
*/
|
|
84
|
+
export function invocationCardLines(card) {
|
|
85
|
+
const identity = card.name === '' ? card.variant : `${card.variant} ${card.name}`;
|
|
86
|
+
const detail = card.facts.length === 0 ? identity : `${identity} ${card.facts.join(' - ')}`;
|
|
87
|
+
const lines = [`${REPLAY_INDENT}[${detail}]`];
|
|
88
|
+
if (card.note)
|
|
89
|
+
lines.push(`${REPLAY_INDENT}${card.note}`);
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
@@ -40,6 +40,7 @@ import { GUTTER_WIDTH } from './Gutter.js';
|
|
|
40
40
|
import { expandTabs } from '../display-tab.js';
|
|
41
41
|
import { USER_ENTRY_MAX_ROWS } from '../composer-limits.js';
|
|
42
42
|
import { isLiveTeamCard } from '../team-status.js';
|
|
43
|
+
import { invocationCardRows } from './invocation-card.js';
|
|
43
44
|
export const VIRTUAL_LIMITS = {
|
|
44
45
|
/**
|
|
45
46
|
* Entries kept mounted on each side of the visible band.
|
|
@@ -68,8 +69,12 @@ export const VIRTUAL_LIMITS = {
|
|
|
68
69
|
*/
|
|
69
70
|
export function entryRevision(entry) {
|
|
70
71
|
switch (entry.kind) {
|
|
72
|
+
// A card's shape is fixed at creation; the suffix only tells "card" from "plain" (and
|
|
73
|
+
// the three row-bearing parts), which is what makes a `queued -> user` rewrite re-measure.
|
|
71
74
|
case 'user':
|
|
72
|
-
return
|
|
75
|
+
return entry.invocation
|
|
76
|
+
? `i${entry.invocation.input.length}.${entry.invocation.inputHidden > 0 ? 1 : 0}.${entry.invocation.note ? 1 : 0}`
|
|
77
|
+
: 'u';
|
|
73
78
|
case 'notice':
|
|
74
79
|
return 'n';
|
|
75
80
|
// `thinkingMs` is O(1) and it CHANGES THE ROW: sealing the clock turns
|
|
@@ -259,6 +264,11 @@ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisibl
|
|
|
259
264
|
const separation = density === 'comfortable' ? 1 : 0;
|
|
260
265
|
switch (entry.kind) {
|
|
261
266
|
case 'user':
|
|
267
|
+
// A CARD HAS ITS OWN, BOUNDED HEIGHT (tui-invocation-cards I-4): the same
|
|
268
|
+
// `invocationCardRows` the painter is built from, never the command line's wrapped rows.
|
|
269
|
+
if (entry.invocation) {
|
|
270
|
+
return separation + invocationCardRows(entry.invocation, usable, wrappedRows);
|
|
271
|
+
}
|
|
262
272
|
// MIRRORS `UserEntry`'s RENDER CAP (+1 for the `... +N more lines` tail).
|
|
263
273
|
// THESE TWO NUMBERS ARE ONE NUMBER (I-13). Capping the renderer alone
|
|
264
274
|
// would leave this function believing a 5 000-line message is 5 000 rows
|
|
@@ -3,6 +3,7 @@ import { formatRetryLine } from '../agent/retry-view.js';
|
|
|
3
3
|
import { childStatus, outcomePhrase, rosterCounts, statusMarkerCell } from './team-status.js';
|
|
4
4
|
import { expandTabs } from './display-tab.js';
|
|
5
5
|
import { governanceCardLines } from './layout/governance-card.js';
|
|
6
|
+
import { invocationCardLines } from './layout/invocation-card.js';
|
|
6
7
|
const DEFAULT_MAX_ENTRIES = 200;
|
|
7
8
|
const CONTINUATION_INDENT = ' ';
|
|
8
9
|
const TOOL_INDENT = ' ';
|
|
@@ -87,7 +88,9 @@ function renderEntry(entry, glyphs) {
|
|
|
87
88
|
case 'queued':
|
|
88
89
|
return block('Queued but never sent:', entry.text);
|
|
89
90
|
case 'user':
|
|
90
|
-
return
|
|
91
|
+
return entry.invocation
|
|
92
|
+
? [...block(glyphs.user, entry.text), ...invocationCardLines(entry.invocation)]
|
|
93
|
+
: block(glyphs.user, entry.text);
|
|
91
94
|
case 'assistant': {
|
|
92
95
|
const suffix = entry.aborted ? ' (aborted)' : '';
|
|
93
96
|
return block(glyphs.assistant, `${entry.text}${suffix}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.46",
|
|
4
4
|
"description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AragonAgent contributors",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepublishOnly": "npm run build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aragon-agent/core": "^0.2.
|
|
59
|
+
"@aragon-agent/core": "^0.2.58",
|
|
60
60
|
"chalk": "^5.6.2",
|
|
61
61
|
"cli-highlight": "^2.0.0",
|
|
62
62
|
"commander": "^12.1.0",
|