@oh-my-pi/pi-coding-agent 12.11.2 → 12.12.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 +34 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +8 -0
- package/src/modes/components/agent-dashboard.ts +1130 -0
- package/src/modes/components/assistant-message.ts +15 -7
- package/src/modes/components/model-selector.ts +5 -2
- package/src/modes/controllers/selector-controller.ts +35 -10
- package/src/modes/interactive-mode.ts +30 -0
- package/src/modes/theme/mermaid-cache.ts +33 -11
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/agent-creation-architect.md +65 -0
- package/src/prompts/system/agent-creation-user.md +6 -0
- package/src/session/agent-session.ts +22 -6
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/index.ts +48 -11
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentDashboard - dedicated control center for Task subagent configuration.
|
|
3
|
+
*
|
|
4
|
+
* Layout:
|
|
5
|
+
* - Top: source tabs (All, Project, User, Bundled)
|
|
6
|
+
* - Body: two-column view (agent list + inspector)
|
|
7
|
+
*
|
|
8
|
+
* Controls:
|
|
9
|
+
* - Up/Down or j/k: move selection
|
|
10
|
+
* - Tab / Shift+Tab: switch source tab
|
|
11
|
+
* - Space: enable/disable selected agent
|
|
12
|
+
* - Enter: edit model override for selected agent
|
|
13
|
+
* - N: start agent creation flow
|
|
14
|
+
* - Esc: clear search (if any) or close dashboard
|
|
15
|
+
* - Ctrl+R: reload discovered agents
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "node:fs/promises";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
20
|
+
import {
|
|
21
|
+
type Component,
|
|
22
|
+
Container,
|
|
23
|
+
Input,
|
|
24
|
+
matchesKey,
|
|
25
|
+
padding,
|
|
26
|
+
replaceTabs,
|
|
27
|
+
Spacer,
|
|
28
|
+
Text,
|
|
29
|
+
truncateToWidth,
|
|
30
|
+
visibleWidth,
|
|
31
|
+
wrapTextWithAnsi,
|
|
32
|
+
} from "@oh-my-pi/pi-tui";
|
|
33
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
34
|
+
import { YAML } from "bun";
|
|
35
|
+
import { getConfigDirs } from "../../config";
|
|
36
|
+
import type { ModelRegistry } from "../../config/model-registry";
|
|
37
|
+
import { formatModelString, isDefaultModelAlias, resolveModelOverride } from "../../config/model-resolver";
|
|
38
|
+
import { renderPromptTemplate } from "../../config/prompt-templates";
|
|
39
|
+
import { Settings } from "../../config/settings";
|
|
40
|
+
import agentCreationArchitectPrompt from "../../prompts/system/agent-creation-architect.md" with { type: "text" };
|
|
41
|
+
import agentCreationUserPrompt from "../../prompts/system/agent-creation-user.md" with { type: "text" };
|
|
42
|
+
import { createAgentSession } from "../../sdk";
|
|
43
|
+
import { discoverAgents } from "../../task/discovery";
|
|
44
|
+
import type { AgentDefinition, AgentSource } from "../../task/types";
|
|
45
|
+
import { shortenPath } from "../../tools/render-utils";
|
|
46
|
+
import { theme } from "../theme/theme";
|
|
47
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
48
|
+
|
|
49
|
+
type SourceTabId = "all" | AgentSource;
|
|
50
|
+
type AgentScope = "project" | "user";
|
|
51
|
+
|
|
52
|
+
interface SourceTab {
|
|
53
|
+
id: SourceTabId;
|
|
54
|
+
label: string;
|
|
55
|
+
count: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface DashboardAgent extends AgentDefinition {
|
|
59
|
+
disabled: boolean;
|
|
60
|
+
overrideModel?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ModelResolution {
|
|
64
|
+
resolved: string;
|
|
65
|
+
thinkingLevel?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface GeneratedAgentSpec {
|
|
69
|
+
identifier: string;
|
|
70
|
+
whenToUse: string;
|
|
71
|
+
systemPrompt: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface AgentDashboardModelContext {
|
|
75
|
+
modelRegistry?: ModelRegistry;
|
|
76
|
+
activeModelPattern?: string;
|
|
77
|
+
defaultModelPattern?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const SOURCE_ORDER: Record<AgentSource, number> = {
|
|
81
|
+
project: 0,
|
|
82
|
+
user: 1,
|
|
83
|
+
bundled: 2,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const SOURCE_LABEL: Record<AgentSource, string> = {
|
|
87
|
+
project: "Project",
|
|
88
|
+
user: "User",
|
|
89
|
+
bundled: "Bundled",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const IDENTIFIER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+){1,5}$/;
|
|
93
|
+
|
|
94
|
+
function normalizeModelPatterns(value: string | string[] | undefined): string[] {
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return value.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0);
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
const normalized = value.trim();
|
|
100
|
+
if (normalized.length > 0) {
|
|
101
|
+
return [normalized];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function joinPatterns(patterns: string[]): string {
|
|
108
|
+
if (patterns.length === 0) return "(session default)";
|
|
109
|
+
return patterns.join(", ");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function matchAgent(agent: DashboardAgent, query: string): boolean {
|
|
113
|
+
const q = query.toLowerCase();
|
|
114
|
+
if (agent.name.toLowerCase().includes(q)) return true;
|
|
115
|
+
if (agent.description.toLowerCase().includes(q)) return true;
|
|
116
|
+
if (SOURCE_LABEL[agent.source].toLowerCase().includes(q)) return true;
|
|
117
|
+
if (agent.overrideModel?.toLowerCase().includes(q)) return true;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractAssistantText(messages: AgentMessage[]): string | null {
|
|
122
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
123
|
+
const message = messages[i];
|
|
124
|
+
if (!message || message.role !== "assistant") continue;
|
|
125
|
+
const blocks = message.content;
|
|
126
|
+
if (!Array.isArray(blocks)) continue;
|
|
127
|
+
const text = blocks
|
|
128
|
+
.map(block => {
|
|
129
|
+
if (!block || typeof block !== "object") return "";
|
|
130
|
+
if (!("type" in block) || (block as { type?: unknown }).type !== "text") return "";
|
|
131
|
+
const value = (block as { text?: unknown }).text;
|
|
132
|
+
return typeof value === "string" ? value : "";
|
|
133
|
+
})
|
|
134
|
+
.join("\n")
|
|
135
|
+
.trim();
|
|
136
|
+
if (text.length > 0) return text;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractJsonObject(raw: string): string {
|
|
142
|
+
const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
143
|
+
if (fenceMatch?.[1]) {
|
|
144
|
+
return fenceMatch[1].trim();
|
|
145
|
+
}
|
|
146
|
+
const start = raw.indexOf("{");
|
|
147
|
+
const end = raw.lastIndexOf("}");
|
|
148
|
+
if (start >= 0 && end >= start) {
|
|
149
|
+
return raw.slice(start, end + 1).trim();
|
|
150
|
+
}
|
|
151
|
+
return raw.trim();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseGeneratedAgentSpec(raw: string): GeneratedAgentSpec {
|
|
155
|
+
const parsed = JSON.parse(extractJsonObject(raw)) as Partial<GeneratedAgentSpec>;
|
|
156
|
+
if (!parsed || typeof parsed !== "object") {
|
|
157
|
+
throw new Error("Model output is not a JSON object");
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
typeof parsed.identifier !== "string" ||
|
|
161
|
+
typeof parsed.whenToUse !== "string" ||
|
|
162
|
+
typeof parsed.systemPrompt !== "string"
|
|
163
|
+
) {
|
|
164
|
+
throw new Error("Model output is missing required fields (identifier, whenToUse, systemPrompt)");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const identifier = parsed.identifier.trim();
|
|
168
|
+
const whenToUse = parsed.whenToUse.trim();
|
|
169
|
+
const systemPrompt = parsed.systemPrompt.trim();
|
|
170
|
+
|
|
171
|
+
if (!IDENTIFIER_PATTERN.test(identifier)) {
|
|
172
|
+
throw new Error("Generated identifier is invalid (must be lowercase kebab-case, 2+ words)");
|
|
173
|
+
}
|
|
174
|
+
if (!whenToUse.toLowerCase().startsWith("use this agent when")) {
|
|
175
|
+
throw new Error("Generated whenToUse must start with 'Use this agent when...'");
|
|
176
|
+
}
|
|
177
|
+
if (!systemPrompt) {
|
|
178
|
+
throw new Error("Generated systemPrompt is empty");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { identifier, whenToUse, systemPrompt };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
class AgentListPane implements Component {
|
|
185
|
+
constructor(
|
|
186
|
+
private readonly agents: DashboardAgent[],
|
|
187
|
+
private readonly selectedIndex: number,
|
|
188
|
+
private readonly scrollOffset: number,
|
|
189
|
+
private readonly searchQuery: string,
|
|
190
|
+
private readonly maxVisible: number,
|
|
191
|
+
) {}
|
|
192
|
+
|
|
193
|
+
render(width: number): string[] {
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
const searchPrefix = theme.fg("muted", "Search: ");
|
|
196
|
+
const searchText = this.searchQuery || theme.fg("dim", "type to filter");
|
|
197
|
+
lines.push(`${searchPrefix}${searchText}`);
|
|
198
|
+
lines.push("");
|
|
199
|
+
|
|
200
|
+
if (this.agents.length === 0) {
|
|
201
|
+
lines.push(theme.fg("muted", " No agents found."));
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const start = this.scrollOffset;
|
|
206
|
+
const end = Math.min(start + this.maxVisible, this.agents.length);
|
|
207
|
+
|
|
208
|
+
for (let i = start; i < end; i++) {
|
|
209
|
+
const agent = this.agents[i];
|
|
210
|
+
const selected = i === this.selectedIndex;
|
|
211
|
+
const status = agent.disabled
|
|
212
|
+
? theme.fg("dim", theme.status.disabled)
|
|
213
|
+
: theme.fg("success", theme.status.enabled);
|
|
214
|
+
const source = theme.fg("dim", `[${SOURCE_LABEL[agent.source]}]`);
|
|
215
|
+
const override = agent.overrideModel ? ` ${theme.fg("warning", "(override)")}` : "";
|
|
216
|
+
let line = ` ${status} ${replaceTabs(agent.name)} ${source}${override}`;
|
|
217
|
+
|
|
218
|
+
if (selected) {
|
|
219
|
+
line = theme.bg("selectedBg", theme.bold(theme.fg("accent", line)));
|
|
220
|
+
} else if (agent.disabled) {
|
|
221
|
+
line = theme.fg("dim", line);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
lines.push(truncateToWidth(line, width));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.agents.length > this.maxVisible) {
|
|
228
|
+
lines.push(theme.fg("muted", ` (${this.selectedIndex + 1}/${this.agents.length})`));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return lines;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
invalidate(): void {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
class AgentInspectorPane implements Component {
|
|
238
|
+
constructor(
|
|
239
|
+
private readonly agent: DashboardAgent | null,
|
|
240
|
+
private readonly defaultPatterns: string[],
|
|
241
|
+
private readonly defaultResolution: ModelResolution | undefined,
|
|
242
|
+
private readonly effectivePatterns: string[],
|
|
243
|
+
private readonly effectiveResolution: ModelResolution | undefined,
|
|
244
|
+
) {}
|
|
245
|
+
|
|
246
|
+
render(width: number): string[] {
|
|
247
|
+
if (!this.agent) {
|
|
248
|
+
return [theme.fg("muted", "Select an agent"), theme.fg("dim", "to inspect settings")];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const lines: string[] = [];
|
|
252
|
+
const state = this.agent.disabled
|
|
253
|
+
? theme.fg("dim", `${theme.status.disabled} Disabled`)
|
|
254
|
+
: theme.fg("success", `${theme.status.enabled} Enabled`);
|
|
255
|
+
|
|
256
|
+
lines.push(theme.bold(theme.fg("accent", replaceTabs(this.agent.name))));
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push(`${theme.fg("muted", "Status:")} ${state}`);
|
|
259
|
+
lines.push(`${theme.fg("muted", "Source:")} ${SOURCE_LABEL[this.agent.source]}`);
|
|
260
|
+
lines.push("");
|
|
261
|
+
|
|
262
|
+
lines.push(`${theme.fg("muted", "Default pattern:")} ${replaceTabs(joinPatterns(this.defaultPatterns))}`);
|
|
263
|
+
lines.push(
|
|
264
|
+
`${theme.fg("muted", "Default resolves:")} ${this.defaultResolution ? this.#formatResolution(this.defaultResolution) : theme.fg("dim", "(unresolved)")}`,
|
|
265
|
+
);
|
|
266
|
+
lines.push(
|
|
267
|
+
`${theme.fg("muted", "Override:")} ${this.agent.overrideModel ? theme.fg("warning", replaceTabs(this.agent.overrideModel)) : theme.fg("dim", "(none)")}`,
|
|
268
|
+
);
|
|
269
|
+
lines.push(`${theme.fg("muted", "Effective pattern:")} ${replaceTabs(joinPatterns(this.effectivePatterns))}`);
|
|
270
|
+
lines.push(
|
|
271
|
+
`${theme.fg("muted", "Effective:")} ${this.effectiveResolution ? this.#formatResolution(this.effectiveResolution) : theme.fg("dim", "(unresolved)")}`,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (this.agent.filePath) {
|
|
275
|
+
lines.push("");
|
|
276
|
+
lines.push(theme.fg("muted", "Path:"));
|
|
277
|
+
lines.push(theme.fg("dim", ` ${replaceTabs(shortenPath(this.agent.filePath))}`));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (this.agent.description) {
|
|
281
|
+
lines.push("");
|
|
282
|
+
lines.push(theme.fg("muted", "Description:"));
|
|
283
|
+
for (const wrapped of wrapTextWithAnsi(replaceTabs(this.agent.description), Math.max(10, width - 2))) {
|
|
284
|
+
lines.push(truncateToWidth(wrapped, width));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return lines;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#formatResolution(resolution: ModelResolution): string {
|
|
292
|
+
if (resolution.thinkingLevel && resolution.thinkingLevel !== "off") {
|
|
293
|
+
return `${theme.fg("success", resolution.resolved)} ${theme.fg("dim", `(${resolution.thinkingLevel})`)}`;
|
|
294
|
+
}
|
|
295
|
+
return theme.fg("success", resolution.resolved);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
invalidate(): void {}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
class TwoColumnBody implements Component {
|
|
302
|
+
constructor(
|
|
303
|
+
private readonly leftPane: AgentListPane,
|
|
304
|
+
private readonly rightPane: AgentInspectorPane,
|
|
305
|
+
private readonly maxHeight: number,
|
|
306
|
+
) {}
|
|
307
|
+
|
|
308
|
+
render(width: number): string[] {
|
|
309
|
+
const leftWidth = Math.floor(width * 0.5);
|
|
310
|
+
const rightWidth = width - leftWidth - 3;
|
|
311
|
+
const leftLines = this.leftPane.render(leftWidth);
|
|
312
|
+
const rightLines = this.rightPane.render(rightWidth);
|
|
313
|
+
const lineCount = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
|
314
|
+
const out: string[] = [];
|
|
315
|
+
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i < lineCount; i++) {
|
|
318
|
+
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
319
|
+
const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
|
|
320
|
+
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
|
321
|
+
out.push(leftPadded + separator + right);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
invalidate(): void {
|
|
328
|
+
this.leftPane.invalidate?.();
|
|
329
|
+
this.rightPane.invalidate?.();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export class AgentDashboard extends Container {
|
|
334
|
+
#settingsManager: Settings | null = null;
|
|
335
|
+
#allAgents: DashboardAgent[] = [];
|
|
336
|
+
#filteredAgents: DashboardAgent[] = [];
|
|
337
|
+
#tabs: SourceTab[] = [{ id: "all", label: "All", count: 0 }];
|
|
338
|
+
#activeTabIndex = 0;
|
|
339
|
+
#selectedIndex = 0;
|
|
340
|
+
#scrollOffset = 0;
|
|
341
|
+
#searchQuery = "";
|
|
342
|
+
#loading = true;
|
|
343
|
+
#loadError: string | null = null;
|
|
344
|
+
#notice: string | null = null;
|
|
345
|
+
|
|
346
|
+
#editInput: Input | null = null;
|
|
347
|
+
#editingAgentName: string | null = null;
|
|
348
|
+
|
|
349
|
+
#createInput: Input | null = null;
|
|
350
|
+
#createDescription = "";
|
|
351
|
+
#createScope: AgentScope = "project";
|
|
352
|
+
#createGenerating = false;
|
|
353
|
+
#createSpec: GeneratedAgentSpec | null = null;
|
|
354
|
+
#createError: string | null = null;
|
|
355
|
+
#createStreamingText = "";
|
|
356
|
+
|
|
357
|
+
onClose?: () => void;
|
|
358
|
+
onRequestRender?: () => void;
|
|
359
|
+
|
|
360
|
+
private constructor(
|
|
361
|
+
private readonly cwd: string,
|
|
362
|
+
private readonly settings: Settings | null,
|
|
363
|
+
private readonly terminalHeight: number,
|
|
364
|
+
private readonly modelContext: AgentDashboardModelContext,
|
|
365
|
+
) {
|
|
366
|
+
super();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
static async create(
|
|
370
|
+
cwd: string,
|
|
371
|
+
settings: Settings | null = null,
|
|
372
|
+
terminalHeight?: number,
|
|
373
|
+
modelContext: AgentDashboardModelContext = {},
|
|
374
|
+
): Promise<AgentDashboard> {
|
|
375
|
+
const dashboard = new AgentDashboard(cwd, settings, terminalHeight ?? process.stdout.rows ?? 24, modelContext);
|
|
376
|
+
await dashboard.#init();
|
|
377
|
+
return dashboard;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async #init(): Promise<void> {
|
|
381
|
+
this.#settingsManager = this.settings ?? (await Settings.init());
|
|
382
|
+
await this.#reloadData();
|
|
383
|
+
this.#buildLayout();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async #reloadData(): Promise<void> {
|
|
387
|
+
this.#loading = true;
|
|
388
|
+
this.#loadError = null;
|
|
389
|
+
this.#buildLayout();
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const selectedName = this.#selectedAgent()?.name;
|
|
393
|
+
const activeTabId = this.#tabs[this.#activeTabIndex]?.id ?? "all";
|
|
394
|
+
const { agents } = await discoverAgents(this.cwd);
|
|
395
|
+
const disabled = new Set((this.#settingsManager?.get("task.disabledAgents") as string[] | undefined) ?? []);
|
|
396
|
+
const overrides =
|
|
397
|
+
(this.#settingsManager?.get("task.agentModelOverrides") as Record<string, string> | undefined) ?? {};
|
|
398
|
+
|
|
399
|
+
this.#allAgents = agents
|
|
400
|
+
.slice()
|
|
401
|
+
.sort((a, b) => {
|
|
402
|
+
const sourceCmp = SOURCE_ORDER[a.source] - SOURCE_ORDER[b.source];
|
|
403
|
+
if (sourceCmp !== 0) return sourceCmp;
|
|
404
|
+
return a.name.localeCompare(b.name);
|
|
405
|
+
})
|
|
406
|
+
.map(agent => ({
|
|
407
|
+
...agent,
|
|
408
|
+
disabled: disabled.has(agent.name),
|
|
409
|
+
overrideModel: overrides[agent.name]?.trim() || undefined,
|
|
410
|
+
}));
|
|
411
|
+
|
|
412
|
+
this.#tabs = this.#buildTabs(this.#allAgents);
|
|
413
|
+
const nextTabIndex = this.#tabs.findIndex(tab => tab.id === activeTabId);
|
|
414
|
+
this.#activeTabIndex = nextTabIndex >= 0 ? nextTabIndex : 0;
|
|
415
|
+
this.#applyFilters();
|
|
416
|
+
|
|
417
|
+
if (selectedName) {
|
|
418
|
+
const idx = this.#filteredAgents.findIndex(agent => agent.name === selectedName);
|
|
419
|
+
if (idx >= 0) {
|
|
420
|
+
this.#selectedIndex = idx;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
this.#clampSelection();
|
|
424
|
+
} catch (error) {
|
|
425
|
+
this.#allAgents = [];
|
|
426
|
+
this.#filteredAgents = [];
|
|
427
|
+
this.#tabs = [{ id: "all", label: "All", count: 0 }];
|
|
428
|
+
this.#activeTabIndex = 0;
|
|
429
|
+
this.#selectedIndex = 0;
|
|
430
|
+
this.#scrollOffset = 0;
|
|
431
|
+
this.#loadError = error instanceof Error ? error.message : String(error);
|
|
432
|
+
} finally {
|
|
433
|
+
this.#loading = false;
|
|
434
|
+
this.#rebuildAndRender();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#buildTabs(agents: DashboardAgent[]): SourceTab[] {
|
|
439
|
+
const tabs: SourceTab[] = [{ id: "all", label: "All", count: agents.length }];
|
|
440
|
+
const counts: Record<AgentSource, number> = { project: 0, user: 0, bundled: 0 };
|
|
441
|
+
|
|
442
|
+
for (const agent of agents) {
|
|
443
|
+
counts[agent.source] += 1;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const source of ["project", "user", "bundled"] as const) {
|
|
447
|
+
if (counts[source] > 0) {
|
|
448
|
+
tabs.push({ id: source, label: SOURCE_LABEL[source], count: counts[source] });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return tabs;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#selectedAgent(): DashboardAgent | null {
|
|
456
|
+
return this.#filteredAgents[this.#selectedIndex] ?? null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#applyFilters(): void {
|
|
460
|
+
const activeTab = this.#tabs[this.#activeTabIndex] ?? this.#tabs[0];
|
|
461
|
+
const tabFiltered =
|
|
462
|
+
activeTab.id === "all" ? this.#allAgents : this.#allAgents.filter(agent => agent.source === activeTab.id);
|
|
463
|
+
|
|
464
|
+
if (!this.#searchQuery) {
|
|
465
|
+
this.#filteredAgents = tabFiltered;
|
|
466
|
+
} else {
|
|
467
|
+
this.#filteredAgents = tabFiltered.filter(agent => matchAgent(agent, this.#searchQuery));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
this.#clampSelection();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
#getMaxVisibleItems(): number {
|
|
474
|
+
return Math.max(5, this.terminalHeight - 14);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
#clampSelection(): void {
|
|
478
|
+
if (this.#filteredAgents.length === 0) {
|
|
479
|
+
this.#selectedIndex = 0;
|
|
480
|
+
this.#scrollOffset = 0;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.#selectedIndex = Math.min(this.#selectedIndex, this.#filteredAgents.length - 1);
|
|
485
|
+
this.#selectedIndex = Math.max(0, this.#selectedIndex);
|
|
486
|
+
|
|
487
|
+
const maxVisible = this.#getMaxVisibleItems();
|
|
488
|
+
if (this.#selectedIndex < this.#scrollOffset) {
|
|
489
|
+
this.#scrollOffset = this.#selectedIndex;
|
|
490
|
+
} else if (this.#selectedIndex >= this.#scrollOffset + maxVisible) {
|
|
491
|
+
this.#scrollOffset = this.#selectedIndex - maxVisible + 1;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#persistDisabledAgents(): void {
|
|
496
|
+
if (!this.#settingsManager) return;
|
|
497
|
+
const disabled = this.#allAgents
|
|
498
|
+
.filter(agent => agent.disabled)
|
|
499
|
+
.map(agent => agent.name)
|
|
500
|
+
.sort((a, b) => a.localeCompare(b));
|
|
501
|
+
this.#settingsManager.set("task.disabledAgents", disabled);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#persistModelOverrides(): void {
|
|
505
|
+
if (!this.#settingsManager) return;
|
|
506
|
+
const overrides: Record<string, string> = {};
|
|
507
|
+
for (const agent of this.#allAgents) {
|
|
508
|
+
const value = agent.overrideModel?.trim();
|
|
509
|
+
if (value) {
|
|
510
|
+
overrides[agent.name] = value;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
this.#settingsManager.set("task.agentModelOverrides", overrides);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
#toggleSelectedAgent(): void {
|
|
517
|
+
const selected = this.#selectedAgent();
|
|
518
|
+
if (!selected) return;
|
|
519
|
+
selected.disabled = !selected.disabled;
|
|
520
|
+
this.#persistDisabledAgents();
|
|
521
|
+
this.#buildLayout();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#beginModelEdit(): void {
|
|
525
|
+
const selected = this.#selectedAgent();
|
|
526
|
+
if (!selected) return;
|
|
527
|
+
this.#createError = null;
|
|
528
|
+
this.#editingAgentName = selected.name;
|
|
529
|
+
this.#editInput = new Input();
|
|
530
|
+
if (selected.overrideModel) {
|
|
531
|
+
this.#editInput.setValue(selected.overrideModel);
|
|
532
|
+
}
|
|
533
|
+
this.#editInput.onSubmit = value => {
|
|
534
|
+
this.#saveModelOverride(value);
|
|
535
|
+
};
|
|
536
|
+
this.#buildLayout();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
#saveModelOverride(rawValue: string): void {
|
|
540
|
+
if (!this.#editingAgentName) return;
|
|
541
|
+
const selected = this.#allAgents.find(agent => agent.name === this.#editingAgentName);
|
|
542
|
+
if (!selected) return;
|
|
543
|
+
const value = rawValue.trim();
|
|
544
|
+
selected.overrideModel = value || undefined;
|
|
545
|
+
this.#persistModelOverrides();
|
|
546
|
+
this.#editingAgentName = null;
|
|
547
|
+
this.#editInput = null;
|
|
548
|
+
this.#applyFilters();
|
|
549
|
+
this.#notice = `Updated model override for ${selected.name}`;
|
|
550
|
+
this.#buildLayout();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#cancelModelEdit(): void {
|
|
554
|
+
this.#editingAgentName = null;
|
|
555
|
+
this.#editInput = null;
|
|
556
|
+
this.#buildLayout();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#beginCreateFlow(): void {
|
|
560
|
+
if (this.#createGenerating) return;
|
|
561
|
+
this.#createError = null;
|
|
562
|
+
this.#createSpec = null;
|
|
563
|
+
this.#createDescription = "";
|
|
564
|
+
this.#createInput = new Input();
|
|
565
|
+
this.#createInput.onSubmit = value => {
|
|
566
|
+
void this.#generateAgentFromDescription(value);
|
|
567
|
+
};
|
|
568
|
+
this.#buildLayout();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
#clearCreateFlow(): void {
|
|
572
|
+
this.#createInput = null;
|
|
573
|
+
this.#createDescription = "";
|
|
574
|
+
this.#createGenerating = false;
|
|
575
|
+
this.#createSpec = null;
|
|
576
|
+
this.#createError = null;
|
|
577
|
+
this.#createStreamingText = "";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#toggleCreateScope(): void {
|
|
581
|
+
this.#createScope = this.#createScope === "project" ? "user" : "project";
|
|
582
|
+
this.#buildLayout();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async #generateAgentFromDescription(rawDescription: string): Promise<void> {
|
|
586
|
+
const description = rawDescription.trim();
|
|
587
|
+
this.#createDescription = description;
|
|
588
|
+
if (!description) {
|
|
589
|
+
this.#createError = "Description is required.";
|
|
590
|
+
this.#buildLayout();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
this.#createGenerating = true;
|
|
595
|
+
this.#createError = null;
|
|
596
|
+
this.#createSpec = null;
|
|
597
|
+
this.#createStreamingText = "";
|
|
598
|
+
this.#buildLayout();
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const spec = await this.#runAgentCreationArchitect(description);
|
|
602
|
+
this.#createSpec = spec;
|
|
603
|
+
this.#notice = null;
|
|
604
|
+
} catch (error) {
|
|
605
|
+
this.#createError = error instanceof Error ? error.message : String(error);
|
|
606
|
+
} finally {
|
|
607
|
+
this.#createGenerating = false;
|
|
608
|
+
this.#rebuildAndRender();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async #runAgentCreationArchitect(description: string): Promise<GeneratedAgentSpec> {
|
|
613
|
+
const modelRegistry = this.modelContext.modelRegistry;
|
|
614
|
+
if (!modelRegistry) {
|
|
615
|
+
throw new Error("Model registry unavailable in current session.");
|
|
616
|
+
}
|
|
617
|
+
await modelRegistry.refresh();
|
|
618
|
+
|
|
619
|
+
const settings = this.#settingsManager ?? undefined;
|
|
620
|
+
const modelPatterns = normalizeModelPatterns(
|
|
621
|
+
this.modelContext.activeModelPattern ??
|
|
622
|
+
this.modelContext.defaultModelPattern ??
|
|
623
|
+
settings?.getModelRole("default"),
|
|
624
|
+
);
|
|
625
|
+
const { model } = resolveModelOverride(modelPatterns, modelRegistry, settings);
|
|
626
|
+
const fallbackModel = modelRegistry.getAvailable()[0];
|
|
627
|
+
const selectedModel = model ?? fallbackModel;
|
|
628
|
+
if (!selectedModel) {
|
|
629
|
+
throw new Error("No available model to generate agent specification.");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const systemPrompt = renderPromptTemplate(agentCreationArchitectPrompt, { TASK_TOOL_NAME: "task" });
|
|
633
|
+
const userPrompt = renderPromptTemplate(agentCreationUserPrompt, { request: description });
|
|
634
|
+
|
|
635
|
+
const { session } = await createAgentSession({
|
|
636
|
+
cwd: this.cwd,
|
|
637
|
+
authStorage: modelRegistry.authStorage,
|
|
638
|
+
modelRegistry,
|
|
639
|
+
settings,
|
|
640
|
+
model: selectedModel,
|
|
641
|
+
systemPrompt,
|
|
642
|
+
hasUI: false,
|
|
643
|
+
enableLsp: false,
|
|
644
|
+
enableMCP: false,
|
|
645
|
+
disableExtensionDiscovery: true,
|
|
646
|
+
toolNames: ["__none__"],
|
|
647
|
+
customTools: [],
|
|
648
|
+
skills: [],
|
|
649
|
+
contextFiles: [],
|
|
650
|
+
promptTemplates: [],
|
|
651
|
+
slashCommands: [],
|
|
652
|
+
});
|
|
653
|
+
const unsubscribe = session.subscribe(event => {
|
|
654
|
+
if (event.type === "message_update" && "assistantMessageEvent" in event) {
|
|
655
|
+
const ame = event.assistantMessageEvent;
|
|
656
|
+
if (ame.type === "text_delta") {
|
|
657
|
+
this.#createStreamingText += ame.delta;
|
|
658
|
+
this.#rebuildAndRender();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
await session.prompt(userPrompt, { expandPromptTemplates: false });
|
|
665
|
+
const raw = extractAssistantText(session.state.messages);
|
|
666
|
+
if (!raw) {
|
|
667
|
+
throw new Error("No response returned by agent creation architect.");
|
|
668
|
+
}
|
|
669
|
+
return parseGeneratedAgentSpec(raw);
|
|
670
|
+
} finally {
|
|
671
|
+
unsubscribe();
|
|
672
|
+
await session.dispose();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async #saveGeneratedAgent(): Promise<void> {
|
|
677
|
+
const spec = this.#createSpec;
|
|
678
|
+
if (!spec) return;
|
|
679
|
+
|
|
680
|
+
const dirs = getConfigDirs("agents", {
|
|
681
|
+
user: this.#createScope === "user",
|
|
682
|
+
project: this.#createScope === "project",
|
|
683
|
+
cwd: this.cwd,
|
|
684
|
+
});
|
|
685
|
+
const targetDir = dirs[0]?.path;
|
|
686
|
+
if (!targetDir) {
|
|
687
|
+
throw new Error(`Cannot resolve ${this.#createScope} agents directory.`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const filePath = path.join(targetDir, `${spec.identifier}.md`);
|
|
691
|
+
try {
|
|
692
|
+
await fs.stat(filePath);
|
|
693
|
+
throw new Error(`Agent file already exists: ${shortenPath(filePath)}`);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (!isEnoent(error)) {
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const frontmatter = YAML.stringify({
|
|
701
|
+
name: spec.identifier,
|
|
702
|
+
description: spec.whenToUse,
|
|
703
|
+
}).trimEnd();
|
|
704
|
+
const content = `---\n${frontmatter}\n---\n\n${spec.systemPrompt.trim()}\n`;
|
|
705
|
+
await Bun.write(filePath, content);
|
|
706
|
+
await this.#reloadData();
|
|
707
|
+
this.#clearCreateFlow();
|
|
708
|
+
this.#notice = `Created agent ${spec.identifier} at ${shortenPath(filePath)}`;
|
|
709
|
+
this.#rebuildAndRender();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
#getModelSuggestions(input: string): string[] {
|
|
713
|
+
const modelRegistry = this.modelContext.modelRegistry;
|
|
714
|
+
if (!modelRegistry) return [];
|
|
715
|
+
const query = input.trim().toLowerCase();
|
|
716
|
+
if (!query) return [];
|
|
717
|
+
const available = modelRegistry.getAvailable();
|
|
718
|
+
const seen = new Set<string>();
|
|
719
|
+
const matches: string[] = [];
|
|
720
|
+
for (const model of available) {
|
|
721
|
+
const full = `${model.provider}/${model.id}`;
|
|
722
|
+
if (seen.has(full)) continue;
|
|
723
|
+
if (!full.toLowerCase().includes(query)) continue;
|
|
724
|
+
seen.add(full);
|
|
725
|
+
matches.push(full);
|
|
726
|
+
if (matches.length >= 5) break;
|
|
727
|
+
}
|
|
728
|
+
return matches;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
#switchTab(direction: 1 | -1): void {
|
|
732
|
+
if (this.#tabs.length === 0) return;
|
|
733
|
+
this.#activeTabIndex = (this.#activeTabIndex + direction + this.#tabs.length) % this.#tabs.length;
|
|
734
|
+
this.#selectedIndex = 0;
|
|
735
|
+
this.#scrollOffset = 0;
|
|
736
|
+
this.#applyFilters();
|
|
737
|
+
this.#buildLayout();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
#moveSelection(delta: -1 | 1): void {
|
|
741
|
+
if (this.#filteredAgents.length === 0) return;
|
|
742
|
+
this.#selectedIndex = Math.max(0, Math.min(this.#filteredAgents.length - 1, this.#selectedIndex + delta));
|
|
743
|
+
this.#clampSelection();
|
|
744
|
+
this.#buildLayout();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
#defaultPatternsFor(agent: DashboardAgent): string[] {
|
|
748
|
+
const explicitAgentPatterns = isDefaultModelAlias(agent.model) ? [] : normalizeModelPatterns(agent.model);
|
|
749
|
+
if (explicitAgentPatterns.length > 0) {
|
|
750
|
+
return explicitAgentPatterns;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const fallback =
|
|
754
|
+
this.modelContext.activeModelPattern?.trim() ||
|
|
755
|
+
this.modelContext.defaultModelPattern?.trim() ||
|
|
756
|
+
this.#settingsManager?.getModelRole("default")?.trim() ||
|
|
757
|
+
"";
|
|
758
|
+
if (!fallback) return [];
|
|
759
|
+
return normalizeModelPatterns(fallback);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
#effectivePatternsFor(agent: DashboardAgent, draftOverride: string | undefined): string[] {
|
|
763
|
+
const override = draftOverride?.trim() || "";
|
|
764
|
+
if (override.length > 0) {
|
|
765
|
+
return [override];
|
|
766
|
+
}
|
|
767
|
+
return this.#defaultPatternsFor(agent);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
#resolvePatterns(patterns: string[]): ModelResolution | undefined {
|
|
771
|
+
const modelRegistry = this.modelContext.modelRegistry;
|
|
772
|
+
if (!modelRegistry || patterns.length === 0) return undefined;
|
|
773
|
+
const { model, thinkingLevel } = resolveModelOverride(
|
|
774
|
+
patterns,
|
|
775
|
+
modelRegistry,
|
|
776
|
+
this.#settingsManager ?? undefined,
|
|
777
|
+
);
|
|
778
|
+
if (!model) return undefined;
|
|
779
|
+
return {
|
|
780
|
+
resolved: formatModelString(model),
|
|
781
|
+
thinkingLevel,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
#renderTabBar(): string {
|
|
786
|
+
const parts: string[] = [" "];
|
|
787
|
+
for (let i = 0; i < this.#tabs.length; i++) {
|
|
788
|
+
const tab = this.#tabs[i];
|
|
789
|
+
const label = `${tab.label} (${tab.count})`;
|
|
790
|
+
if (i === this.#activeTabIndex) {
|
|
791
|
+
parts.push(theme.bg("selectedBg", ` ${label} `));
|
|
792
|
+
} else {
|
|
793
|
+
parts.push(theme.fg("muted", ` ${label} `));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return parts.join("");
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
#renderCreateInput(): void {
|
|
800
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", " Create New Agent")), 0, 0));
|
|
801
|
+
this.addChild(new Spacer(1));
|
|
802
|
+
this.addChild(new Text(theme.fg("muted", "Describe what the new agent should do:"), 0, 0));
|
|
803
|
+
this.addChild(new Spacer(1));
|
|
804
|
+
if (this.#createInput) {
|
|
805
|
+
this.addChild(this.#createInput);
|
|
806
|
+
}
|
|
807
|
+
this.addChild(new Spacer(1));
|
|
808
|
+
this.addChild(new Text(theme.fg("muted", `Scope: ${this.#createScope}`), 0, 0));
|
|
809
|
+
if (this.#createGenerating) {
|
|
810
|
+
this.addChild(new Spacer(1));
|
|
811
|
+
this.addChild(new Text(theme.fg("accent", "Generating agent specification..."), 0, 0));
|
|
812
|
+
if (this.#createStreamingText) {
|
|
813
|
+
this.addChild(new Spacer(1));
|
|
814
|
+
const maxPreview = Math.max(3, this.terminalHeight - 18);
|
|
815
|
+
const contentWidth = Math.max(20, this.#uiWidth() - 4);
|
|
816
|
+
const wrappedLines: string[] = [];
|
|
817
|
+
for (const raw of this.#createStreamingText.split("\n")) {
|
|
818
|
+
for (const w of wrapTextWithAnsi(replaceTabs(raw), contentWidth)) {
|
|
819
|
+
wrappedLines.push(w);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const tail = wrappedLines.slice(-maxPreview);
|
|
823
|
+
if (wrappedLines.length > maxPreview) {
|
|
824
|
+
this.addChild(new Text(theme.fg("dim", ` ... ${wrappedLines.length - maxPreview} lines above`), 0, 0));
|
|
825
|
+
}
|
|
826
|
+
for (const line of tail) {
|
|
827
|
+
this.addChild(new Text(theme.fg("dim", ` ${line}`), 0, 0));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (this.#createError) {
|
|
832
|
+
this.addChild(new Text(theme.fg("error", replaceTabs(this.#createError)), 0, 0));
|
|
833
|
+
}
|
|
834
|
+
this.addChild(new Spacer(1));
|
|
835
|
+
const hints = this.#createGenerating ? " Generating..." : " Enter: generate Tab: toggle scope Esc: cancel";
|
|
836
|
+
this.addChild(new Text(theme.fg("dim", hints), 0, 0));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
#renderCreateReview(): void {
|
|
840
|
+
const spec = this.#createSpec;
|
|
841
|
+
if (!spec) return;
|
|
842
|
+
|
|
843
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", " Review Generated Agent")), 0, 0));
|
|
844
|
+
this.addChild(new Spacer(1));
|
|
845
|
+
this.addChild(new Text(theme.fg("muted", `Identifier: ${spec.identifier}`), 0, 0));
|
|
846
|
+
this.addChild(new Text(theme.fg("muted", `Scope: ${this.#createScope}`), 0, 0));
|
|
847
|
+
this.addChild(new Spacer(1));
|
|
848
|
+
this.addChild(new Text(theme.fg("muted", "whenToUse:"), 0, 0));
|
|
849
|
+
for (const line of wrapTextWithAnsi(replaceTabs(spec.whenToUse), Math.max(20, this.#uiWidth() - 2)).slice(0, 8)) {
|
|
850
|
+
this.addChild(new Text(truncateToWidth(line, this.#uiWidth() - 2), 0, 0));
|
|
851
|
+
}
|
|
852
|
+
this.addChild(new Spacer(1));
|
|
853
|
+
this.addChild(new Text(theme.fg("muted", "systemPrompt preview:"), 0, 0));
|
|
854
|
+
const promptWidth = Math.max(20, this.#uiWidth() - 4);
|
|
855
|
+
const wrappedPrompt: string[] = [];
|
|
856
|
+
for (const raw of spec.systemPrompt.split("\n")) {
|
|
857
|
+
for (const w of wrapTextWithAnsi(replaceTabs(raw), promptWidth)) {
|
|
858
|
+
wrappedPrompt.push(w);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const promptPreview = wrappedPrompt.slice(0, 10);
|
|
862
|
+
for (const line of promptPreview) {
|
|
863
|
+
this.addChild(new Text(` ${line}`, 0, 0));
|
|
864
|
+
}
|
|
865
|
+
if (wrappedPrompt.length > promptPreview.length) {
|
|
866
|
+
this.addChild(
|
|
867
|
+
new Text(theme.fg("dim", ` ... ${wrappedPrompt.length - promptPreview.length} more lines`), 0, 0),
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
if (this.#createError) {
|
|
871
|
+
this.addChild(new Spacer(1));
|
|
872
|
+
this.addChild(new Text(theme.fg("error", replaceTabs(this.#createError)), 0, 0));
|
|
873
|
+
}
|
|
874
|
+
this.addChild(new Spacer(1));
|
|
875
|
+
this.addChild(new Text(theme.fg("dim", " Enter: save Tab: toggle scope R: regenerate Esc: cancel"), 0, 0));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
#uiWidth(): number {
|
|
879
|
+
return Math.max(40, process.stdout.columns ?? 100);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Rebuild layout and request a TUI render pass (for use after async state changes). */
|
|
883
|
+
#rebuildAndRender(): void {
|
|
884
|
+
this.#buildLayout();
|
|
885
|
+
this.onRequestRender?.();
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
#buildLayout(): void {
|
|
889
|
+
this.clear();
|
|
890
|
+
this.addChild(new DynamicBorder());
|
|
891
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", " Agent Control Center")), 0, 0));
|
|
892
|
+
this.addChild(new Text(this.#renderTabBar(), 0, 0));
|
|
893
|
+
this.addChild(new Spacer(1));
|
|
894
|
+
|
|
895
|
+
if (this.#notice) {
|
|
896
|
+
this.addChild(new Text(theme.fg("success", replaceTabs(this.#notice)), 0, 0));
|
|
897
|
+
this.addChild(new Spacer(1));
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (this.#loading) {
|
|
901
|
+
this.addChild(new Text(theme.fg("muted", "Loading agents..."), 0, 0));
|
|
902
|
+
this.addChild(new Spacer(1));
|
|
903
|
+
} else if (this.#loadError) {
|
|
904
|
+
this.addChild(new Text(theme.fg("error", `Failed to load agents: ${replaceTabs(this.#loadError)}`), 0, 0));
|
|
905
|
+
this.addChild(new Spacer(1));
|
|
906
|
+
} else if (this.#createSpec) {
|
|
907
|
+
this.#renderCreateReview();
|
|
908
|
+
} else if (this.#createInput || this.#createGenerating) {
|
|
909
|
+
this.#renderCreateInput();
|
|
910
|
+
} else if (this.#editInput && this.#editingAgentName) {
|
|
911
|
+
const editingAgent = this.#allAgents.find(agent => agent.name === this.#editingAgentName) ?? null;
|
|
912
|
+
const draft = this.#editInput.getValue();
|
|
913
|
+
const defaultPatterns = editingAgent ? this.#defaultPatternsFor(editingAgent) : [];
|
|
914
|
+
const defaultResolution = editingAgent ? this.#resolvePatterns(defaultPatterns) : undefined;
|
|
915
|
+
const previewPatterns = editingAgent ? this.#effectivePatternsFor(editingAgent, draft) : [];
|
|
916
|
+
const previewResolution = editingAgent ? this.#resolvePatterns(previewPatterns) : undefined;
|
|
917
|
+
const suggestions = this.#getModelSuggestions(draft);
|
|
918
|
+
|
|
919
|
+
this.addChild(
|
|
920
|
+
new Text(theme.bold(theme.fg("accent", `Model override: ${replaceTabs(this.#editingAgentName)}`)), 0, 0),
|
|
921
|
+
);
|
|
922
|
+
this.addChild(new Spacer(1));
|
|
923
|
+
this.addChild(new Text(theme.fg("muted", "Enter model pattern (empty clears override)"), 0, 0));
|
|
924
|
+
this.addChild(new Spacer(1));
|
|
925
|
+
this.addChild(this.#editInput);
|
|
926
|
+
this.addChild(new Spacer(1));
|
|
927
|
+
|
|
928
|
+
this.addChild(
|
|
929
|
+
new Text(theme.fg("muted", `Default pattern: ${replaceTabs(joinPatterns(defaultPatterns))}`), 0, 0),
|
|
930
|
+
);
|
|
931
|
+
this.addChild(
|
|
932
|
+
new Text(
|
|
933
|
+
theme.fg(
|
|
934
|
+
"muted",
|
|
935
|
+
`Default resolves: ${defaultResolution ? defaultResolution.resolved : "(unresolved)"}`,
|
|
936
|
+
),
|
|
937
|
+
0,
|
|
938
|
+
0,
|
|
939
|
+
),
|
|
940
|
+
);
|
|
941
|
+
this.addChild(
|
|
942
|
+
new Text(
|
|
943
|
+
theme.fg(
|
|
944
|
+
"muted",
|
|
945
|
+
`Preview effective: ${previewResolution ? previewResolution.resolved : "(unresolved)"}`,
|
|
946
|
+
),
|
|
947
|
+
0,
|
|
948
|
+
0,
|
|
949
|
+
),
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
if (suggestions.length > 0) {
|
|
953
|
+
this.addChild(new Spacer(1));
|
|
954
|
+
this.addChild(new Text(theme.fg("muted", "Suggestions:"), 0, 0));
|
|
955
|
+
for (const suggestion of suggestions) {
|
|
956
|
+
this.addChild(new Text(theme.fg("dim", ` ${suggestion}`), 0, 0));
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
this.addChild(new Spacer(1));
|
|
961
|
+
this.addChild(new Text(theme.fg("dim", " Enter: save Esc: cancel"), 0, 0));
|
|
962
|
+
} else {
|
|
963
|
+
const selected = this.#selectedAgent();
|
|
964
|
+
const defaultPatterns = selected ? this.#defaultPatternsFor(selected) : [];
|
|
965
|
+
const defaultResolution = selected ? this.#resolvePatterns(defaultPatterns) : undefined;
|
|
966
|
+
const effectivePatterns = selected ? this.#effectivePatternsFor(selected, selected.overrideModel) : [];
|
|
967
|
+
const effectiveResolution = selected ? this.#resolvePatterns(effectivePatterns) : undefined;
|
|
968
|
+
|
|
969
|
+
const listPane = new AgentListPane(
|
|
970
|
+
this.#filteredAgents,
|
|
971
|
+
this.#selectedIndex,
|
|
972
|
+
this.#scrollOffset,
|
|
973
|
+
this.#searchQuery,
|
|
974
|
+
this.#getMaxVisibleItems(),
|
|
975
|
+
);
|
|
976
|
+
const inspector = new AgentInspectorPane(
|
|
977
|
+
selected,
|
|
978
|
+
defaultPatterns,
|
|
979
|
+
defaultResolution,
|
|
980
|
+
effectivePatterns,
|
|
981
|
+
effectiveResolution,
|
|
982
|
+
);
|
|
983
|
+
const bodyHeight = Math.max(5, this.terminalHeight - 8);
|
|
984
|
+
this.addChild(new TwoColumnBody(listPane, inspector, bodyHeight));
|
|
985
|
+
this.addChild(new Spacer(1));
|
|
986
|
+
this.addChild(
|
|
987
|
+
new Text(
|
|
988
|
+
theme.fg(
|
|
989
|
+
"dim",
|
|
990
|
+
" ↑/↓: navigate Space: toggle Enter: model override N: new agent Tab: source Ctrl+R: reload Esc: close",
|
|
991
|
+
),
|
|
992
|
+
0,
|
|
993
|
+
0,
|
|
994
|
+
),
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
this.addChild(new DynamicBorder());
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
handleInput(data: string): void {
|
|
1002
|
+
const charCode = data.length === 1 ? data.charCodeAt(0) : -1;
|
|
1003
|
+
|
|
1004
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
1005
|
+
this.onClose?.();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (this.#createSpec) {
|
|
1010
|
+
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
|
1011
|
+
this.#clearCreateFlow();
|
|
1012
|
+
this.#buildLayout();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (matchesKey(data, "tab") || matchesKey(data, "shift+tab")) {
|
|
1016
|
+
this.#toggleCreateScope();
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (data.toLowerCase() === "r") {
|
|
1020
|
+
void this.#generateAgentFromDescription(this.#createDescription);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
1024
|
+
void this.#saveGeneratedAgent().catch(error => {
|
|
1025
|
+
this.#createError = error instanceof Error ? error.message : String(error);
|
|
1026
|
+
this.#rebuildAndRender();
|
|
1027
|
+
});
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (this.#createInput || this.#createGenerating) {
|
|
1034
|
+
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
|
1035
|
+
if (!this.#createGenerating) {
|
|
1036
|
+
this.#clearCreateFlow();
|
|
1037
|
+
this.#buildLayout();
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (!this.#createGenerating && (matchesKey(data, "tab") || matchesKey(data, "shift+tab"))) {
|
|
1042
|
+
this.#toggleCreateScope();
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (!this.#createGenerating && this.#createInput) {
|
|
1046
|
+
this.#createInput.handleInput(data);
|
|
1047
|
+
this.#createDescription = this.#createInput.getValue();
|
|
1048
|
+
this.#buildLayout();
|
|
1049
|
+
}
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (this.#editInput) {
|
|
1054
|
+
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
|
1055
|
+
this.#cancelModelEdit();
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
this.#editInput.handleInput(data);
|
|
1059
|
+
if (this.#editInput) {
|
|
1060
|
+
this.#buildLayout();
|
|
1061
|
+
}
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
|
1066
|
+
if (this.#searchQuery.length > 0) {
|
|
1067
|
+
this.#searchQuery = "";
|
|
1068
|
+
this.#applyFilters();
|
|
1069
|
+
this.#buildLayout();
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
this.onClose?.();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (matchesKey(data, "ctrl+r")) {
|
|
1077
|
+
void this.#reloadData();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (matchesKey(data, "tab")) {
|
|
1082
|
+
this.#switchTab(1);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (matchesKey(data, "shift+tab")) {
|
|
1086
|
+
this.#switchTab(-1);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
1091
|
+
this.#moveSelection(-1);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
1095
|
+
this.#moveSelection(1);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (data === " ") {
|
|
1100
|
+
this.#toggleSelectedAgent();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
1104
|
+
this.#beginModelEdit();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (data.toLowerCase() === "n") {
|
|
1108
|
+
this.#beginCreateFlow();
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (matchesKey(data, "backspace")) {
|
|
1113
|
+
if (this.#searchQuery.length > 0) {
|
|
1114
|
+
this.#searchQuery = this.#searchQuery.slice(0, -1);
|
|
1115
|
+
this.#applyFilters();
|
|
1116
|
+
this.#buildLayout();
|
|
1117
|
+
}
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (data.length === 1 && charCode > 32 && charCode < 127) {
|
|
1122
|
+
if (data === "j" || data === "k") {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
this.#searchQuery += data;
|
|
1126
|
+
this.#applyFilters();
|
|
1127
|
+
this.#buildLayout();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|