@compilr-dev/cli 0.8.10 → 0.9.0
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 +35 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +1 -0
- package/dist/commands-v2/handlers/project.js +88 -229
- package/dist/commands-v2/handlers/skill.js +10 -6
- package/dist/commands-v2/handlers/team.d.ts +19 -1
- package/dist/commands-v2/handlers/team.js +178 -3
- package/dist/compilr-diff-companion.vsix +0 -0
- package/dist/index.js +55 -3
- package/dist/multi-agent/agent-edit.d.ts +75 -0
- package/dist/multi-agent/agent-edit.js +84 -0
- package/dist/multi-agent/capability-loader.d.ts +15 -5
- package/dist/multi-agent/capability-loader.js +18 -8
- package/dist/multi-agent/custom-agents.d.ts +12 -0
- package/dist/multi-agent/custom-agents.js +22 -1
- package/dist/repl-helpers.d.ts +6 -1
- package/dist/repl-helpers.js +14 -7
- package/dist/repl-v2.d.ts +10 -0
- package/dist/repl-v2.js +43 -2
- package/dist/ui/overlay/impl/agent-edit-overlay-v2.d.ts +105 -0
- package/dist/ui/overlay/impl/agent-edit-overlay-v2.js +364 -0
- package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +1 -8
- package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +16 -120
- package/dist/ui/overlay/impl/project-edit-overlay-v2.d.ts +2 -1
- package/dist/ui/overlay/impl/project-edit-overlay-v2.js +26 -19
- package/dist/ui/overlay/impl/skills-overlay-v2.d.ts +42 -2
- package/dist/ui/overlay/impl/skills-overlay-v2.js +238 -185
- package/dist/ui/overlay/impl/team-overlay-v2.d.ts +1 -2
- package/dist/ui/overlay/impl/team-overlay-v2.js +6 -9
- package/dist/ui/terminal-types.d.ts +8 -0
- package/package.json +2 -2
package/dist/repl-helpers.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and improve maintainability. These functions support the REPL but don't
|
|
6
6
|
* depend on REPL instance state.
|
|
7
7
|
*/
|
|
8
|
-
import { detectCollisions, formatCollisionWarnings, resolveSkillPrompt, getAllAvailableSkills,
|
|
8
|
+
import { detectCollisions, formatCollisionWarnings, resolveSkillPrompt, getAllAvailableSkills, platformMacros } from '@compilr-dev/sdk';
|
|
9
9
|
import { getSkillsDir } from './utils/skill-paths.js';
|
|
10
10
|
import { getProjectPath } from './utils/project-status.js';
|
|
11
11
|
import { codingSkills } from '@compilr-dev/agents-coding';
|
|
@@ -202,7 +202,7 @@ export function hasBacklogItems(backlogPath) {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
/** Get the current project directory (best effort). */
|
|
205
|
-
function getProjectDir() {
|
|
205
|
+
export function getProjectDir() {
|
|
206
206
|
try {
|
|
207
207
|
return getProjectPath(process.cwd());
|
|
208
208
|
}
|
|
@@ -211,7 +211,7 @@ function getProjectDir() {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
/** Get CLI skill sources (factory + coding). */
|
|
214
|
-
function getCliSkillSources() {
|
|
214
|
+
export function getCliSkillSources() {
|
|
215
215
|
return { factorySkills, codingSkills };
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
@@ -229,16 +229,16 @@ function getCliSkillSources() {
|
|
|
229
229
|
* of date again, so the rule now reads the skill's own tag. The SDK has a matching guard
|
|
230
230
|
* asserting every canvas skill carries it.
|
|
231
231
|
*/
|
|
232
|
-
const
|
|
232
|
+
const DESKTOP_ONLY_MACROS = new Set(platformMacros.filter((skill) => skill.tags?.includes('canvas')).map((skill) => skill.name));
|
|
233
233
|
/**
|
|
234
234
|
* Get skill prompt by name. Delegates to SDK resolveSkillPrompt, minus any
|
|
235
|
-
* Desktop-only SDK skills (see
|
|
235
|
+
* Desktop-only SDK skills (see DESKTOP_ONLY_MACROS).
|
|
236
236
|
*/
|
|
237
237
|
export function getSkillPrompt(name) {
|
|
238
238
|
const resolution = resolveSkillPrompt(name, getProjectDir(), getCliSkillSources());
|
|
239
239
|
if (!resolution)
|
|
240
240
|
return null;
|
|
241
|
-
if (resolution.source === 'sdk' &&
|
|
241
|
+
if (resolution.source === 'sdk' && DESKTOP_ONLY_MACROS.has(resolution.skillName))
|
|
242
242
|
return null;
|
|
243
243
|
return resolution.prompt;
|
|
244
244
|
}
|
|
@@ -250,7 +250,7 @@ export function getSkillResolution(name) {
|
|
|
250
250
|
const resolution = resolveSkillPrompt(name, getProjectDir(), getCliSkillSources());
|
|
251
251
|
if (!resolution)
|
|
252
252
|
return null;
|
|
253
|
-
if (resolution.source === 'sdk' &&
|
|
253
|
+
if (resolution.source === 'sdk' && DESKTOP_ONLY_MACROS.has(resolution.skillName))
|
|
254
254
|
return null;
|
|
255
255
|
return {
|
|
256
256
|
prompt: resolution.prompt,
|
|
@@ -267,6 +267,13 @@ export function getSkillAutocompleteEntries() {
|
|
|
267
267
|
const entries = getAllAvailableSkills(getProjectDir(), getCliSkillSources());
|
|
268
268
|
return entries
|
|
269
269
|
.filter((e) => e.source !== 'sdk' && e.source !== 'agents') // SDK/agents skills already in built-in commands
|
|
270
|
+
/*
|
|
271
|
+
⚠️ A DISABLED SKILL MUST NOT BE OFFERED. Pressing Space in `/skill` writes `enabled: false`
|
|
272
|
+
into the SKILL.md and `resolveSkillPrompt` then refuses it — but autocomplete filtered only
|
|
273
|
+
on `source`, so the skill kept appearing, and accepting the suggestion answered "Unknown
|
|
274
|
+
command". Desktop's `getSkillList` has always filtered on this; the CLI did not.
|
|
275
|
+
*/
|
|
276
|
+
.filter((e) => e.enabled !== false)
|
|
270
277
|
.map((e) => {
|
|
271
278
|
const desc = e.description.length > 60 ? e.description.slice(0, 57) + '...' : e.description;
|
|
272
279
|
return {
|
package/dist/repl-v2.d.ts
CHANGED
|
@@ -304,6 +304,16 @@ export declare class ReplV2 {
|
|
|
304
304
|
* For foreground: sets currentSession and foregroundAgentId
|
|
305
305
|
* For background: only adds to sessions map
|
|
306
306
|
*/
|
|
307
|
+
/**
|
|
308
|
+
* A warning to prepend when a macro needs tools the active agent does not have.
|
|
309
|
+
*
|
|
310
|
+
* ⚠️ WITHOUT IT THE AGENT PROMISES AND THEN FAILS. `/design` tells the agent to use `ask_user`
|
|
311
|
+
* and `workitem_add`; sent to an agent that cannot reach those it announces a requirements
|
|
312
|
+
* session and dies partway, which reads as the agent being broken. Addressed to the agent, not
|
|
313
|
+
* shown as a terminal warning: the user does not know which tools /design needs or which
|
|
314
|
+
* teammate has them, and the agent can explain and offer the handoff.
|
|
315
|
+
*/
|
|
316
|
+
private macroPreamble;
|
|
307
317
|
private startSession;
|
|
308
318
|
/**
|
|
309
319
|
* End an agent session.
|
package/dist/repl-v2.js
CHANGED
|
@@ -56,6 +56,7 @@ import { isMemoryInput } from './input-handlers/index.js';
|
|
|
56
56
|
import { addAnchor } from './anchors/index.js';
|
|
57
57
|
import { getActiveProject } from './tools/project-db.js';
|
|
58
58
|
import { getSkillResolution, getSkillAutocompleteEntries, checkSkillCollisions } from './repl-helpers.js';
|
|
59
|
+
import { macroToolGap, macroToolGapPreamble } from '@compilr-dev/sdk';
|
|
59
60
|
import { getProjectSessionManager } from './session/project-session-manager.js';
|
|
60
61
|
import { ResumeOverlayV2 } from './ui/overlay/impl/resume-overlay-v2.js';
|
|
61
62
|
import { updateCacheInfo, updateHistoryEstimate, formatBreakdownCompact, formatCacheInfo, estimateTokens } from './utils/token-tracker.js';
|
|
@@ -302,8 +303,36 @@ export class ReplV2 {
|
|
|
302
303
|
* For foreground: sets currentSession and foregroundAgentId
|
|
303
304
|
* For background: only adds to sessions map
|
|
304
305
|
*/
|
|
306
|
+
/**
|
|
307
|
+
* A warning to prepend when a macro needs tools the active agent does not have.
|
|
308
|
+
*
|
|
309
|
+
* ⚠️ WITHOUT IT THE AGENT PROMISES AND THEN FAILS. `/design` tells the agent to use `ask_user`
|
|
310
|
+
* and `workitem_add`; sent to an agent that cannot reach those it announces a requirements
|
|
311
|
+
* session and dies partway, which reads as the agent being broken. Addressed to the agent, not
|
|
312
|
+
* shown as a terminal warning: the user does not know which tools /design needs or which
|
|
313
|
+
* teammate has them, and the agent can explain and offer the handoff.
|
|
314
|
+
*/
|
|
315
|
+
macroPreamble(macroName) {
|
|
316
|
+
const active = this.team?.getActive();
|
|
317
|
+
if (!active?.toolFilter)
|
|
318
|
+
return null;
|
|
319
|
+
const gap = macroToolGap(macroName, active.toolFilter);
|
|
320
|
+
return gap ? macroToolGapPreamble(macroName, gap) : null;
|
|
321
|
+
}
|
|
305
322
|
startSession(session) {
|
|
306
323
|
this.sessions.set(session.agentId, session);
|
|
324
|
+
/*
|
|
325
|
+
⚠️ TELLS THE SDK THE AGENT IS MID-REPLY, so `/team` -> `e` cannot save an edit that
|
|
326
|
+
rebuilds it. A rebuild snapshots the history and drops the instance, but the in-flight run
|
|
327
|
+
holds its own reference and finishes anyway — the reply arrives and the rebuilt agent has
|
|
328
|
+
no memory of having sent it. Nothing errors; the agent just answers the next question from
|
|
329
|
+
a history the user can see is wrong.
|
|
330
|
+
|
|
331
|
+
Reachable here through BACKGROUND agents (`$agent msg &`): a foreground turn blocks the
|
|
332
|
+
REPL, so `/team` is out of reach until it finishes, but a background one leaves the prompt
|
|
333
|
+
free. Bracketing start/end covers both, and they are the one chokepoint every run passes.
|
|
334
|
+
*/
|
|
335
|
+
this.team?.get(session.agentId)?.beginTurn();
|
|
307
336
|
if (!session.isBackground) {
|
|
308
337
|
this.currentSession = session;
|
|
309
338
|
this.foregroundAgentId = session.agentId;
|
|
@@ -316,6 +345,8 @@ export class ReplV2 {
|
|
|
316
345
|
*/
|
|
317
346
|
endSession(agentId) {
|
|
318
347
|
this.sessions.delete(agentId);
|
|
348
|
+
// Pairs with beginTurn() in startSession, however the turn ended.
|
|
349
|
+
this.team?.get(agentId)?.endTurn();
|
|
319
350
|
if (this.foregroundAgentId === agentId) {
|
|
320
351
|
this.currentSession = null;
|
|
321
352
|
this.foregroundAgentId = null;
|
|
@@ -2047,7 +2078,13 @@ export class ReplV2 {
|
|
|
2047
2078
|
},
|
|
2048
2079
|
// Queue agent message (for commands that invoke the agent)
|
|
2049
2080
|
queueAgentMessage: (options) => {
|
|
2050
|
-
|
|
2081
|
+
/*
|
|
2082
|
+
A macro assumes tools. Warn the AGENT here — the one place every hardcoded macro
|
|
2083
|
+
handler funnels through — rather than in each handler, which would be eight copies of
|
|
2084
|
+
a rule that must not drift.
|
|
2085
|
+
*/
|
|
2086
|
+
const preamble = options.macro ? this.macroPreamble(options.macro) : null;
|
|
2087
|
+
this.ui.queueAgentMessage(preamble ? { ...options, message: `${preamble}\n\n---\n\n${options.message}` } : options);
|
|
2051
2088
|
},
|
|
2052
2089
|
// Session stats
|
|
2053
2090
|
startTime: this.startTime,
|
|
@@ -2092,7 +2129,11 @@ export class ReplV2 {
|
|
|
2092
2129
|
this.ui.print({ type: 'info', message: `🛠 /${cmd} → ${resolution.skillName}${forkInfo}` });
|
|
2093
2130
|
}
|
|
2094
2131
|
this.ui.print({ type: 'user-message', text: `/${cmd}${args ? ' ' + args : ''}` });
|
|
2095
|
-
|
|
2132
|
+
// Same warning as the hardcoded macros get — this path serves every other macro name,
|
|
2133
|
+
// including a user's own skill invoked as one.
|
|
2134
|
+
const macroPreamble = this.macroPreamble(cmd);
|
|
2135
|
+
const body = args ? `${resolution.prompt}\n\nUser request: ${args}` : resolution.prompt;
|
|
2136
|
+
const fullPrompt = macroPreamble ? `${macroPreamble}\n\n---\n\n${body}` : body;
|
|
2096
2137
|
if (this.agent) {
|
|
2097
2138
|
await this.runAgentReal(fullPrompt);
|
|
2098
2139
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Edit Overlay V2
|
|
3
|
+
*
|
|
4
|
+
* Standalone overlay for editing an existing team agent's attributes.
|
|
5
|
+
* Opened from /team (via 'e' key) — see commands-v2/handlers/team.ts.
|
|
6
|
+
*
|
|
7
|
+
* Adapted from ProjectEditOverlayV2, which has the same shape: a list of fields,
|
|
8
|
+
* one selected, Enter to change it. The additions here are choice fields (model
|
|
9
|
+
* tier, tool profile, mascot) which cycle rather than accept typing.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ THIS OVERLAY DOES NOT SAVE. It returns the edited values and the caller applies
|
|
12
|
+
* them. `AgentTeam.updateAgent` is async and the REPL's live-agent reference may need
|
|
13
|
+
* replacing afterwards (see the handler); overlay key handling is synchronous.
|
|
14
|
+
*/
|
|
15
|
+
import { BaseOverlayV2 } from '../../base/index.js';
|
|
16
|
+
import type { RenderContext, OverlayAction, KeyEvent } from '../types.js';
|
|
17
|
+
import { type ToolProfile, type MascotExpression } from '@compilr-dev/sdk';
|
|
18
|
+
import { type ModelTier } from '../../../models/index.js';
|
|
19
|
+
type FieldKey = 'displayName' | 'specialty' | 'personality' | 'instructions' | 'modelTier' | 'toolProfile' | 'mascot';
|
|
20
|
+
interface EditableField {
|
|
21
|
+
key: FieldKey;
|
|
22
|
+
label: string;
|
|
23
|
+
/** 'text' fields are typed into; 'choice' fields cycle through `choices`. */
|
|
24
|
+
kind: 'text' | 'choice';
|
|
25
|
+
value: string;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
maxLength?: number;
|
|
28
|
+
choices?: string[];
|
|
29
|
+
}
|
|
30
|
+
interface AgentEditState {
|
|
31
|
+
fields: EditableField[];
|
|
32
|
+
selectedFieldIndex: number;
|
|
33
|
+
editingField: boolean;
|
|
34
|
+
inputBuffer: string;
|
|
35
|
+
error: string | null;
|
|
36
|
+
}
|
|
37
|
+
/** Everything the overlay needs to render, and to hand back unchanged if untouched. */
|
|
38
|
+
export interface AgentEditOptions {
|
|
39
|
+
agentId: string;
|
|
40
|
+
displayName: string;
|
|
41
|
+
mascot: MascotExpression;
|
|
42
|
+
/** TeamAgent.description — the one-line "what this agent is for". */
|
|
43
|
+
specialty: string;
|
|
44
|
+
/** Tone only. Composed at initialize() as "Your manner: …" (SDK 0.23+). */
|
|
45
|
+
personality: string;
|
|
46
|
+
/**
|
|
47
|
+
* The user's OWN instructions — never the composed prompt.
|
|
48
|
+
* For a custom agent this comes from `CustomAgentDefinition.systemPromptAddition`; for a
|
|
49
|
+
* role agent, from the live `TeamAgent.systemPromptAddition`, which since SDK 0.23 holds
|
|
50
|
+
* user-authored text only. A role agent that has never been edited stores nothing here,
|
|
51
|
+
* so an empty box is the normal state rather than a loading failure.
|
|
52
|
+
*/
|
|
53
|
+
instructions: string;
|
|
54
|
+
modelTier: ModelTier;
|
|
55
|
+
toolProfile: ToolProfile;
|
|
56
|
+
/** Custom agents regenerate their prompt on save; role agents do not. */
|
|
57
|
+
isCustom: boolean;
|
|
58
|
+
/** Mascots already in use by other agents, marked in the picker. */
|
|
59
|
+
takenMascots?: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface AgentEditValues {
|
|
62
|
+
displayName: string;
|
|
63
|
+
specialty: string;
|
|
64
|
+
personality: string;
|
|
65
|
+
instructions: string;
|
|
66
|
+
modelTier: ModelTier;
|
|
67
|
+
toolProfile: ToolProfile;
|
|
68
|
+
mascot: MascotExpression;
|
|
69
|
+
}
|
|
70
|
+
export interface AgentEditResult {
|
|
71
|
+
saved: boolean;
|
|
72
|
+
agentId: string;
|
|
73
|
+
/** Present only when saved. The caller diffs these against the originals. */
|
|
74
|
+
values?: AgentEditValues;
|
|
75
|
+
}
|
|
76
|
+
export declare class AgentEditOverlayV2 extends BaseOverlayV2<AgentEditState, AgentEditResult> {
|
|
77
|
+
readonly type: "inline";
|
|
78
|
+
readonly id = "agent-edit-overlay-v2";
|
|
79
|
+
private readonly opts;
|
|
80
|
+
constructor(opts: AgentEditOptions);
|
|
81
|
+
/** Human-readable rendering of a choice field's current value. */
|
|
82
|
+
private describeChoice;
|
|
83
|
+
/**
|
|
84
|
+
* One-line preview of a text value.
|
|
85
|
+
*
|
|
86
|
+
* ⚠️ Collapse newlines BEFORE truncating. Instructions are multi-line, and a raw `\n`
|
|
87
|
+
* reaching the renderer makes one logical line occupy two physical rows, which breaks
|
|
88
|
+
* the overlay's line accounting.
|
|
89
|
+
*/
|
|
90
|
+
private previewText;
|
|
91
|
+
/**
|
|
92
|
+
* Truncate plain text to fit the frame.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ Applied BEFORE styling. Cutting a styled string can slice through an ANSI escape
|
|
95
|
+
* and leave the rest of the terminal wearing the colour.
|
|
96
|
+
*/
|
|
97
|
+
private fit;
|
|
98
|
+
protected renderContent(context: RenderContext): string[];
|
|
99
|
+
/** Move a choice field to its next/previous option, wrapping. */
|
|
100
|
+
private cycleChoice;
|
|
101
|
+
handleKey(key: KeyEvent): OverlayAction<AgentEditResult>;
|
|
102
|
+
private valueOf;
|
|
103
|
+
private saveAndClose;
|
|
104
|
+
}
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Edit Overlay V2
|
|
3
|
+
*
|
|
4
|
+
* Standalone overlay for editing an existing team agent's attributes.
|
|
5
|
+
* Opened from /team (via 'e' key) — see commands-v2/handlers/team.ts.
|
|
6
|
+
*
|
|
7
|
+
* Adapted from ProjectEditOverlayV2, which has the same shape: a list of fields,
|
|
8
|
+
* one selected, Enter to change it. The additions here are choice fields (model
|
|
9
|
+
* tier, tool profile, mascot) which cycle rather than accept typing.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ THIS OVERLAY DOES NOT SAVE. It returns the edited values and the caller applies
|
|
12
|
+
* them. `AgentTeam.updateAgent` is async and the REPL's live-agent reference may need
|
|
13
|
+
* replacing afterwards (see the handler); overlay key handling is synchronous.
|
|
14
|
+
*/
|
|
15
|
+
import { BaseOverlayV2, renderBorder } from '../../base/index.js';
|
|
16
|
+
import { PROFILE_INFO, CUSTOM_MASCOTS, } from '@compilr-dev/sdk';
|
|
17
|
+
import { MODEL_TIERS, TIER_INFO } from '../../../models/index.js';
|
|
18
|
+
const PROFILE_LIST = Object.keys(PROFILE_INFO);
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Agent Edit Overlay V2
|
|
21
|
+
// =============================================================================
|
|
22
|
+
export class AgentEditOverlayV2 extends BaseOverlayV2 {
|
|
23
|
+
type = 'inline';
|
|
24
|
+
id = 'agent-edit-overlay-v2';
|
|
25
|
+
opts;
|
|
26
|
+
constructor(opts) {
|
|
27
|
+
// A custom agent's mascot is user-chosen; keep the current one selectable even
|
|
28
|
+
// if another agent also holds it, so opening the editor can never silently
|
|
29
|
+
// move an agent off its own face.
|
|
30
|
+
const mascotChoices = Array.from(new Set([opts.mascot, ...CUSTOM_MASCOTS]));
|
|
31
|
+
super({
|
|
32
|
+
fields: [
|
|
33
|
+
{
|
|
34
|
+
key: 'displayName',
|
|
35
|
+
label: 'Display Name',
|
|
36
|
+
kind: 'text',
|
|
37
|
+
value: opts.displayName,
|
|
38
|
+
placeholder: 'e.g. Solution Architect',
|
|
39
|
+
maxLength: 60,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: 'specialty',
|
|
43
|
+
label: 'Specialty',
|
|
44
|
+
kind: 'text',
|
|
45
|
+
value: opts.specialty,
|
|
46
|
+
placeholder: 'e.g. system design and trade-offs',
|
|
47
|
+
maxLength: 120,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'personality',
|
|
51
|
+
label: 'Personality',
|
|
52
|
+
kind: 'text',
|
|
53
|
+
value: opts.personality,
|
|
54
|
+
placeholder: 'e.g. Pragmatic and thorough (tone only)',
|
|
55
|
+
maxLength: 200,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: 'instructions',
|
|
59
|
+
label: 'Instructions',
|
|
60
|
+
kind: 'text',
|
|
61
|
+
value: opts.instructions,
|
|
62
|
+
// Empty is the normal state for a role agent: its role scaffold is composed by
|
|
63
|
+
// the SDK and was never the user's to edit.
|
|
64
|
+
placeholder: opts.isCustom
|
|
65
|
+
? '(none — add your own instructions)'
|
|
66
|
+
: '(none — role behaviour is built in)',
|
|
67
|
+
maxLength: 2000,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: 'modelTier',
|
|
71
|
+
label: 'Model Tier',
|
|
72
|
+
kind: 'choice',
|
|
73
|
+
value: opts.modelTier,
|
|
74
|
+
choices: [...MODEL_TIERS],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'toolProfile',
|
|
78
|
+
label: 'Tool Profile',
|
|
79
|
+
kind: 'choice',
|
|
80
|
+
value: opts.toolProfile,
|
|
81
|
+
choices: [...PROFILE_LIST],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: 'mascot',
|
|
85
|
+
label: 'Mascot',
|
|
86
|
+
kind: 'choice',
|
|
87
|
+
value: opts.mascot,
|
|
88
|
+
choices: mascotChoices,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
selectedFieldIndex: 0,
|
|
92
|
+
editingField: false,
|
|
93
|
+
inputBuffer: '',
|
|
94
|
+
error: null,
|
|
95
|
+
});
|
|
96
|
+
this.opts = opts;
|
|
97
|
+
this.minHeight = 24;
|
|
98
|
+
}
|
|
99
|
+
// ===========================================================================
|
|
100
|
+
// Rendering
|
|
101
|
+
// ===========================================================================
|
|
102
|
+
/** Human-readable rendering of a choice field's current value. */
|
|
103
|
+
describeChoice(field, context) {
|
|
104
|
+
const s = context.styles;
|
|
105
|
+
if (field.key === 'modelTier') {
|
|
106
|
+
const info = TIER_INFO[field.value];
|
|
107
|
+
return `${info.label} ${s.muted(`— ${info.shortDescription}`)}`;
|
|
108
|
+
}
|
|
109
|
+
if (field.key === 'toolProfile') {
|
|
110
|
+
const info = PROFILE_INFO[field.value];
|
|
111
|
+
const readOnly = info.isReadOnly ? s.muted(' [read-only]') : '';
|
|
112
|
+
return `${info.label}${readOnly}`;
|
|
113
|
+
}
|
|
114
|
+
// mascot
|
|
115
|
+
const taken = (this.opts.takenMascots ?? []).includes(field.value) && field.value !== this.opts.mascot;
|
|
116
|
+
return `${field.value}${taken ? s.muted(' (in use by another agent)') : ''}`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* One-line preview of a text value.
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ Collapse newlines BEFORE truncating. Instructions are multi-line, and a raw `\n`
|
|
122
|
+
* reaching the renderer makes one logical line occupy two physical rows, which breaks
|
|
123
|
+
* the overlay's line accounting.
|
|
124
|
+
*/
|
|
125
|
+
previewText(value, width) {
|
|
126
|
+
const collapsed = value.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
127
|
+
if (collapsed.length <= width)
|
|
128
|
+
return collapsed;
|
|
129
|
+
return collapsed.slice(0, Math.max(0, width - 1)) + '…';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Truncate plain text to fit the frame.
|
|
133
|
+
*
|
|
134
|
+
* ⚠️ Applied BEFORE styling. Cutting a styled string can slice through an ANSI escape
|
|
135
|
+
* and leave the rest of the terminal wearing the colour.
|
|
136
|
+
*/
|
|
137
|
+
fit(text, cols) {
|
|
138
|
+
const max = Math.max(0, cols - 2);
|
|
139
|
+
if (text.length <= max)
|
|
140
|
+
return text;
|
|
141
|
+
return text.slice(0, Math.max(0, max - 1)) + '…';
|
|
142
|
+
}
|
|
143
|
+
renderContent(context) {
|
|
144
|
+
const s = context.styles;
|
|
145
|
+
/*
|
|
146
|
+
The width we were GIVEN, not the process's. `OverlayManager` builds the context from
|
|
147
|
+
`getTerminalWidth()`, so these are the same value in production — but rendering to the
|
|
148
|
+
width passed in is what makes the frame measurable, and leaves the overlay correct if
|
|
149
|
+
it is ever rendered somewhere narrower than the whole terminal.
|
|
150
|
+
*/
|
|
151
|
+
const cols = context.width;
|
|
152
|
+
const border = renderBorder(cols, s);
|
|
153
|
+
const lines = [];
|
|
154
|
+
const labelWidth = 14;
|
|
155
|
+
const valueWidth = Math.max(20, cols - labelWidth - 8);
|
|
156
|
+
lines.push(border);
|
|
157
|
+
lines.push(` ${s.primaryBold('EDIT AGENT')}`);
|
|
158
|
+
lines.push('');
|
|
159
|
+
lines.push(s.muted(` $${this.opts.agentId} · ${this.opts.isCustom ? 'custom agent' : 'role agent'}`));
|
|
160
|
+
// The id is deliberately not editable — it is written into message history as
|
|
161
|
+
// literal text, so renaming it would strand every earlier $mention.
|
|
162
|
+
lines.push(s.muted(' ' +
|
|
163
|
+
this.fit(cols < 72
|
|
164
|
+
? 'Agent ID cannot be changed'
|
|
165
|
+
: 'Agent ID cannot be changed (earlier $mentions would break)', cols)));
|
|
166
|
+
lines.push('');
|
|
167
|
+
lines.push(s.muted(' ' + '─'.repeat(Math.max(0, cols - 6))));
|
|
168
|
+
lines.push('');
|
|
169
|
+
for (let i = 0; i < this.state.fields.length; i++) {
|
|
170
|
+
const field = this.state.fields[i];
|
|
171
|
+
const isSelected = i === this.state.selectedFieldIndex;
|
|
172
|
+
const isEditing = isSelected && this.state.editingField;
|
|
173
|
+
const cursor = isSelected ? s.primary('❯ ') : ' ';
|
|
174
|
+
const label = field.label.padEnd(labelWidth);
|
|
175
|
+
if (isEditing) {
|
|
176
|
+
const shown = this.previewText(this.state.inputBuffer, valueWidth);
|
|
177
|
+
lines.push(`${cursor}${s.muted(label)} ${s.primary(shown)}█`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
let rendered;
|
|
181
|
+
if (field.kind === 'choice') {
|
|
182
|
+
rendered = this.describeChoice(field, context);
|
|
183
|
+
}
|
|
184
|
+
else if (field.value) {
|
|
185
|
+
rendered = this.previewText(field.value, valueWidth);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
rendered = s.muted(field.placeholder ?? '(not set)');
|
|
189
|
+
}
|
|
190
|
+
lines.push(`${cursor}${isSelected ? s.primary(label) : s.muted(label)} ${rendered}`);
|
|
191
|
+
}
|
|
192
|
+
lines.push('');
|
|
193
|
+
if (this.state.error) {
|
|
194
|
+
lines.push(s.error(' ' + this.fit(this.state.error, cols)));
|
|
195
|
+
}
|
|
196
|
+
else if (this.opts.isCustom) {
|
|
197
|
+
lines.push(s.muted(' ' +
|
|
198
|
+
this.fit(cols < 72
|
|
199
|
+
? 'Saving regenerates the system prompt.'
|
|
200
|
+
: "Saving regenerates this agent's system prompt from its definition.", cols)));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
lines.push('');
|
|
204
|
+
}
|
|
205
|
+
lines.push(border);
|
|
206
|
+
if (this.state.editingField) {
|
|
207
|
+
lines.push(s.muted(' ' + this.fit('Enter Accept field · Esc Cancel edit', cols)));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const selected = this.state.fields[this.state.selectedFieldIndex];
|
|
211
|
+
const verb = selected.kind === 'choice' ? 'Enter/←→ Change' : 'Enter Edit';
|
|
212
|
+
const hint = cols < 72
|
|
213
|
+
? `↑↓ Move · ${verb} · s Save · Esc Cancel`
|
|
214
|
+
: `↑↓/jk Navigate · ${verb} field · s Save & close · Esc Cancel`;
|
|
215
|
+
lines.push(s.muted(' ' + this.fit(hint, cols)));
|
|
216
|
+
}
|
|
217
|
+
lines.push(border);
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
|
220
|
+
// ===========================================================================
|
|
221
|
+
// Key Handling
|
|
222
|
+
// ===========================================================================
|
|
223
|
+
/** Move a choice field to its next/previous option, wrapping. */
|
|
224
|
+
cycleChoice(field, delta) {
|
|
225
|
+
const choices = field.choices ?? [];
|
|
226
|
+
if (choices.length === 0)
|
|
227
|
+
return;
|
|
228
|
+
const current = choices.indexOf(field.value);
|
|
229
|
+
// An unknown current value starts the cycle at the first option rather than
|
|
230
|
+
// throwing the index negative.
|
|
231
|
+
const base = current === -1 ? 0 : current;
|
|
232
|
+
const next = (base + delta + choices.length) % choices.length;
|
|
233
|
+
field.value = choices[next];
|
|
234
|
+
}
|
|
235
|
+
handleKey(key) {
|
|
236
|
+
// Ctrl+C always closes
|
|
237
|
+
if (this.isInterruptKey(key)) {
|
|
238
|
+
return this.close({ saved: false, agentId: this.opts.agentId });
|
|
239
|
+
}
|
|
240
|
+
// Handle field editing mode
|
|
241
|
+
if (this.state.editingField) {
|
|
242
|
+
if (key.name === 'escape') {
|
|
243
|
+
// Cancel field edit — the field keeps the value it had
|
|
244
|
+
this.state.editingField = false;
|
|
245
|
+
this.state.inputBuffer = '';
|
|
246
|
+
return this.rerender();
|
|
247
|
+
}
|
|
248
|
+
if (this.isEnterKey(key)) {
|
|
249
|
+
const field = this.state.fields[this.state.selectedFieldIndex];
|
|
250
|
+
field.value = this.state.inputBuffer;
|
|
251
|
+
this.state.editingField = false;
|
|
252
|
+
this.state.inputBuffer = '';
|
|
253
|
+
this.state.error = null;
|
|
254
|
+
return this.rerender();
|
|
255
|
+
}
|
|
256
|
+
if (key.name === 'backspace') {
|
|
257
|
+
this.state.inputBuffer = this.state.inputBuffer.slice(0, -1);
|
|
258
|
+
if (this.state.inputBuffer.length < (this.state.fields[this.state.selectedFieldIndex].maxLength ?? 200)) {
|
|
259
|
+
this.state.error = null;
|
|
260
|
+
}
|
|
261
|
+
return this.rerender();
|
|
262
|
+
}
|
|
263
|
+
// Regular character input
|
|
264
|
+
if (key.char && key.char.length === 1 && key.char >= ' ' && key.char <= '~') {
|
|
265
|
+
const field = this.state.fields[this.state.selectedFieldIndex];
|
|
266
|
+
const max = field.maxLength ?? 200;
|
|
267
|
+
/*
|
|
268
|
+
⚠️ THE LIMIT BLOCKS GROWTH, NOT EDITING. It used to be `length < max`, which meant a
|
|
269
|
+
value ALREADY over the limit swallowed every keystroke in silence — the field looked
|
|
270
|
+
frozen and gave no reason. Real case: a pre-0.23 role agent carried 6,208 characters of
|
|
271
|
+
generated prompt in a 2,000-character field, so Instructions could not be edited at all.
|
|
272
|
+
The SDK no longer stores prose there, but an over-length value can still arrive from
|
|
273
|
+
disk, and a control that ignores input without saying why is the worse half of that bug.
|
|
274
|
+
*/
|
|
275
|
+
if (this.state.inputBuffer.length < max) {
|
|
276
|
+
this.state.inputBuffer += key.char;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
this.state.error = `${field.label} is at its ${String(max)}-character limit — delete some text first.`;
|
|
280
|
+
}
|
|
281
|
+
return this.rerender();
|
|
282
|
+
}
|
|
283
|
+
return this.noAction();
|
|
284
|
+
}
|
|
285
|
+
// Handle navigation mode
|
|
286
|
+
if (key.name === 'escape') {
|
|
287
|
+
return this.close({ saved: false, agentId: this.opts.agentId });
|
|
288
|
+
}
|
|
289
|
+
// Navigate up
|
|
290
|
+
if (this.isUpKey(key) || key.char === 'k') {
|
|
291
|
+
if (this.state.selectedFieldIndex > 0) {
|
|
292
|
+
this.state.selectedFieldIndex--;
|
|
293
|
+
return this.rerender();
|
|
294
|
+
}
|
|
295
|
+
return this.noAction();
|
|
296
|
+
}
|
|
297
|
+
// Navigate down
|
|
298
|
+
if (this.isDownKey(key) || key.char === 'j') {
|
|
299
|
+
if (this.state.selectedFieldIndex < this.state.fields.length - 1) {
|
|
300
|
+
this.state.selectedFieldIndex++;
|
|
301
|
+
return this.rerender();
|
|
302
|
+
}
|
|
303
|
+
return this.noAction();
|
|
304
|
+
}
|
|
305
|
+
const field = this.state.fields[this.state.selectedFieldIndex];
|
|
306
|
+
// Left/right cycle a choice field
|
|
307
|
+
if (field.kind === 'choice' && (this.isLeftKey(key) || key.char === 'h')) {
|
|
308
|
+
this.cycleChoice(field, -1);
|
|
309
|
+
return this.rerender();
|
|
310
|
+
}
|
|
311
|
+
if (field.kind === 'choice' && (this.isRightKey(key) || key.char === 'l')) {
|
|
312
|
+
this.cycleChoice(field, 1);
|
|
313
|
+
return this.rerender();
|
|
314
|
+
}
|
|
315
|
+
// Enter edits a text field, or advances a choice field
|
|
316
|
+
if (this.isEnterKey(key)) {
|
|
317
|
+
if (field.kind === 'choice') {
|
|
318
|
+
this.cycleChoice(field, 1);
|
|
319
|
+
return this.rerender();
|
|
320
|
+
}
|
|
321
|
+
this.state.inputBuffer = field.value;
|
|
322
|
+
this.state.editingField = true;
|
|
323
|
+
return this.rerender();
|
|
324
|
+
}
|
|
325
|
+
// 's' to save and close
|
|
326
|
+
if (key.char === 's' || key.char === 'S') {
|
|
327
|
+
return this.saveAndClose();
|
|
328
|
+
}
|
|
329
|
+
return this.noAction();
|
|
330
|
+
}
|
|
331
|
+
valueOf(key) {
|
|
332
|
+
const field = this.state.fields.find((f) => f.key === key);
|
|
333
|
+
return field ? field.value : '';
|
|
334
|
+
}
|
|
335
|
+
saveAndClose() {
|
|
336
|
+
const displayName = this.valueOf('displayName').trim();
|
|
337
|
+
// The roster renders this everywhere an agent is named; an empty one leaves a
|
|
338
|
+
// blank where the agent should be, so refuse rather than write it.
|
|
339
|
+
if (!displayName) {
|
|
340
|
+
this.state.error = 'Display Name cannot be empty.';
|
|
341
|
+
this.state.selectedFieldIndex = 0;
|
|
342
|
+
return this.rerender();
|
|
343
|
+
}
|
|
344
|
+
return this.close({
|
|
345
|
+
saved: true,
|
|
346
|
+
agentId: this.opts.agentId,
|
|
347
|
+
values: {
|
|
348
|
+
displayName,
|
|
349
|
+
specialty: this.valueOf('specialty').trim(),
|
|
350
|
+
personality: this.valueOf('personality').trim(),
|
|
351
|
+
instructions: this.valueOf('instructions'),
|
|
352
|
+
modelTier: this.valueOf('modelTier'),
|
|
353
|
+
toolProfile: this.valueOf('toolProfile'),
|
|
354
|
+
/*
|
|
355
|
+
Every option came from `CUSTOM_MASCOTS` or from the agent's own current mascot,
|
|
356
|
+
so the value is always a real expression. `CUSTOM_MASCOTS` is declared `string[]`
|
|
357
|
+
in the SDK, which is why the narrowing has to be stated here — the same cast
|
|
358
|
+
`TeamAgent.fromCustomDefinition` makes on the way in.
|
|
359
|
+
*/
|
|
360
|
+
mascot: this.valueOf('mascot'),
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|