@alisio/alisio-code 0.1.0-alpha.23 → 0.1.0-alpha.25
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/dist/tui/agent-manager.d.ts +105 -0
- package/dist/tui/agent-manager.js +269 -0
- package/dist/tui/app.d.ts +6 -2
- package/dist/tui/app.js +467 -23
- package/dist/tui/connect-input.d.ts +2 -0
- package/dist/tui/connect-input.js +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic of the TUI agent manager (`/agents`): argument parsing, the agent picker rows and
|
|
3
|
+
* their filter, and the editor form with capability-driven options. Rendering and I/O live in
|
|
4
|
+
* `app.ts`; agent files go through the core `AgentDefinitionService` shared with the web server.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentDefinitionInfo, AgentDefinitionInput, AgentDraft, AgentModelCapabilities, AgentReasoningSummary, AgentScope, AgentTemplateInfo } from "@alisio/core";
|
|
7
|
+
import { type ActiveAgent } from "@alisio/core";
|
|
8
|
+
/** Verbs the TUI owns; any other `/agents <verb>` goes to the subagents plugin's command. */
|
|
9
|
+
export type AgentsCommand = {
|
|
10
|
+
kind: "picker";
|
|
11
|
+
} | {
|
|
12
|
+
kind: "new";
|
|
13
|
+
description?: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "templates";
|
|
16
|
+
} | {
|
|
17
|
+
kind: "manage";
|
|
18
|
+
} | {
|
|
19
|
+
kind: "edit";
|
|
20
|
+
id: string;
|
|
21
|
+
scope?: AgentScope;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "delete";
|
|
24
|
+
id: string;
|
|
25
|
+
scope?: AgentScope;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "plugin";
|
|
28
|
+
args: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* `/agents` → picker; `/agents new [description]` (assisted when a description is given),
|
|
32
|
+
* `/agents templates`, `/agents manage`, `/agents edit|delete <id> [project|global]`. Anything
|
|
33
|
+
* else (`list`, `defs`, `reload`, `open`, `cancel`…) belongs to the subagents plugin.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseAgentsArgs(args: string): AgentsCommand;
|
|
36
|
+
/** "Project" / "Global" for agent files, "Built-in" / "Plugin" / "User" otherwise. */
|
|
37
|
+
export declare function agentBadge(agent: Pick<ActiveAgent, "source"> & {
|
|
38
|
+
path?: string;
|
|
39
|
+
}, workspace: string, home: string): string;
|
|
40
|
+
export interface PickerRow {
|
|
41
|
+
value: string;
|
|
42
|
+
label: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Action rows shown above the agents in the `/agents` picker. */
|
|
46
|
+
export declare const AGENT_PICKER_ACTIONS: PickerRow[];
|
|
47
|
+
/** Picker rows: name, badge and markers in the label; description below. */
|
|
48
|
+
export declare function agentPickerRows(agents: Array<ActiveAgent & {
|
|
49
|
+
path?: string;
|
|
50
|
+
}>, activeId: string, roots: {
|
|
51
|
+
workspace: string;
|
|
52
|
+
home: string;
|
|
53
|
+
}): PickerRow[];
|
|
54
|
+
/** Case-insensitive substring match of every query word on the label and description. */
|
|
55
|
+
export declare function filterAgentRows<T extends {
|
|
56
|
+
label: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
value: string;
|
|
59
|
+
}>(rows: ReadonlyArray<T>, query: string): T[];
|
|
60
|
+
/** The editable state of one agent in the TUI editor. */
|
|
61
|
+
export interface AgentForm {
|
|
62
|
+
scope: AgentScope;
|
|
63
|
+
/** Set when editing a saved agent. */
|
|
64
|
+
id?: string;
|
|
65
|
+
name: string;
|
|
66
|
+
description: string;
|
|
67
|
+
instructions: string;
|
|
68
|
+
model: string;
|
|
69
|
+
reasoning: {
|
|
70
|
+
effort?: string;
|
|
71
|
+
summary?: AgentReasoningSummary;
|
|
72
|
+
};
|
|
73
|
+
text: {
|
|
74
|
+
format?: "text" | "json_object" | "json_schema";
|
|
75
|
+
verbosity?: "low" | "medium" | "high";
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export declare function emptyAgentForm(scope: AgentScope, model: string): AgentForm;
|
|
79
|
+
export declare function formFromTemplate(template: AgentTemplateInfo, scope: AgentScope, model: string): AgentForm;
|
|
80
|
+
export declare function formFromDefinition(definition: AgentDefinitionInfo): AgentForm;
|
|
81
|
+
/** Applies a model draft to the form (keeps scope, id and model). */
|
|
82
|
+
export declare function applyDraft(form: AgentForm, draft: AgentDraft): AgentForm;
|
|
83
|
+
/** The API/service body of a form, fitted to the model's capabilities. */
|
|
84
|
+
export declare function formToInput(form: AgentForm, capabilities?: AgentModelCapabilities): AgentDefinitionInput;
|
|
85
|
+
/** Validation errors of a form (empty when it can be saved). */
|
|
86
|
+
export declare function formErrors(form: AgentForm): string[];
|
|
87
|
+
/** One-line editing of multi-line text: newlines shown and typed as `\n`. */
|
|
88
|
+
export declare const encodeMultiline: (text: string) => string;
|
|
89
|
+
export declare const decodeMultiline: (text: string) => string;
|
|
90
|
+
export type AgentField = "scope" | "name" | "description" | "instructions" | "model" | "effort" | "summary" | "verbosity" | "format" | "refine" | "save" | "cancel";
|
|
91
|
+
/**
|
|
92
|
+
* Editor rows. Settings the model does not support are shown as "not supported by this model"
|
|
93
|
+
* (their value is ignored on save); `dirty` enables the Save row.
|
|
94
|
+
*/
|
|
95
|
+
export declare function agentEditorRows(form: AgentForm, capabilities: AgentModelCapabilities, options: {
|
|
96
|
+
creating: boolean;
|
|
97
|
+
scopes: AgentScope[];
|
|
98
|
+
dirty: boolean;
|
|
99
|
+
}): Array<PickerRow & {
|
|
100
|
+
value: AgentField;
|
|
101
|
+
}>;
|
|
102
|
+
/** Choice rows of a select field, honoring the model capabilities. */
|
|
103
|
+
export declare function agentFieldChoices(field: "effort" | "summary" | "verbosity" | "format", capabilities: AgentModelCapabilities): PickerRow[];
|
|
104
|
+
/** Whether a field can be edited for the model (unsupported settings are read-only). */
|
|
105
|
+
export declare function agentFieldSupported(field: "effort" | "summary" | "verbosity", capabilities: AgentModelCapabilities): boolean;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { AGENT_REASONING_SUMMARIES, AGENT_TEXT_FORMATS, AGENT_VERBOSITIES, DEFAULT_AGENT_ID, fitAgentSettings, validateAgentDefinitionInput, } from "@alisio/core";
|
|
2
|
+
/**
|
|
3
|
+
* `/agents` → picker; `/agents new [description]` (assisted when a description is given),
|
|
4
|
+
* `/agents templates`, `/agents manage`, `/agents edit|delete <id> [project|global]`. Anything
|
|
5
|
+
* else (`list`, `defs`, `reload`, `open`, `cancel`…) belongs to the subagents plugin.
|
|
6
|
+
*/
|
|
7
|
+
export function parseAgentsArgs(args) {
|
|
8
|
+
const text = args.trim();
|
|
9
|
+
if (!text)
|
|
10
|
+
return { kind: "picker" };
|
|
11
|
+
const [verb = "", ...rest] = text.split(/\s+/);
|
|
12
|
+
const tail = text.slice(verb.length).trim();
|
|
13
|
+
const scope = (value) => value === "project" || value === "global" ? value : undefined;
|
|
14
|
+
switch (verb.toLowerCase()) {
|
|
15
|
+
case "new":
|
|
16
|
+
case "create":
|
|
17
|
+
return tail ? { kind: "new", description: tail } : { kind: "new" };
|
|
18
|
+
case "templates":
|
|
19
|
+
case "template":
|
|
20
|
+
return { kind: "templates" };
|
|
21
|
+
case "manage":
|
|
22
|
+
return { kind: "manage" };
|
|
23
|
+
case "edit":
|
|
24
|
+
case "delete":
|
|
25
|
+
case "rm": {
|
|
26
|
+
const [id, where] = rest;
|
|
27
|
+
if (!id)
|
|
28
|
+
return { kind: "manage" };
|
|
29
|
+
const found = scope(where);
|
|
30
|
+
return {
|
|
31
|
+
kind: verb.toLowerCase() === "edit" ? "edit" : "delete",
|
|
32
|
+
id,
|
|
33
|
+
...(found ? { scope: found } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
default:
|
|
37
|
+
return { kind: "plugin", args: text };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** "Project" / "Global" for agent files, "Built-in" / "Plugin" / "User" otherwise. */
|
|
41
|
+
export function agentBadge(agent, workspace, home) {
|
|
42
|
+
if (agent.source === "builtin")
|
|
43
|
+
return "Built-in";
|
|
44
|
+
if (agent.source === "plugin")
|
|
45
|
+
return "Plugin";
|
|
46
|
+
const path = agent.path ?? "";
|
|
47
|
+
if (path.startsWith(`${workspace}/`) || path.startsWith(`${workspace}\\`))
|
|
48
|
+
return "Project";
|
|
49
|
+
if (path.startsWith(`${home}/`) || path.startsWith(`${home}\\`))
|
|
50
|
+
return "Global";
|
|
51
|
+
return "User";
|
|
52
|
+
}
|
|
53
|
+
/** Action rows shown above the agents in the `/agents` picker. */
|
|
54
|
+
export const AGENT_PICKER_ACTIONS = [
|
|
55
|
+
{
|
|
56
|
+
value: "__agents:new",
|
|
57
|
+
label: "+ Create agent…",
|
|
58
|
+
description: "Blank, from a template or written by Alisio",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
value: "__agents:manage",
|
|
62
|
+
label: "✎ Manage saved agents…",
|
|
63
|
+
description: "Edit or delete project and global agents",
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
/** Picker rows: name, badge and markers in the label; description below. */
|
|
67
|
+
export function agentPickerRows(agents, activeId, roots) {
|
|
68
|
+
return agents.map((agent) => ({
|
|
69
|
+
value: agent.id,
|
|
70
|
+
label: [
|
|
71
|
+
agent.name,
|
|
72
|
+
`[${agentBadge(agent, roots.workspace, roots.home)}]`,
|
|
73
|
+
agent.id === activeId ? "(current)" : undefined,
|
|
74
|
+
agent.id === DEFAULT_AGENT_ID ? "(default)" : undefined,
|
|
75
|
+
agent.readOnly ? "read-only" : undefined,
|
|
76
|
+
]
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.join(" "),
|
|
79
|
+
description: [agent.description, agent.model ? `model: ${agent.model}` : undefined]
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(" · "),
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
/** Case-insensitive substring match of every query word on the label and description. */
|
|
85
|
+
export function filterAgentRows(rows, query) {
|
|
86
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
87
|
+
if (!words.length)
|
|
88
|
+
return [...rows];
|
|
89
|
+
return rows.filter((row) => {
|
|
90
|
+
const haystack = `${row.value} ${row.label} ${row.description ?? ""}`.toLowerCase();
|
|
91
|
+
return words.every((word) => haystack.includes(word));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export function emptyAgentForm(scope, model) {
|
|
95
|
+
return {
|
|
96
|
+
scope,
|
|
97
|
+
name: "New agent",
|
|
98
|
+
description: "",
|
|
99
|
+
instructions: "",
|
|
100
|
+
model,
|
|
101
|
+
reasoning: {},
|
|
102
|
+
text: {},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const settingsOf = (source) => ({
|
|
106
|
+
reasoning: {
|
|
107
|
+
...(source.reasoning?.effort ? { effort: source.reasoning.effort } : {}),
|
|
108
|
+
...(source.reasoning?.summary ? { summary: source.reasoning.summary } : {}),
|
|
109
|
+
},
|
|
110
|
+
text: {
|
|
111
|
+
...(source.text?.format?.type ? { format: source.text.format.type } : {}),
|
|
112
|
+
...(source.text?.verbosity ? { verbosity: source.text.verbosity } : {}),
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
export function formFromTemplate(template, scope, model) {
|
|
116
|
+
return {
|
|
117
|
+
scope,
|
|
118
|
+
name: template.name,
|
|
119
|
+
description: template.description,
|
|
120
|
+
instructions: template.instructions,
|
|
121
|
+
model,
|
|
122
|
+
...settingsOf(template),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function formFromDefinition(definition) {
|
|
126
|
+
return {
|
|
127
|
+
scope: definition.scope,
|
|
128
|
+
id: definition.id,
|
|
129
|
+
name: definition.name,
|
|
130
|
+
description: definition.description,
|
|
131
|
+
instructions: definition.instructions,
|
|
132
|
+
model: definition.model,
|
|
133
|
+
...settingsOf(definition),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Applies a model draft to the form (keeps scope, id and model). */
|
|
137
|
+
export function applyDraft(form, draft) {
|
|
138
|
+
return {
|
|
139
|
+
...form,
|
|
140
|
+
name: draft.name,
|
|
141
|
+
description: draft.description,
|
|
142
|
+
instructions: draft.instructions,
|
|
143
|
+
...settingsOf(draft),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** The API/service body of a form, fitted to the model's capabilities. */
|
|
147
|
+
export function formToInput(form, capabilities) {
|
|
148
|
+
const input = {
|
|
149
|
+
name: form.name.trim(),
|
|
150
|
+
...(form.description.trim() ? { description: form.description.trim() } : {}),
|
|
151
|
+
...(form.instructions.trim() ? { instructions: form.instructions.trim() } : {}),
|
|
152
|
+
model: form.model.trim(),
|
|
153
|
+
...(form.reasoning.effort || form.reasoning.summary
|
|
154
|
+
? {
|
|
155
|
+
reasoning: {
|
|
156
|
+
...(form.reasoning.effort ? { effort: form.reasoning.effort } : {}),
|
|
157
|
+
...(form.reasoning.summary ? { summary: form.reasoning.summary } : {}),
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
: {}),
|
|
161
|
+
...(form.text.format || form.text.verbosity
|
|
162
|
+
? {
|
|
163
|
+
text: {
|
|
164
|
+
...(form.text.format ? { format: { type: form.text.format } } : {}),
|
|
165
|
+
...(form.text.verbosity ? { verbosity: form.text.verbosity } : {}),
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
: {}),
|
|
169
|
+
};
|
|
170
|
+
return capabilities ? fitAgentSettings(input, capabilities) : input;
|
|
171
|
+
}
|
|
172
|
+
/** Validation errors of a form (empty when it can be saved). */
|
|
173
|
+
export function formErrors(form) {
|
|
174
|
+
const result = validateAgentDefinitionInput(formToInput(form));
|
|
175
|
+
return result.ok ? [] : result.errors.map((e) => e.message);
|
|
176
|
+
}
|
|
177
|
+
/** One-line editing of multi-line text: newlines shown and typed as `\n`. */
|
|
178
|
+
export const encodeMultiline = (text) => text.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
|
|
179
|
+
export const decodeMultiline = (text) => text.replace(/\\(\\|n)/g, (_, c) => (c === "n" ? "\n" : "\\"));
|
|
180
|
+
const clip = (text, max = 60) => {
|
|
181
|
+
const line = text.replace(/\s+/g, " ").trim();
|
|
182
|
+
return line.length > max ? `${line.slice(0, max - 1)}…` : line;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Editor rows. Settings the model does not support are shown as "not supported by this model"
|
|
186
|
+
* (their value is ignored on save); `dirty` enables the Save row.
|
|
187
|
+
*/
|
|
188
|
+
export function agentEditorRows(form, capabilities, options) {
|
|
189
|
+
const unsupported = "not supported by this model";
|
|
190
|
+
const rows = [];
|
|
191
|
+
if (options.creating && options.scopes.length > 1)
|
|
192
|
+
rows.push({
|
|
193
|
+
value: "scope",
|
|
194
|
+
label: `Scope: ${form.scope === "project" ? "Project" : "Global"}`,
|
|
195
|
+
description: "Project: .agents/agents in this workspace · Global: ~/.agents/agents",
|
|
196
|
+
});
|
|
197
|
+
rows.push({ value: "name", label: `Name: ${form.name || "(empty)"}` }, {
|
|
198
|
+
value: "description",
|
|
199
|
+
label: `Description: ${clip(form.description) || "(derived from instructions)"}`,
|
|
200
|
+
}, {
|
|
201
|
+
value: "instructions",
|
|
202
|
+
label: `Instructions: ${clip(form.instructions) || "(empty)"}`,
|
|
203
|
+
description: `${form.instructions.length} characters`,
|
|
204
|
+
}, { value: "model", label: `Model: ${form.model || "(choose one)"}` }, {
|
|
205
|
+
value: "effort",
|
|
206
|
+
label: `Reasoning effort: ${capabilities.reasoning ? (form.reasoning.effort ?? "model default") : "—"}`,
|
|
207
|
+
...(capabilities.reasoning ? {} : { description: unsupported }),
|
|
208
|
+
}, {
|
|
209
|
+
value: "summary",
|
|
210
|
+
label: `Reasoning summary: ${capabilities.summary ? (form.reasoning.summary ?? "auto") : "—"}`,
|
|
211
|
+
...(capabilities.summary ? {} : { description: unsupported }),
|
|
212
|
+
}, {
|
|
213
|
+
value: "verbosity",
|
|
214
|
+
label: `Verbosity: ${capabilities.verbosity ? (form.text.verbosity ?? "default") : "—"}`,
|
|
215
|
+
...(capabilities.verbosity ? {} : { description: unsupported }),
|
|
216
|
+
}, { value: "format", label: `Text format: ${form.text.format ?? "text"}` }, {
|
|
217
|
+
value: "refine",
|
|
218
|
+
label: "✦ Refine with Alisio…",
|
|
219
|
+
description: "The active model rewrites the instructions to the authoring standards",
|
|
220
|
+
});
|
|
221
|
+
const errors = formErrors(form);
|
|
222
|
+
rows.push({
|
|
223
|
+
value: "save",
|
|
224
|
+
label: options.dirty && !errors.length
|
|
225
|
+
? "✓ Save agent definition"
|
|
226
|
+
: "✓ Save agent definition (nothing to save)",
|
|
227
|
+
...(errors.length
|
|
228
|
+
? { description: errors.join("; ") }
|
|
229
|
+
: options.dirty
|
|
230
|
+
? {}
|
|
231
|
+
: { description: "No unsaved changes" }),
|
|
232
|
+
});
|
|
233
|
+
rows.push({ value: "cancel", label: "✗ Close editor" });
|
|
234
|
+
return rows;
|
|
235
|
+
}
|
|
236
|
+
/** Choice rows of a select field, honoring the model capabilities. */
|
|
237
|
+
export function agentFieldChoices(field, capabilities) {
|
|
238
|
+
const levels = field === "effort"
|
|
239
|
+
? capabilities.effortLevels
|
|
240
|
+
: field === "summary"
|
|
241
|
+
? AGENT_REASONING_SUMMARIES
|
|
242
|
+
: field === "verbosity"
|
|
243
|
+
? AGENT_VERBOSITIES
|
|
244
|
+
: AGENT_TEXT_FORMATS.filter((format) => capabilities.textFormats.includes(format));
|
|
245
|
+
return [
|
|
246
|
+
...levels.map((value) => ({
|
|
247
|
+
value,
|
|
248
|
+
label: field === "format"
|
|
249
|
+
? value === "json_object"
|
|
250
|
+
? "JSON"
|
|
251
|
+
: value === "json_schema"
|
|
252
|
+
? "JSON Schema"
|
|
253
|
+
: "Text"
|
|
254
|
+
: value,
|
|
255
|
+
...(field === "effort" && value === capabilities.defaultEffort
|
|
256
|
+
? { description: "model default" }
|
|
257
|
+
: {}),
|
|
258
|
+
})),
|
|
259
|
+
...(field === "format" ? [] : [{ value: "!clear", label: "(use the default)" }]),
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
/** Whether a field can be edited for the model (unsupported settings are read-only). */
|
|
263
|
+
export function agentFieldSupported(field, capabilities) {
|
|
264
|
+
return field === "effort"
|
|
265
|
+
? capabilities.reasoning
|
|
266
|
+
: field === "summary"
|
|
267
|
+
? capabilities.summary
|
|
268
|
+
: capabilities.verbosity;
|
|
269
|
+
}
|
package/dist/tui/app.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AppOptions } from "@alisio/core";
|
|
2
2
|
import { type Component, type SelectItem } from "@earendil-works/pi-tui";
|
|
3
3
|
/**
|
|
4
4
|
* Inline selection list with type-to-filter, rendered above the editor.
|
|
@@ -15,6 +15,8 @@ export declare class Picker implements Component {
|
|
|
15
15
|
private readonly onCancelPick;
|
|
16
16
|
private filterable;
|
|
17
17
|
private detail?;
|
|
18
|
+
/** Custom filter (default: value prefix, keeping group headers). */
|
|
19
|
+
private readonly match?;
|
|
18
20
|
private list;
|
|
19
21
|
private filter;
|
|
20
22
|
/** Full item sequence (group headers + rows) before any filtering. */
|
|
@@ -25,7 +27,9 @@ export declare class Picker implements Component {
|
|
|
25
27
|
private index;
|
|
26
28
|
/** Picker height target: counted on real rows only, so headers never stretch the list. */
|
|
27
29
|
private readonly maxVisible;
|
|
28
|
-
constructor(title: string, items: SelectItem[], onSelectItem: (item: SelectItem) => void, onCancelPick: () => void, filterable?: boolean, detail?: string | undefined
|
|
30
|
+
constructor(title: string, items: SelectItem[], onSelectItem: (item: SelectItem) => void, onCancelPick: () => void, filterable?: boolean, detail?: string | undefined,
|
|
31
|
+
/** Custom filter (default: value prefix, keeping group headers). */
|
|
32
|
+
match?: ((items: SelectItem[], filter: string) => SelectItem[]) | undefined);
|
|
29
33
|
/** Copies items, dimming group-header labels (style helpers stay plain under NO_COLOR). */
|
|
30
34
|
private static displayItems;
|
|
31
35
|
/** (Re)builds the SelectList over the filtered display sequence; selection starts on a row. */
|
package/dist/tui/app.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
+
import { AGENT_TEMPLATES, agentAuthoringGuidance, agentCommandDescriptors, agentIdFromCommand, agentModelCapabilities, agentModelLabels, agentModelOptions, generateAgentDraft, } from "@alisio/core";
|
|
2
3
|
import { CombinedAutocompleteProvider, Container, Editor, getImageDimensions, getNativeClipboard, Key, MouseRegion, matchesKey, ProcessTerminal, ScrollView, SelectList, TuiAltScreen, truncateToWidth, VStack, } from "@earendil-works/pi-tui";
|
|
3
4
|
import { loadVersion } from "../version.js";
|
|
4
|
-
import {
|
|
5
|
+
import { AGENT_PICKER_ACTIONS, agentEditorRows, agentFieldChoices, agentFieldSupported, agentPickerRows, applyDraft, decodeMultiline, emptyAgentForm, encodeMultiline, filterAgentRows, formErrors, formFromDefinition, formFromTemplate, formToInput, parseAgentsArgs, } from "./agent-manager.js";
|
|
6
|
+
import { activeAgentCatalog, agentRunOptions, mainAgentFromRecord, resolveActiveAgent, } from "./agents.js";
|
|
5
7
|
import { MAX_ATTACHMENTS_PER_MESSAGE, MAX_IMAGE_BYTES, pasteImageFromClipboard, removeLastAttachment, toApiAttachment, } from "./attachments.js";
|
|
6
8
|
import { BTW_DESCRIPTION, BTW_USAGE, btwBrowser, btwPending } from "./btw.js";
|
|
7
9
|
import { copyText, nodeSpawn } from "./clipboard.js";
|
|
@@ -33,6 +35,7 @@ export class Picker {
|
|
|
33
35
|
onCancelPick;
|
|
34
36
|
filterable;
|
|
35
37
|
detail;
|
|
38
|
+
match;
|
|
36
39
|
list = new SelectList([], 1, selectListTheme);
|
|
37
40
|
filter = "";
|
|
38
41
|
/** Full item sequence (group headers + rows) before any filtering. */
|
|
@@ -43,12 +46,15 @@ export class Picker {
|
|
|
43
46
|
index = 0;
|
|
44
47
|
/** Picker height target: counted on real rows only, so headers never stretch the list. */
|
|
45
48
|
maxVisible;
|
|
46
|
-
constructor(title, items, onSelectItem, onCancelPick, filterable = false, detail
|
|
49
|
+
constructor(title, items, onSelectItem, onCancelPick, filterable = false, detail,
|
|
50
|
+
/** Custom filter (default: value prefix, keeping group headers). */
|
|
51
|
+
match) {
|
|
47
52
|
this.title = title;
|
|
48
53
|
this.onSelectItem = onSelectItem;
|
|
49
54
|
this.onCancelPick = onCancelPick;
|
|
50
55
|
this.filterable = filterable;
|
|
51
56
|
this.detail = detail;
|
|
57
|
+
this.match = match;
|
|
52
58
|
this.allItems = items;
|
|
53
59
|
this.maxVisible = Math.min(10, Math.max(1, items.filter((item) => !isGroupHeader(item)).length));
|
|
54
60
|
this.resetList();
|
|
@@ -59,7 +65,11 @@ export class Picker {
|
|
|
59
65
|
}
|
|
60
66
|
/** (Re)builds the SelectList over the filtered display sequence; selection starts on a row. */
|
|
61
67
|
resetList() {
|
|
62
|
-
this.items = Picker.displayItems(this.filter
|
|
68
|
+
this.items = Picker.displayItems(this.filter
|
|
69
|
+
? this.match
|
|
70
|
+
? this.match(this.allItems, this.filter)
|
|
71
|
+
: visibleGroupedItems(this.allItems, this.filter)
|
|
72
|
+
: this.allItems);
|
|
63
73
|
this.list = new SelectList(this.items, this.maxVisible, selectListTheme);
|
|
64
74
|
this.list.onSelect = (item) => {
|
|
65
75
|
if (!isGroupHeader(item))
|
|
@@ -684,7 +694,8 @@ export async function runTui(options) {
|
|
|
684
694
|
// read-only agent narrows the run to reads (no approvals). The persisted reasoning effort is
|
|
685
695
|
// resolved against the ACTIVE model and sent when the model advertises supported levels.
|
|
686
696
|
const agent = currentAgent();
|
|
687
|
-
|
|
697
|
+
// An explicit /effort wins; otherwise the agent's own default effort (agentRunOptions) is kept.
|
|
698
|
+
const effort = app.config.agents.effort || !agent.effort ? currentEffort() : undefined;
|
|
688
699
|
return task((signal) => app.runner.run(session, prompt, signal, {
|
|
689
700
|
...agentRunOptions(agent),
|
|
690
701
|
...(effort ? { reasoningEffort: effort } : {}),
|
|
@@ -1160,11 +1171,20 @@ export async function runTui(options) {
|
|
|
1160
1171
|
const openAgentsPicker = () => {
|
|
1161
1172
|
const agents = mainAgents();
|
|
1162
1173
|
const current = currentAgent();
|
|
1163
|
-
|
|
1174
|
+
const rows = [...AGENT_PICKER_ACTIONS, ...agentPickerRows(agents, current.id, agentRoots())];
|
|
1175
|
+
showPicker(new Picker("Agents", rows, (item) => {
|
|
1176
|
+
if (item.value === "__agents:new") {
|
|
1177
|
+
closePicker();
|
|
1178
|
+
return void newAgentFlow().catch(error);
|
|
1179
|
+
}
|
|
1180
|
+
if (item.value === "__agents:manage") {
|
|
1181
|
+
closePicker();
|
|
1182
|
+
return void manageAgents().catch(error);
|
|
1183
|
+
}
|
|
1164
1184
|
const agent = agents.find((candidate) => candidate.id === item.value);
|
|
1165
1185
|
if (agent)
|
|
1166
1186
|
void applyActiveAgent(agent);
|
|
1167
|
-
}, closePicker, true, "
|
|
1187
|
+
}, closePicker, true, "Type to search by name or description. Selecting an agent activates it from the next prompt; build (default) returns to the default Alisio agent.", (items, filter) => filterAgentRows(items, filter)));
|
|
1168
1188
|
};
|
|
1169
1189
|
const applyActiveAgent = (agent) => {
|
|
1170
1190
|
void (async () => {
|
|
@@ -1200,6 +1220,424 @@ export async function runTui(options) {
|
|
|
1200
1220
|
}
|
|
1201
1221
|
})();
|
|
1202
1222
|
};
|
|
1223
|
+
// ---- Agent manager: project/global `.agents/agents` files (shared core service) ----------
|
|
1224
|
+
const agentRoots = () => ({ workspace: app.workspace, home: homedir() });
|
|
1225
|
+
let agentModels;
|
|
1226
|
+
/** Configured models with capabilities (cached per process; the running model always listed). */
|
|
1227
|
+
const loadAgentModels = async () => {
|
|
1228
|
+
if (agentModels)
|
|
1229
|
+
return agentModels;
|
|
1230
|
+
let options = [];
|
|
1231
|
+
try {
|
|
1232
|
+
options = agentModelOptions(await app.listAvailableModels(), {
|
|
1233
|
+
...(app.providerInfo?.profileName ? { profile: app.providerInfo.profileName } : {}),
|
|
1234
|
+
...(app.providerInfo?.id ? { provider: app.providerInfo.id } : {}),
|
|
1235
|
+
model: view.model,
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
/* configured catalogs unavailable: offer the running model only */
|
|
1240
|
+
}
|
|
1241
|
+
if (!options.some((option) => option.active)) {
|
|
1242
|
+
const capabilities = agentModelCapabilities(modelCatalog.get(view.model) ?? { id: view.model });
|
|
1243
|
+
options.unshift({
|
|
1244
|
+
reference: view.model,
|
|
1245
|
+
provider: app.provider.id,
|
|
1246
|
+
profile: app.providerInfo?.profileName ?? "",
|
|
1247
|
+
providerName: app.providers.get(activeProvider?.id ?? "")?.name ?? app.provider.id,
|
|
1248
|
+
id: view.model,
|
|
1249
|
+
active: true,
|
|
1250
|
+
capabilities,
|
|
1251
|
+
labels: agentModelLabels(capabilities),
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
agentModels = options;
|
|
1255
|
+
return options;
|
|
1256
|
+
};
|
|
1257
|
+
const capabilitiesOf = (models, reference) => models.find((m) => m.reference === reference || m.id === reference)?.capabilities ??
|
|
1258
|
+
agentModelCapabilities(undefined);
|
|
1259
|
+
const defaultAgentModel = (models) => models.find((m) => m.active)?.reference ?? view.model;
|
|
1260
|
+
/**
|
|
1261
|
+
* "Building with Alisio": one tool-less call of the ACTIVE model writes (or refines) the agent
|
|
1262
|
+
* with the authoring guidance (a discovered `create-agent` skill, else the bundled one). Runs as
|
|
1263
|
+
* a task, so Esc cancels it; failures are reported and leave the form untouched.
|
|
1264
|
+
*/
|
|
1265
|
+
const buildWithAlisio = async (description, base) => {
|
|
1266
|
+
let draft;
|
|
1267
|
+
flashHint("◐ Building with Alisio… writing the agent instructions · Esc to cancel", 180_000);
|
|
1268
|
+
await task(async (signal) => {
|
|
1269
|
+
draft = await generateAgentDraft(app.provider, description, {
|
|
1270
|
+
signal,
|
|
1271
|
+
guidance: await agentAuthoringGuidance(app.skills),
|
|
1272
|
+
...(base
|
|
1273
|
+
? {
|
|
1274
|
+
base: {
|
|
1275
|
+
name: base.name,
|
|
1276
|
+
description: base.description,
|
|
1277
|
+
instructions: base.instructions,
|
|
1278
|
+
},
|
|
1279
|
+
}
|
|
1280
|
+
: {}),
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
flashHint(draft
|
|
1284
|
+
? `Draft by ${draft.generatedBy} (${draft.guidance}). Review it, then save.`
|
|
1285
|
+
: "Building with Alisio stopped; nothing was changed.", 5000);
|
|
1286
|
+
return draft;
|
|
1287
|
+
};
|
|
1288
|
+
/** Saves a form; reports honestly whether the running registry already uses the change. */
|
|
1289
|
+
const saveAgentForm = async (form, models) => {
|
|
1290
|
+
const input = formToInput(form, capabilitiesOf(models, form.model));
|
|
1291
|
+
const change = form.id
|
|
1292
|
+
? await app.agentDefinitions.update(form.scope, form.id, input)
|
|
1293
|
+
: await app.agentDefinitions.create(form.scope, input);
|
|
1294
|
+
editor.setAutocompleteProvider(buildSlashCompletionProvider());
|
|
1295
|
+
notice(change.live
|
|
1296
|
+
? `Agent ${change.result.name} saved to ${change.result.path} and available now: /agent:${change.result.id}.`
|
|
1297
|
+
: `Agent ${change.result.name} saved to ${change.result.path}. Restart Alisio to use it${form.scope === "project" ? " (project agents also need a trusted workspace)" : ""}.`);
|
|
1298
|
+
return change;
|
|
1299
|
+
};
|
|
1300
|
+
/** Starts a fresh session with `id` as the active agent (the "Try it" action). */
|
|
1301
|
+
const tryAgentInNewSession = async (id) => {
|
|
1302
|
+
const agent = mainAgents().find((candidate) => candidate.id === id);
|
|
1303
|
+
if (!agent)
|
|
1304
|
+
return notice(`Agent ${id} is not loaded in this process; restart Alisio to try it.`);
|
|
1305
|
+
await endSession("clear");
|
|
1306
|
+
await app.updateSetting("agents.active", agent.id);
|
|
1307
|
+
let created;
|
|
1308
|
+
if (agent.model && agent.model !== view.model)
|
|
1309
|
+
created = await app.switchModel(agent.model).catch(() => undefined);
|
|
1310
|
+
if (created) {
|
|
1311
|
+
activeProvider = app.providerInfo;
|
|
1312
|
+
modelList = undefined;
|
|
1313
|
+
session = created.id;
|
|
1314
|
+
reset(initialViewState(created.model));
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
session = app.store.create(app.workspace, app.provider.id, view.model).id;
|
|
1318
|
+
reset(initialViewState(view.model));
|
|
1319
|
+
}
|
|
1320
|
+
notice(`New session ${session} with agent ${agent.name}. Type a prompt to try it.`);
|
|
1321
|
+
void app.herdr.report("idle", session);
|
|
1322
|
+
refreshEffort();
|
|
1323
|
+
refreshEstimate();
|
|
1324
|
+
};
|
|
1325
|
+
/** After the first save: try the agent in a new session (default) or keep editing. */
|
|
1326
|
+
const offerTryAgent = async (form, models, live) => {
|
|
1327
|
+
const choice = await askChoice(`Agent ${form.name} created`, live
|
|
1328
|
+
? [
|
|
1329
|
+
{ value: "try", label: "▶ Try it in a new session", description: "Default" },
|
|
1330
|
+
{ value: "stay", label: "Stay in editor" },
|
|
1331
|
+
]
|
|
1332
|
+
: [{ value: "stay", label: "Stay in editor", description: "Restart Alisio to try it" }]);
|
|
1333
|
+
if (choice === "try" && form.id)
|
|
1334
|
+
return tryAgentInNewSession(form.id);
|
|
1335
|
+
if (choice === "stay")
|
|
1336
|
+
return openAgentEditor(form, models, form);
|
|
1337
|
+
};
|
|
1338
|
+
/** The editor: one picker row per field; each edit returns to it. Esc closes. */
|
|
1339
|
+
const openAgentEditor = async (initial, models, saved) => {
|
|
1340
|
+
let form = initial;
|
|
1341
|
+
let baseline = saved ? JSON.stringify(saved) : undefined;
|
|
1342
|
+
const show = () => {
|
|
1343
|
+
const capabilities = capabilitiesOf(models, form.model);
|
|
1344
|
+
const dirty = baseline === undefined || JSON.stringify(form) !== baseline;
|
|
1345
|
+
showPicker(new Picker(form.id ? `Agents › ${form.name}` : "Agents › New agent", agentEditorRows(form, capabilities, {
|
|
1346
|
+
creating: !form.id,
|
|
1347
|
+
scopes: app.agentDefinitions.scopes,
|
|
1348
|
+
dirty,
|
|
1349
|
+
}), (item) => {
|
|
1350
|
+
closePicker();
|
|
1351
|
+
void edit(item.value, dirty).catch((cause) => {
|
|
1352
|
+
error(cause);
|
|
1353
|
+
show();
|
|
1354
|
+
});
|
|
1355
|
+
}, closePicker, false, `${form.scope === "project" ? "Project" : "Global"} scope · ${shortenPath(app.agentDefinitions.dir(form.scope), homedir())}`));
|
|
1356
|
+
};
|
|
1357
|
+
const input = (label, initialValue, placeholder, hint) => askInput({
|
|
1358
|
+
provider: "agent",
|
|
1359
|
+
title: form.id ? `Agents › ${form.name}` : "Agents › New agent",
|
|
1360
|
+
label,
|
|
1361
|
+
initial: initialValue,
|
|
1362
|
+
placeholder,
|
|
1363
|
+
...(hint ? { hint } : {}),
|
|
1364
|
+
step: 1,
|
|
1365
|
+
steps: 1,
|
|
1366
|
+
});
|
|
1367
|
+
const edit = async (field, dirty) => {
|
|
1368
|
+
const capabilities = capabilitiesOf(models, form.model);
|
|
1369
|
+
switch (field) {
|
|
1370
|
+
case "scope": {
|
|
1371
|
+
const scope = await askChoice("Scope", app.agentDefinitions.scopes.map((value) => ({
|
|
1372
|
+
value,
|
|
1373
|
+
label: value === "project" ? "Project" : "Global",
|
|
1374
|
+
description: app.agentDefinitions.dir(value),
|
|
1375
|
+
})));
|
|
1376
|
+
if (scope === "project" || scope === "global")
|
|
1377
|
+
form = { ...form, scope };
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
case "name": {
|
|
1381
|
+
const value = await input("Name", form.name, "New agent");
|
|
1382
|
+
if (value !== undefined)
|
|
1383
|
+
form = { ...form, name: value.trim() };
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
case "description": {
|
|
1387
|
+
const value = await input("Description", form.description, "One sentence: what this agent does");
|
|
1388
|
+
if (value !== undefined)
|
|
1389
|
+
form = { ...form, description: value.trim() };
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
case "instructions": {
|
|
1393
|
+
const value = await input("Instructions", encodeMultiline(form.instructions), "Describe desired model behavior (tone, tool usage, response style)", "Type \\n for a new line. ✦ Refine with Alisio writes them for you.");
|
|
1394
|
+
if (value !== undefined)
|
|
1395
|
+
form = { ...form, instructions: decodeMultiline(value) };
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
case "model": {
|
|
1399
|
+
const value = await askChoice("Model", models.map((m) => ({
|
|
1400
|
+
value: m.reference,
|
|
1401
|
+
label: `${m.name ?? m.id} · ${m.providerName}${m.active ? " (active)" : ""}${m.reference === form.model ? " ✓" : ""}`,
|
|
1402
|
+
description: m.labels.join(" · ") || "capabilities not declared by the provider",
|
|
1403
|
+
})));
|
|
1404
|
+
if (value) {
|
|
1405
|
+
const fitted = formToInput({ ...form, model: value }, capabilitiesOf(models, value));
|
|
1406
|
+
form = {
|
|
1407
|
+
...form,
|
|
1408
|
+
model: value,
|
|
1409
|
+
reasoning: { ...(fitted.reasoning ?? {}) },
|
|
1410
|
+
text: {
|
|
1411
|
+
...(fitted.text?.format ? { format: fitted.text.format.type } : {}),
|
|
1412
|
+
...(fitted.text?.verbosity ? { verbosity: fitted.text.verbosity } : {}),
|
|
1413
|
+
},
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
case "effort":
|
|
1419
|
+
case "summary":
|
|
1420
|
+
case "verbosity":
|
|
1421
|
+
case "format": {
|
|
1422
|
+
if (field !== "format" && !agentFieldSupported(field, capabilities)) {
|
|
1423
|
+
flashHint(`${form.model} does not support this setting.`);
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
const value = await askChoice(field, agentFieldChoices(field, capabilities));
|
|
1427
|
+
if (value === undefined)
|
|
1428
|
+
break;
|
|
1429
|
+
const clear = value === "!clear" ? undefined : value;
|
|
1430
|
+
if (field === "effort")
|
|
1431
|
+
form = { ...form, reasoning: { ...form.reasoning, effort: clear } };
|
|
1432
|
+
if (field === "summary")
|
|
1433
|
+
form = {
|
|
1434
|
+
...form,
|
|
1435
|
+
reasoning: { ...form.reasoning, summary: clear },
|
|
1436
|
+
};
|
|
1437
|
+
if (field === "verbosity")
|
|
1438
|
+
form = {
|
|
1439
|
+
...form,
|
|
1440
|
+
text: { ...form.text, verbosity: clear },
|
|
1441
|
+
};
|
|
1442
|
+
if (field === "format")
|
|
1443
|
+
form = {
|
|
1444
|
+
...form,
|
|
1445
|
+
text: { ...form.text, format: clear },
|
|
1446
|
+
};
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
case "refine": {
|
|
1450
|
+
const changes = await input("What should change? (optional)", "", "e.g. focus on security, answer in Spanish");
|
|
1451
|
+
if (changes === undefined)
|
|
1452
|
+
break;
|
|
1453
|
+
const draft = await buildWithAlisio(changes, form);
|
|
1454
|
+
if (draft)
|
|
1455
|
+
form = applyDraft(form, draft);
|
|
1456
|
+
break;
|
|
1457
|
+
}
|
|
1458
|
+
case "save": {
|
|
1459
|
+
const errors = formErrors(form);
|
|
1460
|
+
if (errors.length) {
|
|
1461
|
+
flashHint(errors.join("; "), 5000);
|
|
1462
|
+
break;
|
|
1463
|
+
}
|
|
1464
|
+
if (!dirty) {
|
|
1465
|
+
flashHint("No unsaved changes");
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
const creating = !form.id;
|
|
1469
|
+
const change = await saveAgentForm(form, models);
|
|
1470
|
+
form = { ...form, id: change.result.id, name: change.result.name };
|
|
1471
|
+
baseline = JSON.stringify(form);
|
|
1472
|
+
if (creating)
|
|
1473
|
+
return offerTryAgent(form, models, change.live);
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
case "cancel": {
|
|
1477
|
+
if (dirty) {
|
|
1478
|
+
const discard = await askChoice("Discard unsaved changes?", [
|
|
1479
|
+
{ value: "keep", label: "Keep editing" },
|
|
1480
|
+
{ value: "discard", label: "Discard and close" },
|
|
1481
|
+
]);
|
|
1482
|
+
if (discard === "discard")
|
|
1483
|
+
return;
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
show();
|
|
1490
|
+
};
|
|
1491
|
+
show();
|
|
1492
|
+
};
|
|
1493
|
+
/** `/agents new [description]`: assisted (with a description), template or blank. */
|
|
1494
|
+
const newAgentFlow = async (description) => {
|
|
1495
|
+
const models = await loadAgentModels();
|
|
1496
|
+
const blank = emptyAgentForm(app.agentDefinitions.defaultScope, defaultAgentModel(models));
|
|
1497
|
+
let mode = description ? "assisted" : undefined;
|
|
1498
|
+
if (!mode)
|
|
1499
|
+
mode = await askChoice("Agents › New agent", [
|
|
1500
|
+
{
|
|
1501
|
+
value: "assisted",
|
|
1502
|
+
label: "✦ Describe it — Alisio writes the agent",
|
|
1503
|
+
description: "The active model drafts the name, instructions and settings",
|
|
1504
|
+
},
|
|
1505
|
+
{ value: "template", label: "From a template…" },
|
|
1506
|
+
{ value: "blank", label: "Blank agent" },
|
|
1507
|
+
]);
|
|
1508
|
+
if (mode === "template")
|
|
1509
|
+
return chooseAgentTemplate();
|
|
1510
|
+
if (mode === "blank")
|
|
1511
|
+
return openAgentEditor(blank, models);
|
|
1512
|
+
if (mode !== "assisted")
|
|
1513
|
+
return;
|
|
1514
|
+
const text = description ??
|
|
1515
|
+
(await askInput({
|
|
1516
|
+
provider: "agent",
|
|
1517
|
+
title: "Agents › New agent",
|
|
1518
|
+
label: "Describe the agent you want",
|
|
1519
|
+
initial: "",
|
|
1520
|
+
placeholder: "e.g. reviews pull requests for security issues",
|
|
1521
|
+
step: 1,
|
|
1522
|
+
steps: 1,
|
|
1523
|
+
}));
|
|
1524
|
+
if (!text?.trim())
|
|
1525
|
+
return;
|
|
1526
|
+
const draft = await buildWithAlisio(text);
|
|
1527
|
+
if (!draft)
|
|
1528
|
+
return;
|
|
1529
|
+
const form = applyDraft(blank, draft);
|
|
1530
|
+
if (formErrors(form).length)
|
|
1531
|
+
return openAgentEditor(form, models);
|
|
1532
|
+
const change = await saveAgentForm(form, models);
|
|
1533
|
+
return offerTryAgent({ ...form, id: change.result.id }, models, change.live);
|
|
1534
|
+
};
|
|
1535
|
+
/** `/agents templates`: pick a template, then customize it (and optionally refine it). */
|
|
1536
|
+
const chooseAgentTemplate = async () => {
|
|
1537
|
+
const models = await loadAgentModels();
|
|
1538
|
+
showPicker(new Picker("Agents › Templates", AGENT_TEMPLATES.map((template) => ({
|
|
1539
|
+
value: template.id,
|
|
1540
|
+
label: template.name,
|
|
1541
|
+
description: template.description,
|
|
1542
|
+
})), (item) => {
|
|
1543
|
+
closePicker();
|
|
1544
|
+
const template = AGENT_TEMPLATES.find((candidate) => candidate.id === item.value);
|
|
1545
|
+
if (template)
|
|
1546
|
+
void openAgentEditor(formFromTemplate(template, app.agentDefinitions.defaultScope, defaultAgentModel(models)), models).catch(error);
|
|
1547
|
+
}, closePicker, true, "Type to search. The template opens in the editor; ✦ Refine with Alisio adapts it to your changes.", (items, filter) => filterAgentRows(items, filter)));
|
|
1548
|
+
};
|
|
1549
|
+
/** `/agents manage`: saved project/global agents → edit, activate, try or delete. */
|
|
1550
|
+
const manageAgents = async () => {
|
|
1551
|
+
const list = await app.agentDefinitions.list();
|
|
1552
|
+
if (!list.length)
|
|
1553
|
+
return notice("No saved agents yet. Create one with /agents new.");
|
|
1554
|
+
showPicker(new Picker("Agents › Saved agents", list.map((a) => ({
|
|
1555
|
+
value: `${a.scope}:${a.id}`,
|
|
1556
|
+
label: `${a.name} [${a.scope === "project" ? "Project" : "Global"}]${a.overridesGlobal ? " overrides global" : ""}${a.overriddenByProject ? " (overridden by project)" : ""}`,
|
|
1557
|
+
description: [a.description, a.model].filter(Boolean).join(" · "),
|
|
1558
|
+
})), (item) => {
|
|
1559
|
+
closePicker();
|
|
1560
|
+
const found = list.find((a) => `${a.scope}:${a.id}` === item.value);
|
|
1561
|
+
if (found)
|
|
1562
|
+
void agentActions(found).catch(error);
|
|
1563
|
+
}, closePicker, true, `Project: ${app.agentDefinitions.scopes.includes("project") ? app.agentDefinitions.dir("project") : "—"} · Global: ${app.agentDefinitions.dir("global")}`, (items, filter) => filterAgentRows(items, filter)));
|
|
1564
|
+
};
|
|
1565
|
+
const agentActions = async (definition) => {
|
|
1566
|
+
const action = await askChoice(`${definition.name} [${definition.scope}]`, [
|
|
1567
|
+
{ value: "edit", label: "Edit" },
|
|
1568
|
+
{ value: "activate", label: "Activate in this session", description: "From the next prompt" },
|
|
1569
|
+
{ value: "try", label: "Try it in a new session" },
|
|
1570
|
+
{ value: "delete", label: "Delete", description: definition.path },
|
|
1571
|
+
]);
|
|
1572
|
+
if (action === "edit") {
|
|
1573
|
+
const form = formFromDefinition(definition);
|
|
1574
|
+
return openAgentEditor(form, await loadAgentModels(), form);
|
|
1575
|
+
}
|
|
1576
|
+
if (action === "try")
|
|
1577
|
+
return tryAgentInNewSession(definition.id);
|
|
1578
|
+
if (action === "activate") {
|
|
1579
|
+
const agent = mainAgents().find((candidate) => candidate.id === definition.id);
|
|
1580
|
+
if (!agent)
|
|
1581
|
+
return notice(`Agent ${definition.id} is not loaded; restart Alisio to use it.`);
|
|
1582
|
+
return applyActiveAgent(agent);
|
|
1583
|
+
}
|
|
1584
|
+
if (action === "delete") {
|
|
1585
|
+
const confirm = await askChoice(`Delete ${definition.path}?`, [
|
|
1586
|
+
{ value: "no", label: "Keep it" },
|
|
1587
|
+
{ value: "yes", label: "Delete the file" },
|
|
1588
|
+
]);
|
|
1589
|
+
if (confirm !== "yes")
|
|
1590
|
+
return;
|
|
1591
|
+
const change = await app.agentDefinitions.delete(definition.scope, definition.id);
|
|
1592
|
+
editor.setAutocompleteProvider(buildSlashCompletionProvider());
|
|
1593
|
+
notice(change.live
|
|
1594
|
+
? `Deleted agent ${definition.name}; it is no longer offered.`
|
|
1595
|
+
: `Deleted ${definition.path}. Restart Alisio to drop it from the running agents.`);
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
/** Routes `/agents [verb]`: TUI manager verbs, else the subagents plugin's verbs. */
|
|
1599
|
+
const agentsCommand = async (args, raw) => {
|
|
1600
|
+
const command = parseAgentsArgs(args);
|
|
1601
|
+
if (command.kind === "plugin") {
|
|
1602
|
+
const handler = app.plugins.commands.get("agents");
|
|
1603
|
+
if (!handler)
|
|
1604
|
+
return error("Subagent task management needs the Subagents plugin.");
|
|
1605
|
+
const output = await handler(command.args, { sessionId: session });
|
|
1606
|
+
if (/^reload\b/.test(command.args))
|
|
1607
|
+
editor.setAutocompleteProvider(buildSlashCompletionProvider());
|
|
1608
|
+
return info(output);
|
|
1609
|
+
}
|
|
1610
|
+
if (busy) {
|
|
1611
|
+
editor.setText(raw);
|
|
1612
|
+
flashHint("A turn is running: wait for it to finish before managing agents");
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
switch (command.kind) {
|
|
1616
|
+
case "picker":
|
|
1617
|
+
// Pick up files edited outside Alisio before listing.
|
|
1618
|
+
await app.agentDefinitions.reload();
|
|
1619
|
+
return openAgentsPicker();
|
|
1620
|
+
case "new":
|
|
1621
|
+
return newAgentFlow(command.description);
|
|
1622
|
+
case "templates":
|
|
1623
|
+
return chooseAgentTemplate();
|
|
1624
|
+
case "manage":
|
|
1625
|
+
return manageAgents();
|
|
1626
|
+
case "edit":
|
|
1627
|
+
case "delete": {
|
|
1628
|
+
const list = await app.agentDefinitions.list();
|
|
1629
|
+
const found = list.find((a) => a.id === command.id && (!command.scope || a.scope === command.scope)) ??
|
|
1630
|
+
undefined;
|
|
1631
|
+
if (!found)
|
|
1632
|
+
return error(`No saved agent ${command.id}. See /agents manage.`);
|
|
1633
|
+
if (command.kind === "edit") {
|
|
1634
|
+
const form = formFromDefinition(found);
|
|
1635
|
+
return openAgentEditor(form, await loadAgentModels(), form);
|
|
1636
|
+
}
|
|
1637
|
+
return agentActions(found);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1203
1641
|
/**
|
|
1204
1642
|
* `/effort [level]`: without an argument, a picker over the ACTIVE model's advertised effort
|
|
1205
1643
|
* levels (marking the model default); with an argument, the level is validated against
|
|
@@ -1537,6 +1975,19 @@ export async function runTui(options) {
|
|
|
1537
1975
|
return error(`Unknown plugin command: ${command ?? ""}`);
|
|
1538
1976
|
return info(await handler(rest.join(" ")));
|
|
1539
1977
|
}
|
|
1978
|
+
const agentId = agentIdFromCommand(parsed.name);
|
|
1979
|
+
if (agentId) {
|
|
1980
|
+
// `/agent:<id>`: activates a loaded agent from the next prompt (as the /agents picker).
|
|
1981
|
+
if (busy) {
|
|
1982
|
+
editor.setText(raw);
|
|
1983
|
+
flashHint("A turn is running: wait for it to finish before switching the active agent");
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const agent = mainAgents().find((candidate) => candidate.id === agentId);
|
|
1987
|
+
if (!agent)
|
|
1988
|
+
return error(`Unknown agent ${agentId}. Open /agents to see the loaded agents.`);
|
|
1989
|
+
return applyActiveAgent(agent);
|
|
1990
|
+
}
|
|
1540
1991
|
switch (name) {
|
|
1541
1992
|
case "help":
|
|
1542
1993
|
return info(helpReport());
|
|
@@ -1586,23 +2037,11 @@ export async function runTui(options) {
|
|
|
1586
2037
|
`(reuse the user's own wording for the question text). Do not answer in plain text ` +
|
|
1587
2038
|
`first; call the tool right away.`);
|
|
1588
2039
|
}
|
|
1589
|
-
case "agents":
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
|
|
1594
|
-
if (parsed.args) {
|
|
1595
|
-
const handler = app.plugins.commands.get("agents");
|
|
1596
|
-
if (handler)
|
|
1597
|
-
return info(await handler(parsed.args, { sessionId: session }));
|
|
1598
|
-
}
|
|
1599
|
-
if (busy) {
|
|
1600
|
-
editor.setText(raw);
|
|
1601
|
-
flashHint("A turn is running: wait for it to finish before switching the active agent");
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
return openAgentsPicker();
|
|
1605
|
-
}
|
|
2040
|
+
case "agents":
|
|
2041
|
+
// TUI manager verbs (picker, new, templates, manage, edit, delete); other verbs go to the
|
|
2042
|
+
// subagents plugin's own `/agents` command (list/open/cancel/kill/resume/merge/discard/
|
|
2043
|
+
// defs/reload), still available while a turn runs.
|
|
2044
|
+
return await agentsCommand(parsed.args, raw);
|
|
1606
2045
|
case "effort":
|
|
1607
2046
|
if (busy) {
|
|
1608
2047
|
editor.setText(raw);
|
|
@@ -1666,6 +2105,11 @@ export async function runTui(options) {
|
|
|
1666
2105
|
description: c.description ?? `plugin ${c.plugin}`,
|
|
1667
2106
|
...(c.argumentHint ? { argumentHint: c.argumentHint } : {}),
|
|
1668
2107
|
})),
|
|
2108
|
+
// `/agent:<id>` for every loaded agent (namespaced: never collides with other commands).
|
|
2109
|
+
...agentCommandDescriptors(mainAgents()).map((d) => ({
|
|
2110
|
+
name: d.name,
|
|
2111
|
+
description: d.description,
|
|
2112
|
+
})),
|
|
1669
2113
|
];
|
|
1670
2114
|
return new CombinedAutocompleteProvider(slashCompletionCommands(sources, {
|
|
1671
2115
|
sessions: (prefix) => workspaceSessions()
|
|
@@ -26,6 +26,8 @@ export declare class ConnectInputPrompt implements Component {
|
|
|
26
26
|
hint?: string;
|
|
27
27
|
step: number;
|
|
28
28
|
steps: number;
|
|
29
|
+
/** Dialog title (default `Connect · <provider>`). */
|
|
30
|
+
title?: string;
|
|
29
31
|
onSubmit: (value: string) => void;
|
|
30
32
|
onCancel: () => void;
|
|
31
33
|
});
|
|
@@ -92,7 +92,7 @@ export class ConnectInputPrompt {
|
|
|
92
92
|
const rule = "─".repeat(Math.max(1, Math.min(56, width - 2)));
|
|
93
93
|
return [
|
|
94
94
|
truncateToWidth(style.cyan(`┌${rule}┐`), width),
|
|
95
|
-
truncateToWidth(`│ ${style.bold(`Connect · ${this.options.provider}`)} ${style.dim(`(${this.options.step}/${this.options.steps})`)}`, width),
|
|
95
|
+
truncateToWidth(`│ ${style.bold(this.options.title ?? `Connect · ${this.options.provider}`)}${this.options.title && this.options.steps <= 1 ? "" : ` ${style.dim(`(${this.options.step}/${this.options.steps})`)}`}`, width),
|
|
96
96
|
truncateToWidth(`│ ${style.brightCyan("❯")} ${style.bold(this.options.label)}`, width),
|
|
97
97
|
truncateToWidth(`│ ${field}`, width),
|
|
98
98
|
...(this.options.hint ? [truncateToWidth(`│ ${style.dim(this.options.hint)}`, width)] : []),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/alisio-code",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.25",
|
|
4
4
|
"description": "Alisio: an extensible, provider-agnostic coding-agent harness for your terminal. TUI, OpenAI-compatible providers, permissioned local tools, context compaction, persistent memory and a typed plugin SDK.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"alisio": "./dist/main.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@alisio/core": "0.1.0-alpha.
|
|
48
|
+
"@alisio/core": "0.1.0-alpha.21",
|
|
49
49
|
"@alisio/plugin-memory": "0.1.0-alpha.9",
|
|
50
50
|
"@alisio/plugin-openai-compatible": "0.1.0-alpha.9",
|
|
51
|
-
"@alisio/plugin-subagents": "0.1.0-alpha.
|
|
52
|
-
"@alisio/sdk": "0.1.0-alpha.
|
|
53
|
-
"@alisio/server": "0.1.0-alpha.
|
|
51
|
+
"@alisio/plugin-subagents": "0.1.0-alpha.11",
|
|
52
|
+
"@alisio/sdk": "0.1.0-alpha.17",
|
|
53
|
+
"@alisio/server": "0.1.0-alpha.5",
|
|
54
54
|
"@earendil-works/pi-tui": "0.87.1",
|
|
55
55
|
"commander": "15.0.0"
|
|
56
56
|
},
|