@pi-kaush/pi-agent-mode 0.1.0 → 0.1.1
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/package.json +6 -2
- package/src/agent-discovery.ts +40 -31
- package/src/agent-picker.ts +85 -0
- package/src/index.ts +101 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-kaush/pi-agent-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Activate a configured Pi agent as a persistent mode in the current session.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kaushik Gopal",
|
|
@@ -34,11 +34,15 @@
|
|
|
34
34
|
]
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@earendil-works/pi-coding-agent": ">=0.80.6"
|
|
37
|
+
"@earendil-works/pi-coding-agent": ">=0.80.6",
|
|
38
|
+
"@earendil-works/pi-tui": ">=0.80.6"
|
|
38
39
|
},
|
|
39
40
|
"peerDependenciesMeta": {
|
|
40
41
|
"@earendil-works/pi-coding-agent": {
|
|
41
42
|
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"@earendil-works/pi-tui": {
|
|
45
|
+
"optional": true
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
"engines": {
|
package/src/agent-discovery.ts
CHANGED
|
@@ -82,31 +82,46 @@ function loadAgentsFromDir(
|
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
try {
|
|
86
|
+
const { frontmatter, body } = parseFrontmatter<AgentFrontmatter>(content);
|
|
87
|
+
const name =
|
|
88
|
+
typeof frontmatter.name === "string" ? frontmatter.name : undefined;
|
|
89
|
+
const description =
|
|
90
|
+
typeof frontmatter.description === "string"
|
|
91
|
+
? frontmatter.description
|
|
92
|
+
: undefined;
|
|
93
|
+
if (!name || !description) continue;
|
|
94
|
+
|
|
95
|
+
const tools =
|
|
96
|
+
typeof frontmatter.tools === "string"
|
|
97
|
+
? frontmatter.tools
|
|
98
|
+
.split(",")
|
|
99
|
+
.map((tool) => tool.trim())
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
: undefined;
|
|
102
|
+
const emoji =
|
|
103
|
+
typeof frontmatter.emoji === "string"
|
|
104
|
+
? frontmatter.emoji.trim()
|
|
105
|
+
: undefined;
|
|
106
|
+
const model =
|
|
107
|
+
typeof frontmatter.model === "string" ? frontmatter.model : undefined;
|
|
108
|
+
|
|
109
|
+
agents.push({
|
|
110
|
+
name,
|
|
111
|
+
description,
|
|
112
|
+
...(emoji ? { emoji } : {}),
|
|
113
|
+
...(tools && tools.length > 0 ? { tools } : {}),
|
|
114
|
+
...(model ? { model } : {}),
|
|
115
|
+
...(typeof frontmatter.confirmProjectAgents === "boolean"
|
|
116
|
+
? { confirmProjectAgents: frontmatter.confirmProjectAgents }
|
|
117
|
+
: {}),
|
|
118
|
+
systemPrompt: body,
|
|
119
|
+
source,
|
|
120
|
+
filePath,
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
88
123
|
continue;
|
|
89
124
|
}
|
|
90
|
-
|
|
91
|
-
const tools = frontmatter.tools
|
|
92
|
-
?.split(",")
|
|
93
|
-
.map((t: string) => t.trim())
|
|
94
|
-
.filter(Boolean);
|
|
95
|
-
|
|
96
|
-
const emoji = frontmatter.emoji?.trim();
|
|
97
|
-
agents.push({
|
|
98
|
-
name: frontmatter.name,
|
|
99
|
-
description: frontmatter.description,
|
|
100
|
-
...(emoji ? { emoji } : {}),
|
|
101
|
-
...(tools && tools.length > 0 ? { tools } : {}),
|
|
102
|
-
...(frontmatter.model ? { model: frontmatter.model } : {}),
|
|
103
|
-
...(typeof frontmatter.confirmProjectAgents === "boolean"
|
|
104
|
-
? { confirmProjectAgents: frontmatter.confirmProjectAgents }
|
|
105
|
-
: {}),
|
|
106
|
-
systemPrompt: body,
|
|
107
|
-
source,
|
|
108
|
-
filePath,
|
|
109
|
-
});
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
return agents;
|
|
@@ -148,14 +163,8 @@ export function discoverAgents(
|
|
|
148
163
|
|
|
149
164
|
const agentMap = new Map<string, AgentConfig>();
|
|
150
165
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
154
|
-
} else if (scope === "user") {
|
|
155
|
-
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
156
|
-
} else {
|
|
157
|
-
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
158
|
-
}
|
|
166
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
167
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
159
168
|
|
|
160
169
|
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
161
170
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI picker for `/agent`: SelectList with per-item descriptions, theme
|
|
3
|
+
* colors, agent emoji kept in the label, and a ● marker on the active agent.
|
|
4
|
+
* Non-TUI modes fall back to plain ctx.ui.select in the command handler.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DynamicBorder,
|
|
9
|
+
type ExtensionCommandContext,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import {
|
|
12
|
+
Container,
|
|
13
|
+
SelectList,
|
|
14
|
+
Text,
|
|
15
|
+
type SelectItem,
|
|
16
|
+
} from "@earendil-works/pi-tui";
|
|
17
|
+
import { formatAgentDisplayName, type AgentConfig } from "./agent-discovery.ts";
|
|
18
|
+
|
|
19
|
+
export const NONE_VALUE = "none";
|
|
20
|
+
|
|
21
|
+
export function buildAgentPickerItems(
|
|
22
|
+
agents: AgentConfig[],
|
|
23
|
+
activeName: string | undefined,
|
|
24
|
+
): SelectItem[] {
|
|
25
|
+
const items: SelectItem[] = agents.map((agent) => {
|
|
26
|
+
const marker = agent.name === activeName ? "● " : "";
|
|
27
|
+
const origin = agent.source === "project" ? "Project agent. " : "";
|
|
28
|
+
return {
|
|
29
|
+
value: agent.name,
|
|
30
|
+
label: `${marker}${formatAgentDisplayName(agent)}`,
|
|
31
|
+
description: `${origin}${agent.description}`,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
if (activeName) {
|
|
35
|
+
items.push({
|
|
36
|
+
value: NONE_VALUE,
|
|
37
|
+
label: "None",
|
|
38
|
+
description: "Restore the default model, thinking level, and tools",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return items;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Resolves with the picked agent name, NONE_VALUE, or null when cancelled. */
|
|
45
|
+
export async function showAgentPicker(
|
|
46
|
+
ctx: ExtensionCommandContext,
|
|
47
|
+
items: SelectItem[],
|
|
48
|
+
): Promise<string | null> {
|
|
49
|
+
return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
50
|
+
const container = new Container();
|
|
51
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
52
|
+
container.addChild(
|
|
53
|
+
new Text(theme.fg("accent", theme.bold("Activate agent")), 1, 0),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const list = new SelectList(items, Math.min(items.length, 10), {
|
|
57
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
58
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
59
|
+
description: (t: string) => theme.fg("muted", t),
|
|
60
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
61
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
62
|
+
});
|
|
63
|
+
list.onSelect = (item: SelectItem) => done(item.value);
|
|
64
|
+
list.onCancel = () => done(null);
|
|
65
|
+
container.addChild(list);
|
|
66
|
+
|
|
67
|
+
container.addChild(
|
|
68
|
+
new Text(
|
|
69
|
+
theme.fg("dim", "↑↓ navigate • enter select • esc cancel ● active"),
|
|
70
|
+
1,
|
|
71
|
+
0,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
render: (width: number) => container.render(width),
|
|
78
|
+
invalidate: () => container.invalidate(),
|
|
79
|
+
handleInput: (data: string) => {
|
|
80
|
+
list.handleInput(data);
|
|
81
|
+
tui.requestRender();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,18 +21,19 @@ import {
|
|
|
21
21
|
type AgentConfig,
|
|
22
22
|
} from "./agent-discovery.ts";
|
|
23
23
|
import { parseAgentModelSpec, type ThinkingLevel } from "./model-spec.ts";
|
|
24
|
+
import {
|
|
25
|
+
buildAgentPickerItems,
|
|
26
|
+
NONE_VALUE,
|
|
27
|
+
showAgentPicker,
|
|
28
|
+
} from "./agent-picker.ts";
|
|
24
29
|
|
|
25
30
|
interface ModelReference {
|
|
26
31
|
provider: string;
|
|
27
32
|
id: string;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export interface AgentModelSpec {
|
|
31
|
-
model: string;
|
|
32
|
-
thinkingLevel?: ThinkingLevel;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
35
|
export { parseAgentModelSpec };
|
|
36
|
+
export type { AgentModelSpec } from "./model-spec.ts";
|
|
36
37
|
|
|
37
38
|
// Durable session state is namespaced so it cannot collide with other
|
|
38
39
|
// packages. Legacy Aikado sessions wrote `active-agent-state`; restore reads
|
|
@@ -62,6 +63,42 @@ interface ActiveAgentState {
|
|
|
62
63
|
baseline?: AgentBaseline;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
67
|
+
if (typeof value !== "object" || value === null) return false;
|
|
68
|
+
const prototype = Object.getPrototypeOf(value);
|
|
69
|
+
return prototype === Object.prototype || prototype === null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isActiveAgentState(value: unknown): value is ActiveAgentState {
|
|
73
|
+
if (!isPlainObject(value) || typeof value.active !== "boolean") return false;
|
|
74
|
+
if (
|
|
75
|
+
value.active &&
|
|
76
|
+
(typeof value.name !== "string" || value.name.length === 0)
|
|
77
|
+
)
|
|
78
|
+
return false;
|
|
79
|
+
|
|
80
|
+
if (value.baseline !== undefined) {
|
|
81
|
+
if (!isPlainObject(value.baseline)) return false;
|
|
82
|
+
if (typeof value.baseline.thinkingLevel !== "string") return false;
|
|
83
|
+
if (
|
|
84
|
+
!Array.isArray(value.baseline.tools) ||
|
|
85
|
+
!value.baseline.tools.every((tool) => typeof tool === "string")
|
|
86
|
+
)
|
|
87
|
+
return false;
|
|
88
|
+
|
|
89
|
+
if (value.baseline.model !== undefined) {
|
|
90
|
+
if (
|
|
91
|
+
!isPlainObject(value.baseline.model) ||
|
|
92
|
+
typeof value.baseline.model.provider !== "string" ||
|
|
93
|
+
typeof value.baseline.model.id !== "string"
|
|
94
|
+
)
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
65
102
|
export function registerAgentMode(pi: ExtensionAPI) {
|
|
66
103
|
let activeAgent: ActiveAgent | undefined;
|
|
67
104
|
|
|
@@ -192,8 +229,12 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
192
229
|
return false;
|
|
193
230
|
}
|
|
194
231
|
thinkingLevel = modelSpec.thinkingLevel ?? baseline.thinkingLevel;
|
|
195
|
-
} else {
|
|
196
|
-
|
|
232
|
+
} else if (!(await restoreModel(baseline.model, ctx))) {
|
|
233
|
+
ctx.ui.notify(
|
|
234
|
+
`Agent ${formatAgentDisplayName(agent)}: could not restore the baseline model; activation aborted.`,
|
|
235
|
+
"error",
|
|
236
|
+
);
|
|
237
|
+
return false;
|
|
197
238
|
}
|
|
198
239
|
|
|
199
240
|
pi.setThinkingLevel(thinkingLevel);
|
|
@@ -210,15 +251,17 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
210
251
|
return;
|
|
211
252
|
}
|
|
212
253
|
|
|
213
|
-
await restoreModel(activeAgent.baseline.model, ctx);
|
|
254
|
+
const modelRestored = await restoreModel(activeAgent.baseline.model, ctx);
|
|
214
255
|
pi.setThinkingLevel(activeAgent.baseline.thinkingLevel);
|
|
215
256
|
pi.setActiveTools(activeAgent.baseline.tools);
|
|
216
257
|
activeAgent = undefined;
|
|
217
258
|
updateAgentStatus(ctx);
|
|
218
259
|
persistActiveAgent();
|
|
219
260
|
ctx.ui.notify(
|
|
220
|
-
|
|
221
|
-
|
|
261
|
+
modelRestored
|
|
262
|
+
? "Active agent cleared; previous model and tools restored."
|
|
263
|
+
: "Active agent cleared, but the previous model could not be restored.",
|
|
264
|
+
modelRestored ? "info" : "warning",
|
|
222
265
|
);
|
|
223
266
|
}
|
|
224
267
|
|
|
@@ -272,13 +315,52 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
272
315
|
);
|
|
273
316
|
return;
|
|
274
317
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
318
|
+
let picked: string | null;
|
|
319
|
+
if (ctx.mode === "tui") {
|
|
320
|
+
picked = await showAgentPicker(
|
|
321
|
+
ctx,
|
|
322
|
+
buildAgentPickerItems(agents, activeAgent?.agent.name),
|
|
323
|
+
);
|
|
324
|
+
} else {
|
|
325
|
+
const items = buildAgentPickerItems(agents, activeAgent?.agent.name);
|
|
326
|
+
const baseLabels = items.map(
|
|
327
|
+
(item) => `${item.label} — ${item.description}`,
|
|
328
|
+
);
|
|
329
|
+
const labelCounts = new Map<string, number>();
|
|
330
|
+
for (const label of baseLabels) {
|
|
331
|
+
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
|
332
|
+
}
|
|
333
|
+
const labels = baseLabels.map((label, index) =>
|
|
334
|
+
labelCounts.get(label)! > 1
|
|
335
|
+
? `${label} [${items[index]!.value}]`
|
|
336
|
+
: label,
|
|
337
|
+
);
|
|
338
|
+
const choice = await ctx.ui.select("Activate agent:", labels);
|
|
339
|
+
if (choice === undefined) {
|
|
340
|
+
picked = null;
|
|
341
|
+
} else {
|
|
342
|
+
const firstMatch = labels.indexOf(choice);
|
|
343
|
+
if (
|
|
344
|
+
firstMatch !== -1 &&
|
|
345
|
+
labels.lastIndexOf(choice) !== firstMatch
|
|
346
|
+
) {
|
|
347
|
+
ctx.ui.notify(
|
|
348
|
+
"That selection matches more than one agent; rename one of the duplicates.",
|
|
349
|
+
"warning",
|
|
350
|
+
);
|
|
351
|
+
picked = null;
|
|
352
|
+
} else {
|
|
353
|
+
picked =
|
|
354
|
+
firstMatch === -1 ? null : (items[firstMatch]?.value ?? null);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (!picked) return;
|
|
359
|
+
if (picked === NONE_VALUE) {
|
|
360
|
+
await clearActiveAgent(ctx);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
agent = agents.find((candidate) => candidate.name === picked);
|
|
282
364
|
}
|
|
283
365
|
|
|
284
366
|
if (!agent || !(await confirmProjectAgent(agent, ctx))) return;
|
|
@@ -302,7 +384,8 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
302
384
|
.filter(
|
|
303
385
|
(entry): entry is CustomEntry<ActiveAgentState> =>
|
|
304
386
|
entry.type === "custom" &&
|
|
305
|
-
ACTIVE_AGENT_STATE_TYPES.includes(entry.customType)
|
|
387
|
+
ACTIVE_AGENT_STATE_TYPES.includes(entry.customType) &&
|
|
388
|
+
isActiveAgentState(entry.data),
|
|
306
389
|
)
|
|
307
390
|
.pop();
|
|
308
391
|
const state = stateEntry?.data;
|