pi-advisor-flow 0.5.1 → 0.5.3
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 +21 -0
- package/README.md +3 -2
- package/extensions/index.ts +7 -7
- package/package.json +2 -1
- package/src/commands/activation-preparation.ts +117 -0
- package/src/commands/activation.ts +135 -0
- package/src/commands/lifecycle.ts +83 -0
- package/src/commands/manual-command.ts +107 -0
- package/src/commands/manual-consultation.ts +144 -0
- package/src/commands/manual-progress.ts +93 -0
- package/src/commands/model-commands.ts +66 -0
- package/src/commands/model-options.ts +181 -0
- package/src/commands/model-picker.ts +110 -0
- package/src/commands/registration.ts +27 -0
- package/src/commands/renderers.ts +76 -0
- package/src/commands/runtime.ts +114 -0
- package/src/commands/settings-commands.ts +80 -0
- package/src/commands/settings-persistence.ts +81 -0
- package/src/commands/types.ts +78 -0
- package/src/commands.ts +9 -1259
- package/src/config/args.ts +37 -0
- package/src/config/defaults.ts +190 -0
- package/src/config/state.ts +199 -0
- package/src/config/storage.ts +224 -0
- package/src/config/types.ts +63 -0
- package/src/config/validation.ts +234 -0
- package/src/config.ts +104 -814
- package/src/conversation.ts +4 -2
- package/src/herdr.ts +1 -1
- package/src/model-stream.ts +15 -1
- package/src/scout-context.ts +2 -2
- package/src/scout.ts +1 -1
- package/src/tools/consultation.ts +337 -0
- package/src/tools/gate-policy.ts +132 -0
- package/src/tools/gate-protocol.ts +127 -0
- package/src/tools/loop-gate.ts +210 -0
- package/src/tools/prompts.ts +159 -0
- package/src/tools/register-ask-advisor.ts +247 -0
- package/src/tools/register-lifecycle.ts +119 -0
- package/src/tools/register-outcome.ts +75 -0
- package/src/tools/register-renderers.ts +88 -0
- package/src/tools/registration.ts +35 -0
- package/src/tools/render-advisor-result.ts +168 -0
- package/src/tools/render-common.ts +120 -0
- package/src/tools/scout-status.ts +206 -0
- package/src/tools/session.ts +3 -0
- package/src/tools/types.ts +119 -0
- package/src/tools.ts +42 -1865
- package/src/ui/manual-dialog-render.ts +150 -0
- package/src/ui/manual-dialog.ts +305 -0
- package/src/ui/model-selector.ts +166 -0
- package/src/ui/settings-formatting.ts +133 -0
- package/src/ui/settings-items.ts +323 -0
- package/src/ui/settings-list-adapter.ts +103 -0
- package/src/ui/settings-mutations.ts +80 -0
- package/src/ui/settings-selector.ts +161 -0
- package/src/ui/text-setting-submenu.ts +66 -0
- package/src/ui/types.ts +111 -0
- package/src/ui.ts +9 -1418
package/src/commands.ts
CHANGED
|
@@ -1,1259 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
advisorRef,
|
|
11
|
-
alwaysOnRef,
|
|
12
|
-
contextMaxCharsRef,
|
|
13
|
-
executorEffortRef,
|
|
14
|
-
executorRef,
|
|
15
|
-
getAdvisorMaxCallsPerSession,
|
|
16
|
-
getAdvisorSettings,
|
|
17
|
-
getPersistedModelRefs,
|
|
18
|
-
isSimpleMode,
|
|
19
|
-
loadConfig,
|
|
20
|
-
parseArgs,
|
|
21
|
-
saveConfig,
|
|
22
|
-
saveGlobalOutcomeLogging,
|
|
23
|
-
setAdvisorAutoLoopGateRef,
|
|
24
|
-
setAdvisorBlockOnBlockedRef,
|
|
25
|
-
setAdvisorCollapseResponsesRef,
|
|
26
|
-
setAdvisorCompletionGateRef,
|
|
27
|
-
setAdvisorCustomInvocationRef,
|
|
28
|
-
setAdvisorEffortRef,
|
|
29
|
-
setAdvisorFailureGateRef,
|
|
30
|
-
setAdvisorFailureModeRef,
|
|
31
|
-
setAdvisorGitContextMaxCharsRef,
|
|
32
|
-
setAdvisorGitContextRef,
|
|
33
|
-
setAdvisorHerdrIntegrationRef,
|
|
34
|
-
setAdvisorLoopThresholdRef,
|
|
35
|
-
setAdvisorMaxCallsPerSessionRef,
|
|
36
|
-
setAdvisorOutcomeLoggingRef,
|
|
37
|
-
setAdvisorPlanGateRef,
|
|
38
|
-
setAdvisorRedactSecretsRef,
|
|
39
|
-
setAdvisorRef,
|
|
40
|
-
setAdvisorScoutEnabledRef,
|
|
41
|
-
setAdvisorSessionSummaryRef,
|
|
42
|
-
setAdvisorToolPoliciesRef,
|
|
43
|
-
setAdvisorToolResultMaxBytesRef,
|
|
44
|
-
setAdvisorToolResultMaxLinesRef,
|
|
45
|
-
setAdvisorTrackedFileContentRef,
|
|
46
|
-
setAdvisorUntrackedContentRef,
|
|
47
|
-
setAlwaysOnRef,
|
|
48
|
-
setContextMaxCharsRef,
|
|
49
|
-
setExecutorEffortRef,
|
|
50
|
-
setExecutorRef,
|
|
51
|
-
setShowUsageDetailsRef,
|
|
52
|
-
setShowUsageFooterRef,
|
|
53
|
-
setSimpleModeRef,
|
|
54
|
-
splitRef,
|
|
55
|
-
} from "./config.js";
|
|
56
|
-
import type { GitContextLevel } from "./git.js";
|
|
57
|
-
import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "./herdr.js";
|
|
58
|
-
import type { ScoutLifecycleEvent } from "./scout.js";
|
|
59
|
-
import type { AdvisorSessionState } from "./session-state.js";
|
|
60
|
-
import {
|
|
61
|
-
adviceForDisplay,
|
|
62
|
-
appendScoutLifecycleEntry,
|
|
63
|
-
consultAdvisor,
|
|
64
|
-
advisorSessionState as defaultAdvisorSessionState,
|
|
65
|
-
hasSoundVerdict,
|
|
66
|
-
renderAdvisorCallBox,
|
|
67
|
-
renderAdvisorResponseHeader,
|
|
68
|
-
renderScoutDetails,
|
|
69
|
-
renderThinkingMarkdown,
|
|
70
|
-
resolveAdvisorRequest,
|
|
71
|
-
ScoutStatusManager,
|
|
72
|
-
type ScoutToolDetails,
|
|
73
|
-
SPINNER_FRAMES,
|
|
74
|
-
} from "./tools.js";
|
|
75
|
-
import {
|
|
76
|
-
type AdvisorSettings,
|
|
77
|
-
AdvisorSettingsSelector,
|
|
78
|
-
type ContextPreset,
|
|
79
|
-
ManualAdvisorDialog,
|
|
80
|
-
type ManualAdvisorRequest,
|
|
81
|
-
SearchableModelSelector,
|
|
82
|
-
} from "./ui.js";
|
|
83
|
-
import {
|
|
84
|
-
advisorUsageCost,
|
|
85
|
-
formatAdvisorUsage,
|
|
86
|
-
snapshotAdvisorUsage,
|
|
87
|
-
} from "./usage.js";
|
|
88
|
-
|
|
89
|
-
const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
|
|
90
|
-
const SELECTED_PREFIX = "✓ ";
|
|
91
|
-
const EFFORT_LEVELS = [
|
|
92
|
-
DEFAULT_EFFORT_LEVEL,
|
|
93
|
-
"off",
|
|
94
|
-
"minimal",
|
|
95
|
-
"low",
|
|
96
|
-
"medium",
|
|
97
|
-
"high",
|
|
98
|
-
"xhigh",
|
|
99
|
-
"max",
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
const effortChoices = (configured: string | undefined): string[] => {
|
|
103
|
-
const current = configured ?? DEFAULT_EFFORT_LEVEL;
|
|
104
|
-
return [
|
|
105
|
-
`${SELECTED_PREFIX}${current}`,
|
|
106
|
-
...EFFORT_LEVELS.filter((level) => level !== current),
|
|
107
|
-
];
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const selectedEffort = (choice: string): string | undefined => {
|
|
111
|
-
const effort = choice.startsWith(SELECTED_PREFIX)
|
|
112
|
-
? choice.slice(SELECTED_PREFIX.length)
|
|
113
|
-
: choice;
|
|
114
|
-
return effort === DEFAULT_EFFORT_LEVEL ? undefined : effort;
|
|
115
|
-
};
|
|
116
|
-
const ADVISOR_ACTIVATION_EXPLANATION =
|
|
117
|
-
"The Advisor is a second-opinion model that reviews the Executor's context and returns risks, alternatives, and verification steps without changing files or running tools.";
|
|
118
|
-
const ARGUMENT_WHITESPACE = /\s+/;
|
|
119
|
-
const hasModelOverride = (args: string, key: "advisor" | "executor") =>
|
|
120
|
-
args
|
|
121
|
-
.trim()
|
|
122
|
-
.split(ARGUMENT_WHITESPACE)
|
|
123
|
-
.some((token) => {
|
|
124
|
-
const [tokenKey, value] = token.split("=");
|
|
125
|
-
return tokenKey === key && Boolean(value);
|
|
126
|
-
});
|
|
127
|
-
const hasExecutorOverride = (args: string) =>
|
|
128
|
-
hasModelOverride(args, "executor");
|
|
129
|
-
const hasAdvisorOverride = (args: string) => hasModelOverride(args, "advisor");
|
|
130
|
-
|
|
131
|
-
const CONTEXT_PRESETS: ContextPreset[] = [
|
|
132
|
-
{
|
|
133
|
-
description:
|
|
134
|
-
"No conversation history. The Advisor receives only its standing instructions.",
|
|
135
|
-
label: "0",
|
|
136
|
-
value: 0,
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
description: "The most recent 10,000 characters of the current branch.",
|
|
140
|
-
label: "10k",
|
|
141
|
-
value: 10_000,
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
description: "The most recent 25,000 characters of the current branch.",
|
|
145
|
-
label: "25k",
|
|
146
|
-
value: 25_000,
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
description: "The most recent 100,000 characters of the current branch.",
|
|
150
|
-
label: "100k",
|
|
151
|
-
value: 100_000,
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
description: "The most recent 200,000 characters of the current branch.",
|
|
155
|
-
label: "200k",
|
|
156
|
-
value: 200_000,
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
description:
|
|
160
|
-
"The complete reconstructed conversation branch. Cost and model context limits apply.",
|
|
161
|
-
label: "ALL",
|
|
162
|
-
value: Number.MAX_SAFE_INTEGER,
|
|
163
|
-
},
|
|
164
|
-
];
|
|
165
|
-
|
|
166
|
-
type ManualConsult = (
|
|
167
|
-
ctx: ExtensionContext,
|
|
168
|
-
question?: string,
|
|
169
|
-
signal?: AbortSignal,
|
|
170
|
-
onChunk?: (thinking: string, text: string) => void,
|
|
171
|
-
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
172
|
-
gitContext?: GitContextLevel
|
|
173
|
-
) => Promise<{
|
|
174
|
-
markdown: string;
|
|
175
|
-
thinkingText: string;
|
|
176
|
-
draftBytes?: number;
|
|
177
|
-
preferenceBytes?: number;
|
|
178
|
-
usage?: unknown;
|
|
179
|
-
}>;
|
|
180
|
-
type ThinkingLevel = Parameters<ExtensionAPI["setThinkingLevel"]>[0];
|
|
181
|
-
|
|
182
|
-
const notify = (
|
|
183
|
-
ctx: ExtensionContext,
|
|
184
|
-
message: string,
|
|
185
|
-
level: "error" | "info" | "warning"
|
|
186
|
-
) => {
|
|
187
|
-
if (ctx.hasUI) {
|
|
188
|
-
ctx.ui.notify(message, level);
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
type ManualAdvisorProgressPhase =
|
|
193
|
-
| "preparing"
|
|
194
|
-
| "active"
|
|
195
|
-
| "complete"
|
|
196
|
-
| "cancelled"
|
|
197
|
-
| "error";
|
|
198
|
-
interface ManualAdvisorProgressState {
|
|
199
|
-
phase: ManualAdvisorProgressPhase;
|
|
200
|
-
scout?: ScoutToolDetails;
|
|
201
|
-
text?: string;
|
|
202
|
-
thinking?: string;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
class ManualAdvisorProgressComponent implements Component {
|
|
206
|
-
private readonly question: string | undefined;
|
|
207
|
-
private readonly state: ManualAdvisorProgressState;
|
|
208
|
-
private readonly expanded: boolean;
|
|
209
|
-
private readonly theme: Theme;
|
|
210
|
-
|
|
211
|
-
constructor(
|
|
212
|
-
question: string | undefined,
|
|
213
|
-
state: ManualAdvisorProgressState,
|
|
214
|
-
expanded: boolean,
|
|
215
|
-
theme: Theme
|
|
216
|
-
) {
|
|
217
|
-
this.question = question;
|
|
218
|
-
this.state = state;
|
|
219
|
-
this.expanded = expanded;
|
|
220
|
-
this.theme = theme;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
render(width: number): string[] {
|
|
224
|
-
const box = renderAdvisorCallBox(this.question, this.theme);
|
|
225
|
-
if (this.state.phase === "complete" || this.state.phase === "cancelled") {
|
|
226
|
-
return box.render(width);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const { scout } = this.state;
|
|
230
|
-
const scoutActive =
|
|
231
|
-
scout?.status === "calling" || scout?.status === "streaming";
|
|
232
|
-
if (scoutActive) {
|
|
233
|
-
renderScoutDetails(box, scout, this.expanded, this.theme);
|
|
234
|
-
return box.render(width);
|
|
235
|
-
}
|
|
236
|
-
if (scout?.status === "cancelled") {
|
|
237
|
-
return box.render(width);
|
|
238
|
-
}
|
|
239
|
-
if (this.state.phase === "error") {
|
|
240
|
-
box.addChild(
|
|
241
|
-
new Text(
|
|
242
|
-
this.theme.fg("error", this.theme.bold("◆ ADVISOR · FAILED")),
|
|
243
|
-
0,
|
|
244
|
-
0
|
|
245
|
-
)
|
|
246
|
-
);
|
|
247
|
-
return box.render(width);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const frame =
|
|
251
|
-
SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
|
|
252
|
-
let status = "Working…";
|
|
253
|
-
if (this.state.phase === "preparing") {
|
|
254
|
-
status = "Preparing…";
|
|
255
|
-
} else if (this.state.text?.trim()) {
|
|
256
|
-
status = "Responding…";
|
|
257
|
-
}
|
|
258
|
-
box.addChild(
|
|
259
|
-
new Text(
|
|
260
|
-
`${this.theme.fg("warning", this.theme.bold(`◆ ADVISOR ${frame}`))} ${this.theme.fg("dim", `· ${status}`)}`,
|
|
261
|
-
0,
|
|
262
|
-
0
|
|
263
|
-
)
|
|
264
|
-
);
|
|
265
|
-
if (this.state.thinking?.trim()) {
|
|
266
|
-
box.addChild(
|
|
267
|
-
renderThinkingMarkdown(this.state.thinking.slice(-200), this.theme)
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
if (this.state.text) {
|
|
271
|
-
box.addChild(
|
|
272
|
-
new Markdown(
|
|
273
|
-
adviceForDisplay(this.state.text, this.expanded),
|
|
274
|
-
0,
|
|
275
|
-
0,
|
|
276
|
-
getMarkdownTheme()
|
|
277
|
-
)
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
return box.render(width);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
invalidate(): void {
|
|
284
|
-
// The live progress state is read during each render.
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const findConfiguredModel = (
|
|
289
|
-
ctx: ExtensionContext,
|
|
290
|
-
ref: string | undefined
|
|
291
|
-
) => {
|
|
292
|
-
if (!ref) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
const [provider, modelId] = splitRef(ref);
|
|
296
|
-
return ctx.modelRegistry.find(provider, modelId);
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const getAvailableModelRefs = (ctx: ExtensionContext): string[] | undefined => {
|
|
300
|
-
if (typeof ctx.modelRegistry.getAvailable !== "function") {
|
|
301
|
-
return undefined;
|
|
302
|
-
}
|
|
303
|
-
return ctx.modelRegistry
|
|
304
|
-
.getAvailable()
|
|
305
|
-
.map((model) => `${model.provider}/${model.id}`);
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const isSelectableModel = (
|
|
309
|
-
ctx: ExtensionContext,
|
|
310
|
-
ref: string | undefined,
|
|
311
|
-
availableRefs: Set<string> | undefined
|
|
312
|
-
) => {
|
|
313
|
-
if (!ref) {
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
if (availableRefs) {
|
|
317
|
-
const [provider, modelId] = splitRef(ref);
|
|
318
|
-
return availableRefs.has(`${provider}/${modelId}`);
|
|
319
|
-
}
|
|
320
|
-
return Boolean(findConfiguredModel(ctx, ref));
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
const getExplicitModelError = (
|
|
324
|
-
ctx: ExtensionContext,
|
|
325
|
-
ref: string | undefined,
|
|
326
|
-
label: "Advisor" | "Executor",
|
|
327
|
-
overridden: boolean,
|
|
328
|
-
availableRefs: Set<string> | undefined
|
|
329
|
-
) => {
|
|
330
|
-
if (overridden) {
|
|
331
|
-
if (!ref) {
|
|
332
|
-
return `${label} model not configured`;
|
|
333
|
-
}
|
|
334
|
-
if (!findConfiguredModel(ctx, ref)) {
|
|
335
|
-
return `${label} model not found: ${ref}`;
|
|
336
|
-
}
|
|
337
|
-
if (availableRefs && !isSelectableModel(ctx, ref, availableRefs)) {
|
|
338
|
-
return `${label} model unavailable: ${ref}`;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
interface ActivationModelPlan {
|
|
344
|
-
pendingExecutor: string | undefined;
|
|
345
|
-
selectAdvisor: boolean;
|
|
346
|
-
selectExecutor: boolean;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const planActivationModels = (
|
|
350
|
-
ctx: ExtensionContext,
|
|
351
|
-
executor: string | undefined,
|
|
352
|
-
advisor: string | undefined,
|
|
353
|
-
pendingExecutorRef: string | undefined,
|
|
354
|
-
persisted: ReturnType<typeof getPersistedModelRefs>,
|
|
355
|
-
executorOverride: boolean,
|
|
356
|
-
advisorOverride: boolean,
|
|
357
|
-
availableRefs: Set<string> | undefined
|
|
358
|
-
): ActivationModelPlan => {
|
|
359
|
-
const pendingExecutor =
|
|
360
|
-
!executorOverride &&
|
|
361
|
-
pendingExecutorRef &&
|
|
362
|
-
isSelectableModel(ctx, pendingExecutorRef, availableRefs)
|
|
363
|
-
? pendingExecutorRef
|
|
364
|
-
: undefined;
|
|
365
|
-
const executorConfigured =
|
|
366
|
-
executorOverride ||
|
|
367
|
-
Boolean(pendingExecutor) ||
|
|
368
|
-
Boolean(
|
|
369
|
-
persisted.executor && isSelectableModel(ctx, executor, availableRefs)
|
|
370
|
-
);
|
|
371
|
-
const advisorConfigured =
|
|
372
|
-
advisorOverride ||
|
|
373
|
-
Boolean(
|
|
374
|
-
persisted.advisor && isSelectableModel(ctx, advisor, availableRefs)
|
|
375
|
-
);
|
|
376
|
-
return {
|
|
377
|
-
pendingExecutor,
|
|
378
|
-
selectAdvisor: !advisorConfigured,
|
|
379
|
-
selectExecutor: !executorConfigured,
|
|
380
|
-
};
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
interface AdvisorModelSelection {
|
|
384
|
-
advisor: string;
|
|
385
|
-
advisorEffort: string | undefined;
|
|
386
|
-
executor: string;
|
|
387
|
-
executorEffort: string | undefined;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
interface AdvisorModelPickerOptions {
|
|
391
|
-
advisor: string | undefined;
|
|
392
|
-
advisorEffort: string | undefined;
|
|
393
|
-
executor: string | undefined;
|
|
394
|
-
executorEffort: string | undefined;
|
|
395
|
-
selectAdvisor: boolean;
|
|
396
|
-
selectExecutor: boolean;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const selectAdvisorModels = async (
|
|
400
|
-
ctx: ExtensionContext,
|
|
401
|
-
options: AdvisorModelPickerOptions
|
|
402
|
-
): Promise<AdvisorModelSelection | undefined> => {
|
|
403
|
-
if (!ctx.hasUI) {
|
|
404
|
-
return undefined;
|
|
405
|
-
}
|
|
406
|
-
const refs = getAvailableModelRefs(ctx);
|
|
407
|
-
if (refs && refs.length === 0) {
|
|
408
|
-
notify(
|
|
409
|
-
ctx,
|
|
410
|
-
"No selectable models are available. Configure a provider with /login or models.json, then retry.",
|
|
411
|
-
"error"
|
|
412
|
-
);
|
|
413
|
-
return undefined;
|
|
414
|
-
}
|
|
415
|
-
const allOptions = [
|
|
416
|
-
...new Set(
|
|
417
|
-
refs ??
|
|
418
|
-
[options.executor, options.advisor].filter((ref): ref is string =>
|
|
419
|
-
Boolean(ref)
|
|
420
|
-
)
|
|
421
|
-
),
|
|
422
|
-
];
|
|
423
|
-
let { advisor, advisorEffort, executor, executorEffort } = options;
|
|
424
|
-
|
|
425
|
-
if (options.selectExecutor) {
|
|
426
|
-
const selectedExecutor = await ctx.ui.custom<string | undefined>(
|
|
427
|
-
(tui, theme, keybindings, done) =>
|
|
428
|
-
new SearchableModelSelector({
|
|
429
|
-
allOptions,
|
|
430
|
-
currentOption: executor || undefined,
|
|
431
|
-
keybindings,
|
|
432
|
-
onCancel: () => done(undefined),
|
|
433
|
-
onSelect: done,
|
|
434
|
-
theme,
|
|
435
|
-
title: "Select Executor Model",
|
|
436
|
-
tui,
|
|
437
|
-
})
|
|
438
|
-
);
|
|
439
|
-
if (!selectedExecutor) {
|
|
440
|
-
return undefined;
|
|
441
|
-
}
|
|
442
|
-
const selectedExecutorEffort = await ctx.ui.select(
|
|
443
|
-
"Select Executor Reasoning/Thinking Level",
|
|
444
|
-
effortChoices(executorEffort)
|
|
445
|
-
);
|
|
446
|
-
if (!selectedExecutorEffort) {
|
|
447
|
-
return undefined;
|
|
448
|
-
}
|
|
449
|
-
executor = selectedExecutor;
|
|
450
|
-
executorEffort = selectedEffort(selectedExecutorEffort);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (options.selectAdvisor) {
|
|
454
|
-
const selectedAdvisor = await ctx.ui.custom<string | undefined>(
|
|
455
|
-
(tui, theme, keybindings, done) =>
|
|
456
|
-
new SearchableModelSelector({
|
|
457
|
-
allOptions,
|
|
458
|
-
currentOption: advisor || undefined,
|
|
459
|
-
keybindings,
|
|
460
|
-
onCancel: () => done(undefined),
|
|
461
|
-
onSelect: done,
|
|
462
|
-
theme,
|
|
463
|
-
title: "Select Advisor Model",
|
|
464
|
-
tui,
|
|
465
|
-
})
|
|
466
|
-
);
|
|
467
|
-
if (!selectedAdvisor) {
|
|
468
|
-
return undefined;
|
|
469
|
-
}
|
|
470
|
-
const selectedAdvisorEffort = await ctx.ui.select(
|
|
471
|
-
"Select Advisor Reasoning/Thinking Level",
|
|
472
|
-
effortChoices(advisorEffort)
|
|
473
|
-
);
|
|
474
|
-
if (!selectedAdvisorEffort) {
|
|
475
|
-
return undefined;
|
|
476
|
-
}
|
|
477
|
-
advisor = selectedAdvisor;
|
|
478
|
-
advisorEffort = selectedEffort(selectedAdvisorEffort);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (!(advisor && executor)) {
|
|
482
|
-
return undefined;
|
|
483
|
-
}
|
|
484
|
-
return { advisor, advisorEffort, executor, executorEffort };
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one settings form maps every persisted control.
|
|
488
|
-
const applyAdvisorSettings = (settings: AdvisorSettings) => {
|
|
489
|
-
setAdvisorEffortRef(
|
|
490
|
-
settings.effort === "Default (Model Default)" ? undefined : settings.effort
|
|
491
|
-
);
|
|
492
|
-
setContextMaxCharsRef(settings.contextMaxChars);
|
|
493
|
-
setAdvisorPlanGateRef(settings.planGate);
|
|
494
|
-
setAdvisorFailureGateRef(settings.failureGate);
|
|
495
|
-
setAdvisorCompletionGateRef(settings.completionGate);
|
|
496
|
-
setAdvisorCollapseResponsesRef(settings.collapseResponses);
|
|
497
|
-
setAdvisorCustomInvocationRef(settings.customRule);
|
|
498
|
-
setAdvisorBlockOnBlockedRef(settings.blockOnBlocked ?? true);
|
|
499
|
-
setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
|
|
500
|
-
setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
|
|
501
|
-
setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
|
|
502
|
-
setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
|
|
503
|
-
setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
|
|
504
|
-
setShowUsageDetailsRef(settings.showUsageDetails ?? true);
|
|
505
|
-
setShowUsageFooterRef(settings.showUsageFooter ?? false);
|
|
506
|
-
setSimpleModeRef(settings.simpleMode ?? false);
|
|
507
|
-
setAlwaysOnRef(settings.alwaysOn ?? false);
|
|
508
|
-
setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
|
|
509
|
-
setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
|
|
510
|
-
setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
|
|
511
|
-
setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
|
|
512
|
-
setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
|
|
513
|
-
setAdvisorGitContextRef(settings.gitContext ?? "summary");
|
|
514
|
-
setAdvisorGitContextMaxCharsRef(settings.gitContextMaxChars ?? 20_000);
|
|
515
|
-
setAdvisorToolPoliciesRef(settings.toolPolicies ?? {});
|
|
516
|
-
setAdvisorTrackedFileContentRef(settings.trackedFileContent ?? false);
|
|
517
|
-
setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
|
|
518
|
-
setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
const saveAdvisorSettings = (
|
|
522
|
-
ctx: ExtensionContext,
|
|
523
|
-
settings: AdvisorSettings
|
|
524
|
-
) => {
|
|
525
|
-
applyAdvisorSettings(settings);
|
|
526
|
-
const persisted = getPersistedModelRefs();
|
|
527
|
-
saveConfig(ctx, {
|
|
528
|
-
persistAdvisor: Boolean(persisted.advisor),
|
|
529
|
-
persistExecutor: Boolean(persisted.executor),
|
|
530
|
-
});
|
|
531
|
-
saveGlobalOutcomeLogging(settings.outcomeLogging ?? false);
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
export const registerCommands = (
|
|
535
|
-
pi: ExtensionAPI,
|
|
536
|
-
dependencies: {
|
|
537
|
-
consult?: ManualConsult;
|
|
538
|
-
sessionState?: AdvisorSessionState;
|
|
539
|
-
statusManager?: ScoutStatusManager;
|
|
540
|
-
} = {}
|
|
541
|
-
) => {
|
|
542
|
-
const advisorSessionState =
|
|
543
|
-
dependencies.sessionState ?? defaultAdvisorSessionState;
|
|
544
|
-
const scoutStatus =
|
|
545
|
-
dependencies.statusManager ?? new ScoutStatusManager(false);
|
|
546
|
-
const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
|
|
547
|
-
const requestAdvisor =
|
|
548
|
-
dependencies.consult ??
|
|
549
|
-
((ctx, question, signal, onChunk, onScout, gitContext) =>
|
|
550
|
-
consultAdvisor(
|
|
551
|
-
ctx,
|
|
552
|
-
question,
|
|
553
|
-
signal,
|
|
554
|
-
onChunk,
|
|
555
|
-
"manual",
|
|
556
|
-
gitContext,
|
|
557
|
-
undefined,
|
|
558
|
-
undefined,
|
|
559
|
-
undefined,
|
|
560
|
-
onScout,
|
|
561
|
-
undefined
|
|
562
|
-
));
|
|
563
|
-
// Pi's model_select event reports both built-in `/model` changes and direct
|
|
564
|
-
// pi.setModel() calls. Keep an inactive user selection transiently so
|
|
565
|
-
// `/advisor` can adopt it without treating every normal model change as a
|
|
566
|
-
// global default. The suppression flag covers this extension's own restore.
|
|
567
|
-
let pendingExecutorModelRef: string | undefined;
|
|
568
|
-
let suppressModelSelectionSync = false;
|
|
569
|
-
const setExecutorModel = async (
|
|
570
|
-
model: Parameters<ExtensionAPI["setModel"]>[0]
|
|
571
|
-
) => {
|
|
572
|
-
suppressModelSelectionSync = true;
|
|
573
|
-
try {
|
|
574
|
-
return await pi.setModel(model);
|
|
575
|
-
} finally {
|
|
576
|
-
suppressModelSelectionSync = false;
|
|
577
|
-
}
|
|
578
|
-
};
|
|
579
|
-
const manualConsultations = new Map<AbortController, symbol>();
|
|
580
|
-
const manualProgressTimers = new Map<
|
|
581
|
-
AbortController,
|
|
582
|
-
ReturnType<typeof setInterval>
|
|
583
|
-
>();
|
|
584
|
-
const manualProgress = new Map<string, ManualAdvisorProgressState>();
|
|
585
|
-
let manualProgressSequence = 0;
|
|
586
|
-
const updateAdvisorUsageStatus = (ctx: ExtensionContext) => {
|
|
587
|
-
if (ctx.hasUI) {
|
|
588
|
-
ctx.ui.setStatus(
|
|
589
|
-
"advisor-usage",
|
|
590
|
-
getAdvisorSettings().showUsageFooter
|
|
591
|
-
? advisorSessionState.usageStatus()
|
|
592
|
-
: undefined
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
};
|
|
596
|
-
const reportManualBudgetExhausted = (ctx: ExtensionContext) => {
|
|
597
|
-
const message = "Advisor call budget exhausted for this session.";
|
|
598
|
-
notify(ctx, message, "warning");
|
|
599
|
-
notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
// A status update with an undefined value is an invisible render pulse. The
|
|
603
|
-
// transcript owns manual progress now, so no consultation text is kept in the
|
|
604
|
-
// footer while this pulse still drives the live spinner.
|
|
605
|
-
const requestManualRender = (ctx: ExtensionContext) => {
|
|
606
|
-
if (ctx.hasUI) {
|
|
607
|
-
ctx.ui.setStatus("advisor-manual", undefined);
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
const startManualConsultation = (
|
|
612
|
-
ctx: ExtensionContext,
|
|
613
|
-
question: string | undefined,
|
|
614
|
-
controller: AbortController,
|
|
615
|
-
scoutStatusToken: symbol,
|
|
616
|
-
progress: ManualAdvisorProgressState,
|
|
617
|
-
gitContext?: GitContextLevel
|
|
618
|
-
) => {
|
|
619
|
-
herdrAdvisorActivity.start();
|
|
620
|
-
progress.phase = "preparing";
|
|
621
|
-
requestManualRender(ctx);
|
|
622
|
-
if (ctx.hasUI) {
|
|
623
|
-
const timer = setInterval(() => {
|
|
624
|
-
if (
|
|
625
|
-
controller.signal.aborted ||
|
|
626
|
-
manualConsultations.get(controller) !== scoutStatusToken
|
|
627
|
-
) {
|
|
628
|
-
clearInterval(timer);
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
requestManualRender(ctx);
|
|
632
|
-
}, 80);
|
|
633
|
-
manualProgressTimers.set(controller, timer);
|
|
634
|
-
}
|
|
635
|
-
let scoutDetails: Parameters<typeof appendScoutLifecycleEntry>[2];
|
|
636
|
-
return requestAdvisor(
|
|
637
|
-
ctx,
|
|
638
|
-
question,
|
|
639
|
-
controller.signal,
|
|
640
|
-
(thinking, text) => {
|
|
641
|
-
if (controller.signal.aborted) {
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
progress.phase = "active";
|
|
645
|
-
progress.thinking = thinking;
|
|
646
|
-
progress.text = text;
|
|
647
|
-
requestManualRender(ctx);
|
|
648
|
-
},
|
|
649
|
-
(event) => {
|
|
650
|
-
if (!controller.signal.aborted) {
|
|
651
|
-
scoutStatus.update(ctx, scoutStatusToken, event);
|
|
652
|
-
scoutDetails = appendScoutLifecycleEntry(pi, event, scoutDetails);
|
|
653
|
-
progress.scout = scoutDetails;
|
|
654
|
-
progress.phase = "active";
|
|
655
|
-
requestManualRender(ctx);
|
|
656
|
-
}
|
|
657
|
-
},
|
|
658
|
-
gitContext
|
|
659
|
-
)
|
|
660
|
-
.then(({ markdown, usage }) => {
|
|
661
|
-
if (controller.signal.aborted) {
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
progress.phase = "complete";
|
|
665
|
-
advisorSessionState.recordInvocation({
|
|
666
|
-
cost: advisorUsageCost(usage),
|
|
667
|
-
executionEffect: "continued",
|
|
668
|
-
kind: "markdown",
|
|
669
|
-
model: advisorRef,
|
|
670
|
-
trigger: "manual",
|
|
671
|
-
usage,
|
|
672
|
-
});
|
|
673
|
-
updateAdvisorUsageStatus(ctx);
|
|
674
|
-
const normalizedUsage = snapshotAdvisorUsage(usage);
|
|
675
|
-
pi.sendMessage(
|
|
676
|
-
{
|
|
677
|
-
content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${markdown}`,
|
|
678
|
-
customType: "advisor-manual-result",
|
|
679
|
-
details: {
|
|
680
|
-
advisor: advisorRef,
|
|
681
|
-
question,
|
|
682
|
-
text: markdown,
|
|
683
|
-
...(normalizedUsage ? { usage: normalizedUsage } : {}),
|
|
684
|
-
},
|
|
685
|
-
display: true,
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
// Steer lets the current turn finish its active work; the Executor sees
|
|
689
|
-
// the result before its next model call rather than being interrupted.
|
|
690
|
-
deliverAs: "steer",
|
|
691
|
-
triggerTurn: true,
|
|
692
|
-
}
|
|
693
|
-
);
|
|
694
|
-
})
|
|
695
|
-
.catch((error: unknown) => {
|
|
696
|
-
if (controller.signal.aborted) {
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
progress.phase = "error";
|
|
700
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
701
|
-
advisorSessionState.recordInvocation({
|
|
702
|
-
executionEffect: "continued",
|
|
703
|
-
failure: "provider-error",
|
|
704
|
-
kind: "markdown",
|
|
705
|
-
model: advisorRef,
|
|
706
|
-
trigger: "manual",
|
|
707
|
-
});
|
|
708
|
-
updateAdvisorUsageStatus(ctx);
|
|
709
|
-
pi.sendMessage(
|
|
710
|
-
{
|
|
711
|
-
content: `Manual Advisor consultation failed: ${message}`,
|
|
712
|
-
customType: "advisor-manual-result",
|
|
713
|
-
details: {
|
|
714
|
-
advisor: advisorRef,
|
|
715
|
-
text: `**Advisor consultation failed:** ${message}`,
|
|
716
|
-
},
|
|
717
|
-
display: true,
|
|
718
|
-
},
|
|
719
|
-
{ deliverAs: "steer", triggerTurn: true }
|
|
720
|
-
);
|
|
721
|
-
notify(ctx, `Advisor consultation failed: ${message}`, "error");
|
|
722
|
-
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
|
723
|
-
})
|
|
724
|
-
.finally(() => {
|
|
725
|
-
if (controller.signal.aborted) {
|
|
726
|
-
progress.phase = "cancelled";
|
|
727
|
-
}
|
|
728
|
-
const timer = manualProgressTimers.get(controller);
|
|
729
|
-
if (timer) {
|
|
730
|
-
clearInterval(timer);
|
|
731
|
-
manualProgressTimers.delete(controller);
|
|
732
|
-
}
|
|
733
|
-
requestManualRender(ctx);
|
|
734
|
-
scoutStatus.release(ctx, scoutStatusToken);
|
|
735
|
-
manualConsultations.delete(controller);
|
|
736
|
-
herdrAdvisorActivity.finish();
|
|
737
|
-
});
|
|
738
|
-
};
|
|
739
|
-
|
|
740
|
-
/** Resolves both models and their auth, or reports why activation cannot proceed. */
|
|
741
|
-
const resolveActivationModels = async (ctx: ExtensionContext) => {
|
|
742
|
-
const executor = findConfiguredModel(ctx, executorRef);
|
|
743
|
-
if (!executor) {
|
|
744
|
-
return {
|
|
745
|
-
error: executorRef
|
|
746
|
-
? `Executor model not found: ${executorRef}`
|
|
747
|
-
: "Executor model not configured",
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
const advisor = findConfiguredModel(ctx, advisorRef);
|
|
751
|
-
if (!advisor) {
|
|
752
|
-
return {
|
|
753
|
-
error: advisorRef
|
|
754
|
-
? `Advisor model not found: ${advisorRef}`
|
|
755
|
-
: "Advisor model not configured",
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
const advisorAuth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
|
|
759
|
-
if (!(advisorAuth.ok && advisorAuth.apiKey)) {
|
|
760
|
-
return { error: `No API key for Advisor ${advisorRef}` };
|
|
761
|
-
}
|
|
762
|
-
if (!(await setExecutorModel(executor))) {
|
|
763
|
-
return { error: `No API key for Executor ${executorRef}` };
|
|
764
|
-
}
|
|
765
|
-
return {};
|
|
766
|
-
};
|
|
767
|
-
|
|
768
|
-
const loadCommandConfig = (ctx: ExtensionContext) => {
|
|
769
|
-
try {
|
|
770
|
-
loadConfig(ctx);
|
|
771
|
-
return true;
|
|
772
|
-
} catch (error) {
|
|
773
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
774
|
-
notify(
|
|
775
|
-
ctx,
|
|
776
|
-
`Advisor command could not load configuration: ${message} Fix advisor.json and retry.`,
|
|
777
|
-
"error"
|
|
778
|
-
);
|
|
779
|
-
return false;
|
|
780
|
-
}
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
const prepareActivationModels = async (
|
|
784
|
-
ctx: ExtensionContext,
|
|
785
|
-
announce: boolean,
|
|
786
|
-
executorOverride: boolean,
|
|
787
|
-
advisorOverride: boolean
|
|
788
|
-
): Promise<
|
|
789
|
-
{ pickedModels: boolean; pendingExecutor?: string } | undefined
|
|
790
|
-
> => {
|
|
791
|
-
const persisted = getPersistedModelRefs();
|
|
792
|
-
const availableRefs = getAvailableModelRefs(ctx);
|
|
793
|
-
const availableRefSet = availableRefs ? new Set(availableRefs) : undefined;
|
|
794
|
-
const explicitError =
|
|
795
|
-
getExplicitModelError(
|
|
796
|
-
ctx,
|
|
797
|
-
executorRef,
|
|
798
|
-
"Executor",
|
|
799
|
-
executorOverride,
|
|
800
|
-
availableRefSet
|
|
801
|
-
) ??
|
|
802
|
-
getExplicitModelError(
|
|
803
|
-
ctx,
|
|
804
|
-
advisorRef,
|
|
805
|
-
"Advisor",
|
|
806
|
-
advisorOverride,
|
|
807
|
-
availableRefSet
|
|
808
|
-
);
|
|
809
|
-
if (explicitError) {
|
|
810
|
-
notify(ctx, explicitError, "error");
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const plan = planActivationModels(
|
|
815
|
-
ctx,
|
|
816
|
-
executorRef,
|
|
817
|
-
advisorRef,
|
|
818
|
-
pendingExecutorModelRef,
|
|
819
|
-
persisted,
|
|
820
|
-
executorOverride,
|
|
821
|
-
advisorOverride,
|
|
822
|
-
availableRefSet
|
|
823
|
-
);
|
|
824
|
-
setExecutorRef(plan.pendingExecutor ?? executorRef);
|
|
825
|
-
if (!(plan.selectExecutor || plan.selectAdvisor)) {
|
|
826
|
-
return { pendingExecutor: plan.pendingExecutor, pickedModels: false };
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Always-on startup cannot open an interactive picker, so it leaves the
|
|
830
|
-
// flow disabled until the user selects both models with `/advisor`.
|
|
831
|
-
if (!announce) {
|
|
832
|
-
notify(
|
|
833
|
-
ctx,
|
|
834
|
-
"Advisor models are not configured or available. Run /advisor to choose them.",
|
|
835
|
-
"error"
|
|
836
|
-
);
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
const selection = await selectAdvisorModels(ctx, {
|
|
840
|
-
advisor: advisorOverride || persisted.advisor ? advisorRef : "",
|
|
841
|
-
advisorEffort: advisorEffortRef,
|
|
842
|
-
executor:
|
|
843
|
-
executorOverride || plan.pendingExecutor || persisted.executor
|
|
844
|
-
? executorRef
|
|
845
|
-
: "",
|
|
846
|
-
executorEffort: executorEffortRef,
|
|
847
|
-
selectAdvisor: plan.selectAdvisor,
|
|
848
|
-
selectExecutor: plan.selectExecutor,
|
|
849
|
-
});
|
|
850
|
-
if (!selection) {
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
setAdvisorRef(selection.advisor);
|
|
854
|
-
setAdvisorEffortRef(selection.advisorEffort);
|
|
855
|
-
setExecutorRef(selection.executor);
|
|
856
|
-
setExecutorEffortRef(selection.executorEffort);
|
|
857
|
-
return { pendingExecutor: plan.pendingExecutor, pickedModels: true };
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
const activateAdvisor = async (
|
|
861
|
-
args: string,
|
|
862
|
-
ctx: ExtensionContext,
|
|
863
|
-
announce = true
|
|
864
|
-
) => {
|
|
865
|
-
if (!loadCommandConfig(ctx)) {
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
const previous = {
|
|
869
|
-
advisor: advisorRef,
|
|
870
|
-
advisorEffort: advisorEffortRef,
|
|
871
|
-
contextMaxChars: contextMaxCharsRef,
|
|
872
|
-
executor: executorRef,
|
|
873
|
-
executorEffort: executorEffortRef,
|
|
874
|
-
};
|
|
875
|
-
const restoreRefs = () => {
|
|
876
|
-
setAdvisorRef(previous.advisor);
|
|
877
|
-
setAdvisorEffortRef(previous.advisorEffort);
|
|
878
|
-
setContextMaxCharsRef(previous.contextMaxChars);
|
|
879
|
-
setExecutorRef(previous.executor);
|
|
880
|
-
setExecutorEffortRef(previous.executorEffort);
|
|
881
|
-
};
|
|
882
|
-
const executorOverride = hasExecutorOverride(args);
|
|
883
|
-
const advisorOverride = hasAdvisorOverride(args);
|
|
884
|
-
const argumentError = parseArgs(args);
|
|
885
|
-
if (argumentError) {
|
|
886
|
-
restoreRefs();
|
|
887
|
-
notify(ctx, argumentError, "error");
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
const prepared = await prepareActivationModels(
|
|
892
|
-
ctx,
|
|
893
|
-
announce,
|
|
894
|
-
executorOverride,
|
|
895
|
-
advisorOverride
|
|
896
|
-
);
|
|
897
|
-
if (!prepared) {
|
|
898
|
-
restoreRefs();
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
const { error } = await resolveActivationModels(ctx);
|
|
902
|
-
if (error) {
|
|
903
|
-
restoreRefs();
|
|
904
|
-
notify(ctx, error, "error");
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
// parseArgs, model picking, and an inactive `/model` selection only mutate
|
|
908
|
-
// in-memory refs. Persist them once both models are known and authenticated,
|
|
909
|
-
// so an unusable model reference is never written to the configuration.
|
|
910
|
-
if (args.trim() || prepared.pickedModels || prepared.pendingExecutor) {
|
|
911
|
-
saveConfig(ctx, { persistAdvisor: true, persistExecutor: true });
|
|
912
|
-
}
|
|
913
|
-
// A successful activation has committed the effective Executor. Do not let
|
|
914
|
-
// an older inactive selection override an explicit activation argument on a
|
|
915
|
-
// later attempt.
|
|
916
|
-
pendingExecutorModelRef = undefined;
|
|
917
|
-
if (executorEffortRef) {
|
|
918
|
-
pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
|
|
919
|
-
}
|
|
920
|
-
if (!flowEnabled()) {
|
|
921
|
-
pi.setActiveTools([
|
|
922
|
-
...pi.getActiveTools(),
|
|
923
|
-
"ask_advisor",
|
|
924
|
-
"record_advisor_outcome",
|
|
925
|
-
]);
|
|
926
|
-
}
|
|
927
|
-
if (announce) {
|
|
928
|
-
notify(
|
|
929
|
-
ctx,
|
|
930
|
-
`${ADVISOR_ACTIVATION_EXPLANATION}\n\nAdvisor flow ready — Executor: ${executorRef} (thinking: ${executorEffortRef || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`,
|
|
931
|
-
"info"
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
|
-
};
|
|
935
|
-
|
|
936
|
-
pi.registerEntryRenderer?.(
|
|
937
|
-
"advisor-manual-call",
|
|
938
|
-
(entry, { expanded }, theme) => {
|
|
939
|
-
const { progressId, question } = (entry.data ?? {}) as {
|
|
940
|
-
progressId?: string;
|
|
941
|
-
question?: string;
|
|
942
|
-
};
|
|
943
|
-
const progress = progressId ? manualProgress.get(progressId) : undefined;
|
|
944
|
-
return progress
|
|
945
|
-
? new ManualAdvisorProgressComponent(
|
|
946
|
-
question,
|
|
947
|
-
progress,
|
|
948
|
-
Boolean(expanded),
|
|
949
|
-
theme
|
|
950
|
-
)
|
|
951
|
-
: renderAdvisorCallBox(question, theme);
|
|
952
|
-
}
|
|
953
|
-
);
|
|
954
|
-
|
|
955
|
-
pi.registerMessageRenderer?.(
|
|
956
|
-
"advisor-manual-result",
|
|
957
|
-
(message, { expanded }, theme) => {
|
|
958
|
-
const details = message.details as
|
|
959
|
-
| { advisor?: string; text?: string; usage?: unknown }
|
|
960
|
-
| undefined;
|
|
961
|
-
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
962
|
-
const advice =
|
|
963
|
-
details?.text ??
|
|
964
|
-
(typeof message.content === "string"
|
|
965
|
-
? message.content
|
|
966
|
-
: "(Advisor returned no advice.)");
|
|
967
|
-
// Manual consultations must render exactly like an Executor ask_advisor call.
|
|
968
|
-
box.addChild(
|
|
969
|
-
new Text(
|
|
970
|
-
renderAdvisorResponseHeader(hasSoundVerdict(advice), theme),
|
|
971
|
-
0,
|
|
972
|
-
0
|
|
973
|
-
)
|
|
974
|
-
);
|
|
975
|
-
if (details?.advisor) {
|
|
976
|
-
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
977
|
-
}
|
|
978
|
-
if (getAdvisorSettings().showUsageDetails) {
|
|
979
|
-
const usage = formatAdvisorUsage(details?.usage);
|
|
980
|
-
if (usage) {
|
|
981
|
-
box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
box.addChild(
|
|
985
|
-
new Markdown(
|
|
986
|
-
adviceForDisplay(advice, expanded),
|
|
987
|
-
0,
|
|
988
|
-
0,
|
|
989
|
-
getMarkdownTheme()
|
|
990
|
-
)
|
|
991
|
-
);
|
|
992
|
-
return box;
|
|
993
|
-
}
|
|
994
|
-
);
|
|
995
|
-
|
|
996
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
997
|
-
pendingExecutorModelRef = undefined;
|
|
998
|
-
// A malformed advisor.json or a provider auth failure must not reject a
|
|
999
|
-
// lifecycle handler and break session startup.
|
|
1000
|
-
try {
|
|
1001
|
-
loadConfig(ctx);
|
|
1002
|
-
if (alwaysOnRef) {
|
|
1003
|
-
await activateAdvisor("", ctx, false);
|
|
1004
|
-
}
|
|
1005
|
-
} catch (error) {
|
|
1006
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1007
|
-
notify(ctx, `Advisor activation failed: ${message}`, "error");
|
|
1008
|
-
}
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
pi.on("model_select", (event, ctx) => {
|
|
1012
|
-
// "restore" replays a stored session model and "cycle" changes the active
|
|
1013
|
-
// model without an explicit `/model` choice. Neither should redefine the
|
|
1014
|
-
// configured Executor.
|
|
1015
|
-
if (event.source !== "set" || suppressModelSelectionSync) {
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
const selected = `${event.model.provider}/${event.model.id}`;
|
|
1019
|
-
if (!flowEnabled()) {
|
|
1020
|
-
// Defer persistence until `/advisor` succeeds. This keeps ordinary model
|
|
1021
|
-
// selection global defaults untouched when the flow is not enabled.
|
|
1022
|
-
pendingExecutorModelRef = selected;
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
pendingExecutorModelRef = undefined;
|
|
1026
|
-
if (selected === executorRef) {
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
const persisted = getPersistedModelRefs();
|
|
1030
|
-
setExecutorRef(selected);
|
|
1031
|
-
saveConfig(ctx, {
|
|
1032
|
-
persistAdvisor: Boolean(persisted.advisor),
|
|
1033
|
-
persistExecutor: true,
|
|
1034
|
-
});
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
pi.on("session_shutdown", (_event, ctx) => {
|
|
1038
|
-
if (ctx.hasUI) {
|
|
1039
|
-
ctx.ui.setStatus("advisor-usage", undefined);
|
|
1040
|
-
}
|
|
1041
|
-
for (const [controller, token] of manualConsultations) {
|
|
1042
|
-
controller.abort();
|
|
1043
|
-
const timer = manualProgressTimers.get(controller);
|
|
1044
|
-
if (timer) {
|
|
1045
|
-
clearInterval(timer);
|
|
1046
|
-
manualProgressTimers.delete(controller);
|
|
1047
|
-
}
|
|
1048
|
-
scoutStatus.release(ctx, token);
|
|
1049
|
-
}
|
|
1050
|
-
scoutStatus.clear(ctx);
|
|
1051
|
-
manualConsultations.clear();
|
|
1052
|
-
manualProgressTimers.clear();
|
|
1053
|
-
manualProgress.clear();
|
|
1054
|
-
herdrAdvisorActivity.clear();
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
pi.registerCommand("advisor-manual", {
|
|
1058
|
-
description:
|
|
1059
|
-
"Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
|
|
1060
|
-
handler: async (args, ctx) => {
|
|
1061
|
-
if (!loadCommandConfig(ctx)) {
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
if (
|
|
1065
|
-
!(
|
|
1066
|
-
isSimpleMode() ||
|
|
1067
|
-
advisorSessionState.canConsult(getAdvisorMaxCallsPerSession())
|
|
1068
|
-
)
|
|
1069
|
-
) {
|
|
1070
|
-
reportManualBudgetExhausted(ctx);
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
let gitContext: GitContextLevel | undefined;
|
|
1075
|
-
let question: string | undefined;
|
|
1076
|
-
if (ctx.mode === "tui") {
|
|
1077
|
-
const request = await ctx.ui.custom<ManualAdvisorRequest | undefined>(
|
|
1078
|
-
(tui, theme, keybindings, done) =>
|
|
1079
|
-
new ManualAdvisorDialog({
|
|
1080
|
-
gitContext: getAdvisorSettings().gitContext,
|
|
1081
|
-
initialMessage: args,
|
|
1082
|
-
keybindings,
|
|
1083
|
-
onCancel: () => done(undefined),
|
|
1084
|
-
onSubmit: done,
|
|
1085
|
-
theme,
|
|
1086
|
-
tui,
|
|
1087
|
-
}),
|
|
1088
|
-
{
|
|
1089
|
-
overlay: true,
|
|
1090
|
-
overlayOptions: {
|
|
1091
|
-
anchor: "center",
|
|
1092
|
-
margin: 2,
|
|
1093
|
-
maxHeight: "80%",
|
|
1094
|
-
minWidth: 56,
|
|
1095
|
-
width: 76,
|
|
1096
|
-
},
|
|
1097
|
-
}
|
|
1098
|
-
);
|
|
1099
|
-
if (!request) {
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
if (
|
|
1103
|
-
!(
|
|
1104
|
-
isSimpleMode() ||
|
|
1105
|
-
advisorSessionState.canConsult(getAdvisorMaxCallsPerSession())
|
|
1106
|
-
)
|
|
1107
|
-
) {
|
|
1108
|
-
reportManualBudgetExhausted(ctx);
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
const { gitContext: selectedGitContext, message } = request;
|
|
1112
|
-
gitContext = selectedGitContext;
|
|
1113
|
-
question = resolveAdvisorRequest(message);
|
|
1114
|
-
} else {
|
|
1115
|
-
question = resolveAdvisorRequest(args);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
if (!isSimpleMode()) {
|
|
1119
|
-
advisorSessionState.consumeCall();
|
|
1120
|
-
}
|
|
1121
|
-
// A single visible progress surface avoids competing consultations overwriting
|
|
1122
|
-
// each other's streamed state. A newer manual request replaces the previous one.
|
|
1123
|
-
for (const [pending, token] of manualConsultations) {
|
|
1124
|
-
pending.abort();
|
|
1125
|
-
scoutStatus.release(ctx, token);
|
|
1126
|
-
}
|
|
1127
|
-
manualConsultations.clear();
|
|
1128
|
-
const controller = new AbortController();
|
|
1129
|
-
const scoutStatusToken = Symbol("manual-scout");
|
|
1130
|
-
manualProgressSequence += 1;
|
|
1131
|
-
const progressId = `manual-${manualProgressSequence}`;
|
|
1132
|
-
const progress: ManualAdvisorProgressState = { phase: "preparing" };
|
|
1133
|
-
manualProgress.set(progressId, progress);
|
|
1134
|
-
scoutStatus.register(scoutStatusToken);
|
|
1135
|
-
manualConsultations.set(controller, scoutStatusToken);
|
|
1136
|
-
pi.appendEntry?.("advisor-manual-call", { progressId, question });
|
|
1137
|
-
startManualConsultation(
|
|
1138
|
-
ctx,
|
|
1139
|
-
question,
|
|
1140
|
-
controller,
|
|
1141
|
-
scoutStatusToken,
|
|
1142
|
-
progress,
|
|
1143
|
-
gitContext
|
|
1144
|
-
);
|
|
1145
|
-
},
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
pi.registerCommand("advisor", {
|
|
1149
|
-
description:
|
|
1150
|
-
"Enable the Executor/Advisor flow and switch to the configured or explicitly selected Executor model; accepts contextMaxChars=N",
|
|
1151
|
-
handler: (args, ctx) => activateAdvisor(args, ctx),
|
|
1152
|
-
});
|
|
1153
|
-
|
|
1154
|
-
pi.registerCommand("advisor-models", {
|
|
1155
|
-
description:
|
|
1156
|
-
"Select and persist the Executor and Advisor models with reasoning levels",
|
|
1157
|
-
handler: async (_args, ctx) => {
|
|
1158
|
-
if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
// When `/model` was used before activation, show that session choice as
|
|
1162
|
-
// the Executor's current option instead of making the persisted Executor
|
|
1163
|
-
// look like the active selection.
|
|
1164
|
-
const persisted = getPersistedModelRefs();
|
|
1165
|
-
const selection = await selectAdvisorModels(ctx, {
|
|
1166
|
-
advisor: persisted.advisor ? advisorRef : "",
|
|
1167
|
-
advisorEffort: advisorEffortRef,
|
|
1168
|
-
executor:
|
|
1169
|
-
pendingExecutorModelRef ?? (persisted.executor ? executorRef : ""),
|
|
1170
|
-
executorEffort: executorEffortRef,
|
|
1171
|
-
selectAdvisor: true,
|
|
1172
|
-
selectExecutor: true,
|
|
1173
|
-
});
|
|
1174
|
-
if (!selection) {
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
setExecutorRef(selection.executor);
|
|
1179
|
-
setAdvisorRef(selection.advisor);
|
|
1180
|
-
setExecutorEffortRef(selection.executorEffort);
|
|
1181
|
-
setAdvisorEffortRef(selection.advisorEffort);
|
|
1182
|
-
|
|
1183
|
-
const path = saveConfig(ctx, {
|
|
1184
|
-
persistAdvisor: true,
|
|
1185
|
-
persistExecutor: true,
|
|
1186
|
-
});
|
|
1187
|
-
pendingExecutorModelRef = undefined;
|
|
1188
|
-
ctx.ui.notify(
|
|
1189
|
-
`Saved Executor + Advisor configurations to ${path}`,
|
|
1190
|
-
"info"
|
|
1191
|
-
);
|
|
1192
|
-
},
|
|
1193
|
-
});
|
|
1194
|
-
|
|
1195
|
-
pi.registerCommand("advisor-settings", {
|
|
1196
|
-
description: "Configure Advisor settings",
|
|
1197
|
-
handler: async (_args, ctx) => {
|
|
1198
|
-
if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
const initial: AdvisorSettings = getAdvisorSettings();
|
|
1203
|
-
await ctx.ui.custom<void>(
|
|
1204
|
-
(tui, theme, _keybindings, done) =>
|
|
1205
|
-
new AdvisorSettingsSelector({
|
|
1206
|
-
effortLevels: EFFORT_LEVELS,
|
|
1207
|
-
initial,
|
|
1208
|
-
onCancel: () => done(),
|
|
1209
|
-
onChange: (settings) => {
|
|
1210
|
-
try {
|
|
1211
|
-
saveAdvisorSettings(ctx, settings);
|
|
1212
|
-
updateAdvisorUsageStatus(ctx);
|
|
1213
|
-
} catch (error) {
|
|
1214
|
-
const message =
|
|
1215
|
-
error instanceof Error ? error.message : String(error);
|
|
1216
|
-
ctx.ui.notify(
|
|
1217
|
-
`Could not save Advisor settings: ${message}`,
|
|
1218
|
-
"error"
|
|
1219
|
-
);
|
|
1220
|
-
}
|
|
1221
|
-
},
|
|
1222
|
-
presets: CONTEXT_PRESETS,
|
|
1223
|
-
theme,
|
|
1224
|
-
tui,
|
|
1225
|
-
})
|
|
1226
|
-
);
|
|
1227
|
-
},
|
|
1228
|
-
});
|
|
1229
|
-
|
|
1230
|
-
pi.registerCommand("advisor-off", {
|
|
1231
|
-
description: "Disable on-demand Advisor calls; keep the current model",
|
|
1232
|
-
handler: (_args, ctx) => {
|
|
1233
|
-
pi.setActiveTools(
|
|
1234
|
-
pi
|
|
1235
|
-
.getActiveTools()
|
|
1236
|
-
.filter(
|
|
1237
|
-
(name) =>
|
|
1238
|
-
name !== "ask_advisor" && name !== "record_advisor_outcome"
|
|
1239
|
-
)
|
|
1240
|
-
);
|
|
1241
|
-
// Leaving alwaysOn set would silently reactivate the flow next session.
|
|
1242
|
-
const wasAlwaysOn = alwaysOnRef;
|
|
1243
|
-
if (wasAlwaysOn) {
|
|
1244
|
-
const persisted = getPersistedModelRefs();
|
|
1245
|
-
setAlwaysOnRef(false);
|
|
1246
|
-
saveConfig(ctx, {
|
|
1247
|
-
persistAdvisor: Boolean(persisted.advisor),
|
|
1248
|
-
persistExecutor: Boolean(persisted.executor),
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
notify(
|
|
1252
|
-
ctx,
|
|
1253
|
-
`Advisor flow disabled. Current model unchanged.${wasAlwaysOn ? " Always on turned off." : ""}`,
|
|
1254
|
-
"info"
|
|
1255
|
-
);
|
|
1256
|
-
return Promise.resolve();
|
|
1257
|
-
},
|
|
1258
|
-
});
|
|
1259
|
-
};
|
|
1
|
+
// biome-ignore lint/performance/noBarrelFile: this facade intentionally preserves the public command module contract.
|
|
2
|
+
export { registerCommands } from "./commands/registration.js";
|
|
3
|
+
export type {
|
|
4
|
+
CommandDependencies,
|
|
5
|
+
ManualAdvisorProgressPhase,
|
|
6
|
+
ManualAdvisorProgressState,
|
|
7
|
+
ManualConsult,
|
|
8
|
+
ThinkingLevel,
|
|
9
|
+
} from "./commands/types.js";
|