@pi-kaush/pi-agent-mode 0.1.0 → 0.2.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 +6 -0
- package/README.md +19 -4
- package/package.json +9 -2
- package/src/agent-discovery.ts +47 -31
- package/src/agent-picker.ts +85 -0
- package/src/index.ts +186 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Add `profile` frontmatter: activate a named model profile from the shared machine-local `~/.pi/agent/profiles.yaml` (`@pi-kaush/pi-model-profiles`), walking candidates in order and applying the first available model and thinking level.
|
|
6
|
+
- Reject agents declaring both `profile` and `model`; precedence is explicit: profile first, then model, then the session default.
|
|
7
|
+
- Load `@pi-kaush/pi-model-profiles` lazily so a missing library degrades only profile support; `/agent` remains fully functional without it.
|
|
8
|
+
|
|
3
9
|
## 0.1.0
|
|
4
10
|
|
|
5
11
|
- Add the `/agent` command for activating a configured Pi agent as a persistent mode in the current session.
|
package/README.md
CHANGED
|
@@ -5,14 +5,13 @@ the session to the agent's model, thinking level, and tool set, appends the agen
|
|
|
5
5
|
prompt to Pi's base instructions, and restores the pre-activation baseline when cleared.
|
|
6
6
|
It does not spawn children and provides no delegated subagents.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
### Install
|
|
9
9
|
|
|
10
|
-
```
|
|
10
|
+
```fish
|
|
11
11
|
pi install npm:@pi-kaush/pi-agent-mode
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Restart Pi or run `/reload`.
|
|
15
|
-
`@0.1.0`.
|
|
14
|
+
Restart Pi or run `/reload`.
|
|
16
15
|
|
|
17
16
|
## Usage
|
|
18
17
|
|
|
@@ -37,11 +36,27 @@ description: ... # required
|
|
|
37
36
|
emoji: 🔍 # optional, shown in the footer status
|
|
38
37
|
tools: read, grep # optional allowlist; unavailable tools are reported and omitted
|
|
39
38
|
model: provider/model:high # optional; thinking suffix may be :off/:low/:medium/:high/:max
|
|
39
|
+
profile: coder # optional; named model candidates from ~/.pi/agent/profiles.yaml
|
|
40
40
|
confirmProjectAgents: false # project agents default to requiring confirmation
|
|
41
41
|
---
|
|
42
42
|
Prompt body appended to Pi's base instructions while the agent is active.
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Model selection precedence
|
|
46
|
+
|
|
47
|
+
Agent frontmatter may declare a `model` (exact model, optional thinking
|
|
48
|
+
suffix) or a `profile` — a named entry in the shared machine-local
|
|
49
|
+
`~/.pi/agent/profiles.yaml` maintained by
|
|
50
|
+
[@pi-kaush/pi-model-profiles](../pi-model-profiles). An agent must not declare
|
|
51
|
+
both. Profile candidates are walked in order at activation and the first
|
|
52
|
+
model that exists and is authenticated wins; once the session is running,
|
|
53
|
+
models never switch mid-request. Agents without either keep the current
|
|
54
|
+
session model.
|
|
55
|
+
|
|
56
|
+
`pi-agent-mode` loads `@pi-kaush/pi-model-profiles` lazily: if the library is
|
|
57
|
+
missing, only agents that declare `profile:` fail (with an install hint) and
|
|
58
|
+
every other `/agent` capability is unencumbered.
|
|
59
|
+
|
|
45
60
|
## Behavior
|
|
46
61
|
|
|
47
62
|
- The first activation captures the current model, thinking level, and active
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-kaush/pi-agent-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -33,12 +33,19 @@
|
|
|
33
33
|
"./src/index.ts"
|
|
34
34
|
]
|
|
35
35
|
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@pi-kaush/pi-model-profiles": "^0.1.0"
|
|
38
|
+
},
|
|
36
39
|
"peerDependencies": {
|
|
37
|
-
"@earendil-works/pi-coding-agent": ">=0.80.6"
|
|
40
|
+
"@earendil-works/pi-coding-agent": ">=0.80.6",
|
|
41
|
+
"@earendil-works/pi-tui": ">=0.80.6"
|
|
38
42
|
},
|
|
39
43
|
"peerDependenciesMeta": {
|
|
40
44
|
"@earendil-works/pi-coding-agent": {
|
|
41
45
|
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@earendil-works/pi-tui": {
|
|
48
|
+
"optional": true
|
|
42
49
|
}
|
|
43
50
|
},
|
|
44
51
|
"engines": {
|
package/src/agent-discovery.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface AgentConfig {
|
|
|
23
23
|
emoji?: string;
|
|
24
24
|
tools?: string[];
|
|
25
25
|
model?: string;
|
|
26
|
+
profile?: string;
|
|
26
27
|
confirmProjectAgents?: boolean;
|
|
27
28
|
systemPrompt: string;
|
|
28
29
|
source: "user" | "project";
|
|
@@ -35,6 +36,7 @@ interface AgentFrontmatter extends Record<string, unknown> {
|
|
|
35
36
|
emoji?: string;
|
|
36
37
|
tools?: string;
|
|
37
38
|
model?: string;
|
|
39
|
+
profile?: string;
|
|
38
40
|
confirmProjectAgents?: boolean;
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -82,31 +84,51 @@ function loadAgentsFromDir(
|
|
|
82
84
|
continue;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
try {
|
|
88
|
+
const { frontmatter, body } = parseFrontmatter<AgentFrontmatter>(content);
|
|
89
|
+
const name =
|
|
90
|
+
typeof frontmatter.name === "string" ? frontmatter.name : undefined;
|
|
91
|
+
const description =
|
|
92
|
+
typeof frontmatter.description === "string"
|
|
93
|
+
? frontmatter.description
|
|
94
|
+
: undefined;
|
|
95
|
+
if (!name || !description) continue;
|
|
96
|
+
|
|
97
|
+
const tools =
|
|
98
|
+
typeof frontmatter.tools === "string"
|
|
99
|
+
? frontmatter.tools
|
|
100
|
+
.split(",")
|
|
101
|
+
.map((tool) => tool.trim())
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
: undefined;
|
|
104
|
+
const emoji =
|
|
105
|
+
typeof frontmatter.emoji === "string"
|
|
106
|
+
? frontmatter.emoji.trim()
|
|
107
|
+
: undefined;
|
|
108
|
+
const model =
|
|
109
|
+
typeof frontmatter.model === "string" ? frontmatter.model : undefined;
|
|
110
|
+
const profile =
|
|
111
|
+
typeof frontmatter.profile === "string"
|
|
112
|
+
? frontmatter.profile.trim()
|
|
113
|
+
: undefined;
|
|
114
|
+
|
|
115
|
+
agents.push({
|
|
116
|
+
name,
|
|
117
|
+
description,
|
|
118
|
+
...(emoji ? { emoji } : {}),
|
|
119
|
+
...(tools && tools.length > 0 ? { tools } : {}),
|
|
120
|
+
...(model ? { model } : {}),
|
|
121
|
+
...(profile ? { profile } : {}),
|
|
122
|
+
...(typeof frontmatter.confirmProjectAgents === "boolean"
|
|
123
|
+
? { confirmProjectAgents: frontmatter.confirmProjectAgents }
|
|
124
|
+
: {}),
|
|
125
|
+
systemPrompt: body,
|
|
126
|
+
source,
|
|
127
|
+
filePath,
|
|
128
|
+
});
|
|
129
|
+
} catch {
|
|
88
130
|
continue;
|
|
89
131
|
}
|
|
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
132
|
}
|
|
111
133
|
|
|
112
134
|
return agents;
|
|
@@ -148,14 +170,8 @@ export function discoverAgents(
|
|
|
148
170
|
|
|
149
171
|
const agentMap = new Map<string, AgentConfig>();
|
|
150
172
|
|
|
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
|
-
}
|
|
173
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
174
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
159
175
|
|
|
160
176
|
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
161
177
|
}
|
|
@@ -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
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
ExtensionAPI,
|
|
15
15
|
ExtensionContext,
|
|
16
16
|
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import type { ModelProfilesConfig } from "@pi-kaush/pi-model-profiles";
|
|
17
19
|
import {
|
|
18
20
|
discoverAgents,
|
|
19
21
|
formatAgentDisplayName,
|
|
@@ -21,18 +23,19 @@ import {
|
|
|
21
23
|
type AgentConfig,
|
|
22
24
|
} from "./agent-discovery.ts";
|
|
23
25
|
import { parseAgentModelSpec, type ThinkingLevel } from "./model-spec.ts";
|
|
26
|
+
import {
|
|
27
|
+
buildAgentPickerItems,
|
|
28
|
+
NONE_VALUE,
|
|
29
|
+
showAgentPicker,
|
|
30
|
+
} from "./agent-picker.ts";
|
|
24
31
|
|
|
25
32
|
interface ModelReference {
|
|
26
33
|
provider: string;
|
|
27
34
|
id: string;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
thinkingLevel?: ThinkingLevel;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export { parseAgentModelSpec };
|
|
37
|
+
export { isModuleUnavailable, parseAgentModelSpec };
|
|
38
|
+
export type { AgentModelSpec } from "./model-spec.ts";
|
|
36
39
|
|
|
37
40
|
// Durable session state is namespaced so it cannot collide with other
|
|
38
41
|
// packages. Legacy Aikado sessions wrote `active-agent-state`; restore reads
|
|
@@ -62,6 +65,51 @@ interface ActiveAgentState {
|
|
|
62
65
|
baseline?: AgentBaseline;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
69
|
+
if (typeof value !== "object" || value === null) return false;
|
|
70
|
+
const prototype = Object.getPrototypeOf(value);
|
|
71
|
+
return prototype === Object.prototype || prototype === null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isActiveAgentState(value: unknown): value is ActiveAgentState {
|
|
75
|
+
if (!isPlainObject(value) || typeof value.active !== "boolean") return false;
|
|
76
|
+
if (
|
|
77
|
+
value.active &&
|
|
78
|
+
(typeof value.name !== "string" || value.name.length === 0)
|
|
79
|
+
)
|
|
80
|
+
return false;
|
|
81
|
+
|
|
82
|
+
if (value.baseline !== undefined) {
|
|
83
|
+
if (!isPlainObject(value.baseline)) return false;
|
|
84
|
+
if (typeof value.baseline.thinkingLevel !== "string") return false;
|
|
85
|
+
if (
|
|
86
|
+
!Array.isArray(value.baseline.tools) ||
|
|
87
|
+
!value.baseline.tools.every((tool) => typeof tool === "string")
|
|
88
|
+
)
|
|
89
|
+
return false;
|
|
90
|
+
|
|
91
|
+
if (value.baseline.model !== undefined) {
|
|
92
|
+
if (
|
|
93
|
+
!isPlainObject(value.baseline.model) ||
|
|
94
|
+
typeof value.baseline.model.provider !== "string" ||
|
|
95
|
+
typeof value.baseline.model.id !== "string"
|
|
96
|
+
)
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isModuleUnavailable(error: unknown): boolean {
|
|
105
|
+
const code = (error as { code?: string } | undefined)?.code;
|
|
106
|
+
return (
|
|
107
|
+
code === "MODULE_NOT_FOUND" ||
|
|
108
|
+
code === "ERR_MODULE_NOT_FOUND" ||
|
|
109
|
+
(error instanceof Error && error.message.includes("Cannot find module"))
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
65
113
|
export function registerAgentMode(pi: ExtensionAPI) {
|
|
66
114
|
let activeAgent: ActiveAgent | undefined;
|
|
67
115
|
|
|
@@ -163,6 +211,67 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
163
211
|
pi.setActiveTools(valid);
|
|
164
212
|
}
|
|
165
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Walk the agent's profile candidates in order and switch to the first
|
|
216
|
+
* model that exists and is authenticated. Fallback happens only during
|
|
217
|
+
* activation; running sessions never switch models mid-request.
|
|
218
|
+
*/
|
|
219
|
+
async function applyProfileModels(
|
|
220
|
+
agent: AgentConfig,
|
|
221
|
+
ctx: ExtensionContext,
|
|
222
|
+
): Promise<{ thinkingLevel?: ThinkingLevel } | false> {
|
|
223
|
+
let profiles: ModelProfilesConfig;
|
|
224
|
+
try {
|
|
225
|
+
// Runtime-only import: a missing @pi-kaush/pi-model-profiles degrades
|
|
226
|
+
// just profile support instead of failing the whole extension load.
|
|
227
|
+
const profilesModule = await import("@pi-kaush/pi-model-profiles");
|
|
228
|
+
profiles = profilesModule.loadModelProfiles(
|
|
229
|
+
profilesModule.resolveProfilesPath(getAgentDir()),
|
|
230
|
+
);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (isModuleUnavailable(error)) {
|
|
233
|
+
ctx.ui.notify(
|
|
234
|
+
`Agent ${formatAgentDisplayName(agent)}: profile support unavailable; install it with pi install npm:@pi-kaush/pi-model-profiles.`,
|
|
235
|
+
"warning",
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
ctx.ui.notify(
|
|
239
|
+
`Agent ${formatAgentDisplayName(agent)}: ${error instanceof Error ? error.message : error}`,
|
|
240
|
+
"error",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const profileName = agent.profile!.trim();
|
|
247
|
+
const profile = profiles.profiles[profileName];
|
|
248
|
+
if (!profile) {
|
|
249
|
+
ctx.ui.notify(
|
|
250
|
+
`Agent ${formatAgentDisplayName(agent)}: unknown profile "${profileName}". Available: ${Object.keys(profiles.profiles).join(", ")}.`,
|
|
251
|
+
"error",
|
|
252
|
+
);
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const candidate of profile.candidates) {
|
|
257
|
+
const model = findModel(candidate.model, ctx);
|
|
258
|
+
if (!model) continue;
|
|
259
|
+
if (await pi.setModel(model)) {
|
|
260
|
+
return {
|
|
261
|
+
...(candidate.thinkingLevel !== undefined
|
|
262
|
+
? { thinkingLevel: candidate.thinkingLevel }
|
|
263
|
+
: {}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
ctx.ui.notify(
|
|
269
|
+
`Agent ${formatAgentDisplayName(agent)}: profile "${profileName}" has no available candidate models. Configured: ${profile.candidates.map((candidate) => candidate.model).join(", ")}.`,
|
|
270
|
+
"error",
|
|
271
|
+
);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
166
275
|
async function activateAgent(
|
|
167
276
|
agent: AgentConfig,
|
|
168
277
|
ctx: ExtensionContext,
|
|
@@ -174,7 +283,18 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
174
283
|
thinkingLevel: baseline.thinkingLevel ?? pi.getThinkingLevel(),
|
|
175
284
|
};
|
|
176
285
|
let thinkingLevel = baseline.thinkingLevel;
|
|
177
|
-
if (agent.model) {
|
|
286
|
+
if (agent.profile && agent.model) {
|
|
287
|
+
ctx.ui.notify(
|
|
288
|
+
`Agent ${formatAgentDisplayName(agent)}: declare either "profile" or "model" in frontmatter, not both.`,
|
|
289
|
+
"error",
|
|
290
|
+
);
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
if (agent.profile) {
|
|
294
|
+
const applied = await applyProfileModels(agent, ctx);
|
|
295
|
+
if (!applied) return false;
|
|
296
|
+
thinkingLevel = applied.thinkingLevel ?? baseline.thinkingLevel;
|
|
297
|
+
} else if (agent.model) {
|
|
178
298
|
const modelSpec = parseAgentModelSpec(agent.model);
|
|
179
299
|
const model = findModel(modelSpec.model, ctx);
|
|
180
300
|
if (!model) {
|
|
@@ -192,8 +312,12 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
192
312
|
return false;
|
|
193
313
|
}
|
|
194
314
|
thinkingLevel = modelSpec.thinkingLevel ?? baseline.thinkingLevel;
|
|
195
|
-
} else {
|
|
196
|
-
|
|
315
|
+
} else if (!(await restoreModel(baseline.model, ctx))) {
|
|
316
|
+
ctx.ui.notify(
|
|
317
|
+
`Agent ${formatAgentDisplayName(agent)}: could not restore the baseline model; activation aborted.`,
|
|
318
|
+
"error",
|
|
319
|
+
);
|
|
320
|
+
return false;
|
|
197
321
|
}
|
|
198
322
|
|
|
199
323
|
pi.setThinkingLevel(thinkingLevel);
|
|
@@ -210,15 +334,17 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
210
334
|
return;
|
|
211
335
|
}
|
|
212
336
|
|
|
213
|
-
await restoreModel(activeAgent.baseline.model, ctx);
|
|
337
|
+
const modelRestored = await restoreModel(activeAgent.baseline.model, ctx);
|
|
214
338
|
pi.setThinkingLevel(activeAgent.baseline.thinkingLevel);
|
|
215
339
|
pi.setActiveTools(activeAgent.baseline.tools);
|
|
216
340
|
activeAgent = undefined;
|
|
217
341
|
updateAgentStatus(ctx);
|
|
218
342
|
persistActiveAgent();
|
|
219
343
|
ctx.ui.notify(
|
|
220
|
-
|
|
221
|
-
|
|
344
|
+
modelRestored
|
|
345
|
+
? "Active agent cleared; previous model and tools restored."
|
|
346
|
+
: "Active agent cleared, but the previous model could not be restored.",
|
|
347
|
+
modelRestored ? "info" : "warning",
|
|
222
348
|
);
|
|
223
349
|
}
|
|
224
350
|
|
|
@@ -272,13 +398,52 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
272
398
|
);
|
|
273
399
|
return;
|
|
274
400
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
401
|
+
let picked: string | null;
|
|
402
|
+
if (ctx.mode === "tui") {
|
|
403
|
+
picked = await showAgentPicker(
|
|
404
|
+
ctx,
|
|
405
|
+
buildAgentPickerItems(agents, activeAgent?.agent.name),
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
const items = buildAgentPickerItems(agents, activeAgent?.agent.name);
|
|
409
|
+
const baseLabels = items.map(
|
|
410
|
+
(item) => `${item.label} — ${item.description}`,
|
|
411
|
+
);
|
|
412
|
+
const labelCounts = new Map<string, number>();
|
|
413
|
+
for (const label of baseLabels) {
|
|
414
|
+
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
|
415
|
+
}
|
|
416
|
+
const labels = baseLabels.map((label, index) =>
|
|
417
|
+
labelCounts.get(label)! > 1
|
|
418
|
+
? `${label} [${items[index]!.value}]`
|
|
419
|
+
: label,
|
|
420
|
+
);
|
|
421
|
+
const choice = await ctx.ui.select("Activate agent:", labels);
|
|
422
|
+
if (choice === undefined) {
|
|
423
|
+
picked = null;
|
|
424
|
+
} else {
|
|
425
|
+
const firstMatch = labels.indexOf(choice);
|
|
426
|
+
if (
|
|
427
|
+
firstMatch !== -1 &&
|
|
428
|
+
labels.lastIndexOf(choice) !== firstMatch
|
|
429
|
+
) {
|
|
430
|
+
ctx.ui.notify(
|
|
431
|
+
"That selection matches more than one agent; rename one of the duplicates.",
|
|
432
|
+
"warning",
|
|
433
|
+
);
|
|
434
|
+
picked = null;
|
|
435
|
+
} else {
|
|
436
|
+
picked =
|
|
437
|
+
firstMatch === -1 ? null : (items[firstMatch]?.value ?? null);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (!picked) return;
|
|
442
|
+
if (picked === NONE_VALUE) {
|
|
443
|
+
await clearActiveAgent(ctx);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
agent = agents.find((candidate) => candidate.name === picked);
|
|
282
447
|
}
|
|
283
448
|
|
|
284
449
|
if (!agent || !(await confirmProjectAgent(agent, ctx))) return;
|
|
@@ -302,7 +467,8 @@ export function registerAgentMode(pi: ExtensionAPI) {
|
|
|
302
467
|
.filter(
|
|
303
468
|
(entry): entry is CustomEntry<ActiveAgentState> =>
|
|
304
469
|
entry.type === "custom" &&
|
|
305
|
-
ACTIVE_AGENT_STATE_TYPES.includes(entry.customType)
|
|
470
|
+
ACTIVE_AGENT_STATE_TYPES.includes(entry.customType) &&
|
|
471
|
+
isActiveAgentState(entry.data),
|
|
306
472
|
)
|
|
307
473
|
.pop();
|
|
308
474
|
const state = stateEntry?.data;
|