pi-advisor-flow 0.4.0 → 0.5.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/CHANGELOG.md +63 -0
- package/README.md +36 -68
- package/package.json +23 -11
- package/src/commands.ts +711 -184
- package/src/config.ts +54 -9
- package/src/model-stream.ts +133 -1
- package/src/session-state.ts +26 -2
- package/src/tools.ts +195 -66
- package/src/ui.ts +1060 -600
package/src/commands.ts
CHANGED
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
type ExtensionAPI,
|
|
3
3
|
type ExtensionContext,
|
|
4
4
|
getMarkdownTheme,
|
|
5
|
+
type Theme,
|
|
5
6
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
7
|
+
import { Box, type Component, Markdown, Text } from "@earendil-works/pi-tui";
|
|
7
8
|
import {
|
|
8
9
|
advisorEffortRef,
|
|
9
10
|
advisorRef,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
executorRef,
|
|
14
15
|
getAdvisorMaxCallsPerSession,
|
|
15
16
|
getAdvisorSettings,
|
|
17
|
+
getPersistedModelRefs,
|
|
16
18
|
isSimpleMode,
|
|
17
19
|
loadConfig,
|
|
18
20
|
parseArgs,
|
|
@@ -46,9 +48,12 @@ import {
|
|
|
46
48
|
setContextMaxCharsRef,
|
|
47
49
|
setExecutorEffortRef,
|
|
48
50
|
setExecutorRef,
|
|
51
|
+
setShowUsageDetailsRef,
|
|
52
|
+
setShowUsageFooterRef,
|
|
49
53
|
setSimpleModeRef,
|
|
50
54
|
splitRef,
|
|
51
55
|
} from "./config.js";
|
|
56
|
+
import type { GitContextLevel } from "./git.js";
|
|
52
57
|
import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "./herdr.js";
|
|
53
58
|
import type { ScoutLifecycleEvent } from "./scout.js";
|
|
54
59
|
import type { AdvisorSessionState } from "./session-state.js";
|
|
@@ -60,13 +65,19 @@ import {
|
|
|
60
65
|
hasSoundVerdict,
|
|
61
66
|
renderAdvisorCallBox,
|
|
62
67
|
renderAdvisorResponseHeader,
|
|
68
|
+
renderScoutDetails,
|
|
69
|
+
renderThinkingMarkdown,
|
|
63
70
|
resolveAdvisorRequest,
|
|
64
71
|
ScoutStatusManager,
|
|
72
|
+
type ScoutToolDetails,
|
|
73
|
+
SPINNER_FRAMES,
|
|
65
74
|
} from "./tools.js";
|
|
66
75
|
import {
|
|
67
76
|
type AdvisorSettings,
|
|
68
77
|
AdvisorSettingsSelector,
|
|
69
78
|
type ContextPreset,
|
|
79
|
+
ManualAdvisorDialog,
|
|
80
|
+
type ManualAdvisorRequest,
|
|
70
81
|
SearchableModelSelector,
|
|
71
82
|
} from "./ui.js";
|
|
72
83
|
import {
|
|
@@ -102,6 +113,20 @@ const selectedEffort = (choice: string): string | undefined => {
|
|
|
102
113
|
: choice;
|
|
103
114
|
return effort === DEFAULT_EFFORT_LEVEL ? undefined : effort;
|
|
104
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");
|
|
105
130
|
|
|
106
131
|
const CONTEXT_PRESETS: ContextPreset[] = [
|
|
107
132
|
{
|
|
@@ -143,7 +168,8 @@ type ManualConsult = (
|
|
|
143
168
|
question?: string,
|
|
144
169
|
signal?: AbortSignal,
|
|
145
170
|
onChunk?: (thinking: string, text: string) => void,
|
|
146
|
-
onScout?: (event: ScoutLifecycleEvent) => void
|
|
171
|
+
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
172
|
+
gitContext?: GitContextLevel
|
|
147
173
|
) => Promise<{
|
|
148
174
|
markdown: string;
|
|
149
175
|
thinkingText: string;
|
|
@@ -163,11 +189,348 @@ const notify = (
|
|
|
163
189
|
}
|
|
164
190
|
};
|
|
165
191
|
|
|
166
|
-
|
|
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
|
+
}
|
|
167
295
|
const [provider, modelId] = splitRef(ref);
|
|
168
296
|
return ctx.modelRegistry.find(provider, modelId);
|
|
169
297
|
};
|
|
170
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
|
+
|
|
171
534
|
export const registerCommands = (
|
|
172
535
|
pi: ExtensionAPI,
|
|
173
536
|
dependencies: {
|
|
@@ -178,42 +541,70 @@ export const registerCommands = (
|
|
|
178
541
|
) => {
|
|
179
542
|
const advisorSessionState =
|
|
180
543
|
dependencies.sessionState ?? defaultAdvisorSessionState;
|
|
181
|
-
const scoutStatus =
|
|
544
|
+
const scoutStatus =
|
|
545
|
+
dependencies.statusManager ?? new ScoutStatusManager(false);
|
|
182
546
|
const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
|
|
183
547
|
const requestAdvisor =
|
|
184
548
|
dependencies.consult ??
|
|
185
|
-
((ctx, question, signal, onChunk, onScout) =>
|
|
549
|
+
((ctx, question, signal, onChunk, onScout, gitContext) =>
|
|
186
550
|
consultAdvisor(
|
|
187
551
|
ctx,
|
|
188
552
|
question,
|
|
189
553
|
signal,
|
|
190
554
|
onChunk,
|
|
191
555
|
"manual",
|
|
192
|
-
|
|
556
|
+
gitContext,
|
|
193
557
|
undefined,
|
|
194
558
|
undefined,
|
|
195
559
|
undefined,
|
|
196
560
|
onScout,
|
|
197
561
|
undefined
|
|
198
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
|
+
};
|
|
199
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;
|
|
200
586
|
const updateAdvisorUsageStatus = (ctx: ExtensionContext) => {
|
|
201
587
|
if (ctx.hasUI) {
|
|
202
|
-
ctx.ui.setStatus(
|
|
588
|
+
ctx.ui.setStatus(
|
|
589
|
+
"advisor-usage",
|
|
590
|
+
getAdvisorSettings().showUsageFooter
|
|
591
|
+
? advisorSessionState.usageStatus()
|
|
592
|
+
: undefined
|
|
593
|
+
);
|
|
203
594
|
}
|
|
204
595
|
};
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
) {
|
|
216
|
-
ctx.ui.setStatus("advisor-manual",
|
|
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);
|
|
217
608
|
}
|
|
218
609
|
};
|
|
219
610
|
|
|
@@ -221,10 +612,26 @@ export const registerCommands = (
|
|
|
221
612
|
ctx: ExtensionContext,
|
|
222
613
|
question: string | undefined,
|
|
223
614
|
controller: AbortController,
|
|
224
|
-
scoutStatusToken: symbol
|
|
615
|
+
scoutStatusToken: symbol,
|
|
616
|
+
progress: ManualAdvisorProgressState,
|
|
617
|
+
gitContext?: GitContextLevel
|
|
225
618
|
) => {
|
|
226
619
|
herdrAdvisorActivity.start();
|
|
227
|
-
|
|
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
|
+
}
|
|
228
635
|
let scoutDetails: Parameters<typeof appendScoutLifecycleEntry>[2];
|
|
229
636
|
return requestAdvisor(
|
|
230
637
|
ctx,
|
|
@@ -234,41 +641,27 @@ export const registerCommands = (
|
|
|
234
641
|
if (controller.signal.aborted) {
|
|
235
642
|
return;
|
|
236
643
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (text.trim()) {
|
|
242
|
-
status = "Advisor responding…";
|
|
243
|
-
}
|
|
244
|
-
setManualStatus(ctx, controller, scoutStatusToken, status);
|
|
644
|
+
progress.phase = "active";
|
|
645
|
+
progress.thinking = thinking;
|
|
646
|
+
progress.text = text;
|
|
647
|
+
requestManualRender(ctx);
|
|
245
648
|
},
|
|
246
649
|
(event) => {
|
|
247
650
|
if (!controller.signal.aborted) {
|
|
248
651
|
scoutStatus.update(ctx, scoutStatusToken, event);
|
|
249
|
-
if (event.type === "call" || event.type === "chunk") {
|
|
250
|
-
setManualStatus(
|
|
251
|
-
ctx,
|
|
252
|
-
controller,
|
|
253
|
-
scoutStatusToken,
|
|
254
|
-
"Advisor Scout curating…"
|
|
255
|
-
);
|
|
256
|
-
} else if (event.type === "success" || event.type === "fallback") {
|
|
257
|
-
setManualStatus(
|
|
258
|
-
ctx,
|
|
259
|
-
controller,
|
|
260
|
-
scoutStatusToken,
|
|
261
|
-
"Advisor working…"
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
652
|
scoutDetails = appendScoutLifecycleEntry(pi, event, scoutDetails);
|
|
653
|
+
progress.scout = scoutDetails;
|
|
654
|
+
progress.phase = "active";
|
|
655
|
+
requestManualRender(ctx);
|
|
265
656
|
}
|
|
266
|
-
}
|
|
657
|
+
},
|
|
658
|
+
gitContext
|
|
267
659
|
)
|
|
268
660
|
.then(({ markdown, usage }) => {
|
|
269
661
|
if (controller.signal.aborted) {
|
|
270
662
|
return;
|
|
271
663
|
}
|
|
664
|
+
progress.phase = "complete";
|
|
272
665
|
advisorSessionState.recordInvocation({
|
|
273
666
|
cost: advisorUsageCost(usage),
|
|
274
667
|
executionEffect: "continued",
|
|
@@ -303,6 +696,7 @@ export const registerCommands = (
|
|
|
303
696
|
if (controller.signal.aborted) {
|
|
304
697
|
return;
|
|
305
698
|
}
|
|
699
|
+
progress.phase = "error";
|
|
306
700
|
const message = error instanceof Error ? error.message : String(error);
|
|
307
701
|
advisorSessionState.recordInvocation({
|
|
308
702
|
executionEffect: "continued",
|
|
@@ -328,12 +722,15 @@ export const registerCommands = (
|
|
|
328
722
|
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
|
329
723
|
})
|
|
330
724
|
.finally(() => {
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
)
|
|
335
|
-
|
|
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);
|
|
336
732
|
}
|
|
733
|
+
requestManualRender(ctx);
|
|
337
734
|
scoutStatus.release(ctx, scoutStatusToken);
|
|
338
735
|
manualConsultations.delete(controller);
|
|
339
736
|
herdrAdvisorActivity.finish();
|
|
@@ -344,17 +741,25 @@ export const registerCommands = (
|
|
|
344
741
|
const resolveActivationModels = async (ctx: ExtensionContext) => {
|
|
345
742
|
const executor = findConfiguredModel(ctx, executorRef);
|
|
346
743
|
if (!executor) {
|
|
347
|
-
return {
|
|
744
|
+
return {
|
|
745
|
+
error: executorRef
|
|
746
|
+
? `Executor model not found: ${executorRef}`
|
|
747
|
+
: "Executor model not configured",
|
|
748
|
+
};
|
|
348
749
|
}
|
|
349
750
|
const advisor = findConfiguredModel(ctx, advisorRef);
|
|
350
751
|
if (!advisor) {
|
|
351
|
-
return {
|
|
752
|
+
return {
|
|
753
|
+
error: advisorRef
|
|
754
|
+
? `Advisor model not found: ${advisorRef}`
|
|
755
|
+
: "Advisor model not configured",
|
|
756
|
+
};
|
|
352
757
|
}
|
|
353
758
|
const advisorAuth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
|
|
354
759
|
if (!(advisorAuth.ok && advisorAuth.apiKey)) {
|
|
355
760
|
return { error: `No API key for Advisor ${advisorRef}` };
|
|
356
761
|
}
|
|
357
|
-
if (!(await
|
|
762
|
+
if (!(await setExecutorModel(executor))) {
|
|
358
763
|
return { error: `No API key for Executor ${executorRef}` };
|
|
359
764
|
}
|
|
360
765
|
return {};
|
|
@@ -375,6 +780,83 @@ export const registerCommands = (
|
|
|
375
780
|
}
|
|
376
781
|
};
|
|
377
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
|
+
|
|
378
860
|
const activateAdvisor = async (
|
|
379
861
|
args: string,
|
|
380
862
|
ctx: ExtensionContext,
|
|
@@ -385,32 +867,53 @@ export const registerCommands = (
|
|
|
385
867
|
}
|
|
386
868
|
const previous = {
|
|
387
869
|
advisor: advisorRef,
|
|
870
|
+
advisorEffort: advisorEffortRef,
|
|
388
871
|
contextMaxChars: contextMaxCharsRef,
|
|
389
872
|
executor: executorRef,
|
|
873
|
+
executorEffort: executorEffortRef,
|
|
390
874
|
};
|
|
391
875
|
const restoreRefs = () => {
|
|
392
876
|
setAdvisorRef(previous.advisor);
|
|
877
|
+
setAdvisorEffortRef(previous.advisorEffort);
|
|
393
878
|
setContextMaxCharsRef(previous.contextMaxChars);
|
|
394
879
|
setExecutorRef(previous.executor);
|
|
880
|
+
setExecutorEffortRef(previous.executorEffort);
|
|
395
881
|
};
|
|
882
|
+
const executorOverride = hasExecutorOverride(args);
|
|
883
|
+
const advisorOverride = hasAdvisorOverride(args);
|
|
396
884
|
const argumentError = parseArgs(args);
|
|
397
885
|
if (argumentError) {
|
|
398
886
|
restoreRefs();
|
|
399
887
|
notify(ctx, argumentError, "error");
|
|
400
888
|
return;
|
|
401
889
|
}
|
|
890
|
+
|
|
891
|
+
const prepared = await prepareActivationModels(
|
|
892
|
+
ctx,
|
|
893
|
+
announce,
|
|
894
|
+
executorOverride,
|
|
895
|
+
advisorOverride
|
|
896
|
+
);
|
|
897
|
+
if (!prepared) {
|
|
898
|
+
restoreRefs();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
402
901
|
const { error } = await resolveActivationModels(ctx);
|
|
403
902
|
if (error) {
|
|
404
903
|
restoreRefs();
|
|
405
904
|
notify(ctx, error, "error");
|
|
406
905
|
return;
|
|
407
906
|
}
|
|
408
|
-
// parseArgs
|
|
409
|
-
//
|
|
907
|
+
// parseArgs, model picking, and an inactive `/model` selection only mutate
|
|
908
|
+
// in-memory refs. Persist them once both models are known and authenticated,
|
|
410
909
|
// so an unusable model reference is never written to the configuration.
|
|
411
|
-
if (args.trim()) {
|
|
412
|
-
saveConfig(ctx);
|
|
910
|
+
if (args.trim() || prepared.pickedModels || prepared.pendingExecutor) {
|
|
911
|
+
saveConfig(ctx, { persistAdvisor: true, persistExecutor: true });
|
|
413
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;
|
|
414
917
|
if (executorEffortRef) {
|
|
415
918
|
pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
|
|
416
919
|
}
|
|
@@ -424,7 +927,7 @@ export const registerCommands = (
|
|
|
424
927
|
if (announce) {
|
|
425
928
|
notify(
|
|
426
929
|
ctx,
|
|
427
|
-
|
|
930
|
+
`${ADVISOR_ACTIVATION_EXPLANATION}\n\nAdvisor flow ready — Executor: ${executorRef} (thinking: ${executorEffortRef || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`,
|
|
428
931
|
"info"
|
|
429
932
|
);
|
|
430
933
|
}
|
|
@@ -432,9 +935,20 @@ export const registerCommands = (
|
|
|
432
935
|
|
|
433
936
|
pi.registerEntryRenderer?.(
|
|
434
937
|
"advisor-manual-call",
|
|
435
|
-
(entry,
|
|
436
|
-
const { question } = (entry.data ?? {}) as {
|
|
437
|
-
|
|
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);
|
|
438
952
|
}
|
|
439
953
|
);
|
|
440
954
|
|
|
@@ -461,9 +975,11 @@ export const registerCommands = (
|
|
|
461
975
|
if (details?.advisor) {
|
|
462
976
|
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
463
977
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
}
|
|
467
983
|
}
|
|
468
984
|
box.addChild(
|
|
469
985
|
new Markdown(
|
|
@@ -478,6 +994,7 @@ export const registerCommands = (
|
|
|
478
994
|
);
|
|
479
995
|
|
|
480
996
|
pi.on("session_start", async (_event, ctx) => {
|
|
997
|
+
pendingExecutorModelRef = undefined;
|
|
481
998
|
// A malformed advisor.json or a provider auth failure must not reject a
|
|
482
999
|
// lifecycle handler and break session startup.
|
|
483
1000
|
try {
|
|
@@ -492,39 +1009,57 @@ export const registerCommands = (
|
|
|
492
1009
|
});
|
|
493
1010
|
|
|
494
1011
|
pi.on("model_select", (event, ctx) => {
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
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) {
|
|
498
1016
|
return;
|
|
499
1017
|
}
|
|
500
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;
|
|
501
1026
|
if (selected === executorRef) {
|
|
502
1027
|
return;
|
|
503
1028
|
}
|
|
1029
|
+
const persisted = getPersistedModelRefs();
|
|
504
1030
|
setExecutorRef(selected);
|
|
505
|
-
saveConfig(ctx
|
|
1031
|
+
saveConfig(ctx, {
|
|
1032
|
+
persistAdvisor: Boolean(persisted.advisor),
|
|
1033
|
+
persistExecutor: true,
|
|
1034
|
+
});
|
|
506
1035
|
});
|
|
507
1036
|
|
|
508
1037
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
509
1038
|
if (ctx.hasUI) {
|
|
510
|
-
ctx.ui.setStatus("advisor-manual", undefined);
|
|
511
1039
|
ctx.ui.setStatus("advisor-usage", undefined);
|
|
512
1040
|
}
|
|
513
1041
|
for (const [controller, token] of manualConsultations) {
|
|
514
1042
|
controller.abort();
|
|
1043
|
+
const timer = manualProgressTimers.get(controller);
|
|
1044
|
+
if (timer) {
|
|
1045
|
+
clearInterval(timer);
|
|
1046
|
+
manualProgressTimers.delete(controller);
|
|
1047
|
+
}
|
|
515
1048
|
scoutStatus.release(ctx, token);
|
|
516
1049
|
}
|
|
517
1050
|
scoutStatus.clear(ctx);
|
|
518
1051
|
manualConsultations.clear();
|
|
1052
|
+
manualProgressTimers.clear();
|
|
1053
|
+
manualProgress.clear();
|
|
519
1054
|
herdrAdvisorActivity.clear();
|
|
520
1055
|
});
|
|
521
1056
|
|
|
522
1057
|
pi.registerCommand("advisor-manual", {
|
|
523
1058
|
description:
|
|
524
1059
|
"Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
|
|
525
|
-
handler: (args, ctx) => {
|
|
1060
|
+
handler: async (args, ctx) => {
|
|
526
1061
|
if (!loadCommandConfig(ctx)) {
|
|
527
|
-
return
|
|
1062
|
+
return;
|
|
528
1063
|
}
|
|
529
1064
|
if (
|
|
530
1065
|
!(
|
|
@@ -532,15 +1067,57 @@ export const registerCommands = (
|
|
|
532
1067
|
advisorSessionState.canConsult(getAdvisorMaxCallsPerSession())
|
|
533
1068
|
)
|
|
534
1069
|
) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
|
|
538
|
-
return Promise.resolve();
|
|
1070
|
+
reportManualBudgetExhausted(ctx);
|
|
1071
|
+
return;
|
|
539
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
|
+
|
|
540
1118
|
if (!isSimpleMode()) {
|
|
541
1119
|
advisorSessionState.consumeCall();
|
|
542
1120
|
}
|
|
543
|
-
const question = resolveAdvisorRequest(args);
|
|
544
1121
|
// A single visible progress surface avoids competing consultations overwriting
|
|
545
1122
|
// each other's streamed state. A newer manual request replaces the previous one.
|
|
546
1123
|
for (const [pending, token] of manualConsultations) {
|
|
@@ -550,17 +1127,27 @@ export const registerCommands = (
|
|
|
550
1127
|
manualConsultations.clear();
|
|
551
1128
|
const controller = new AbortController();
|
|
552
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);
|
|
553
1134
|
scoutStatus.register(scoutStatusToken);
|
|
554
1135
|
manualConsultations.set(controller, scoutStatusToken);
|
|
555
|
-
pi.appendEntry?.("advisor-manual-call", { question });
|
|
556
|
-
startManualConsultation(
|
|
557
|
-
|
|
1136
|
+
pi.appendEntry?.("advisor-manual-call", { progressId, question });
|
|
1137
|
+
startManualConsultation(
|
|
1138
|
+
ctx,
|
|
1139
|
+
question,
|
|
1140
|
+
controller,
|
|
1141
|
+
scoutStatusToken,
|
|
1142
|
+
progress,
|
|
1143
|
+
gitContext
|
|
1144
|
+
);
|
|
558
1145
|
},
|
|
559
1146
|
});
|
|
560
1147
|
|
|
561
1148
|
pi.registerCommand("advisor", {
|
|
562
1149
|
description:
|
|
563
|
-
"Enable the Executor/Advisor flow and switch to the configured Executor model; accepts contextMaxChars=N",
|
|
1150
|
+
"Enable the Executor/Advisor flow and switch to the configured or explicitly selected Executor model; accepts contextMaxChars=N",
|
|
564
1151
|
handler: (args, ctx) => activateAdvisor(args, ctx),
|
|
565
1152
|
});
|
|
566
1153
|
|
|
@@ -571,66 +1158,33 @@ export const registerCommands = (
|
|
|
571
1158
|
if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
|
|
572
1159
|
return;
|
|
573
1160
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
tui,
|
|
589
|
-
})
|
|
590
|
-
);
|
|
591
|
-
if (!executor) {
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const executorEffort = await ctx.ui.select(
|
|
596
|
-
"Select Executor Reasoning/Thinking Level",
|
|
597
|
-
effortChoices(executorEffortRef)
|
|
598
|
-
);
|
|
599
|
-
if (!executorEffort) {
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const advisor = await ctx.ui.custom<string | undefined>(
|
|
604
|
-
(tui, theme, keybindings, done) =>
|
|
605
|
-
new SearchableModelSelector({
|
|
606
|
-
allOptions: refs,
|
|
607
|
-
currentOption: advisorRef,
|
|
608
|
-
keybindings,
|
|
609
|
-
onCancel: () => done(undefined),
|
|
610
|
-
onSelect: done,
|
|
611
|
-
theme,
|
|
612
|
-
title: "Select Advisor Model",
|
|
613
|
-
tui,
|
|
614
|
-
})
|
|
615
|
-
);
|
|
616
|
-
if (!advisor) {
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const advisorEffort = await ctx.ui.select(
|
|
621
|
-
"Select Advisor Reasoning/Thinking Level",
|
|
622
|
-
effortChoices(advisorEffortRef)
|
|
623
|
-
);
|
|
624
|
-
if (!advisorEffort) {
|
|
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) {
|
|
625
1175
|
return;
|
|
626
1176
|
}
|
|
627
1177
|
|
|
628
|
-
setExecutorRef(executor);
|
|
629
|
-
setAdvisorRef(advisor);
|
|
630
|
-
setExecutorEffortRef(
|
|
631
|
-
setAdvisorEffortRef(
|
|
1178
|
+
setExecutorRef(selection.executor);
|
|
1179
|
+
setAdvisorRef(selection.advisor);
|
|
1180
|
+
setExecutorEffortRef(selection.executorEffort);
|
|
1181
|
+
setAdvisorEffortRef(selection.advisorEffort);
|
|
632
1182
|
|
|
633
|
-
const path = saveConfig(ctx
|
|
1183
|
+
const path = saveConfig(ctx, {
|
|
1184
|
+
persistAdvisor: true,
|
|
1185
|
+
persistExecutor: true,
|
|
1186
|
+
});
|
|
1187
|
+
pendingExecutorModelRef = undefined;
|
|
634
1188
|
ctx.ui.notify(
|
|
635
1189
|
`Saved Executor + Advisor configurations to ${path}`,
|
|
636
1190
|
"info"
|
|
@@ -639,68 +1193,37 @@ export const registerCommands = (
|
|
|
639
1193
|
});
|
|
640
1194
|
|
|
641
1195
|
pi.registerCommand("advisor-settings", {
|
|
642
|
-
description: "Configure Advisor
|
|
643
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one settings form maps every persisted control.
|
|
1196
|
+
description: "Configure Advisor settings",
|
|
644
1197
|
handler: async (_args, ctx) => {
|
|
645
1198
|
if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
|
|
646
1199
|
return;
|
|
647
1200
|
}
|
|
648
1201
|
|
|
649
1202
|
const initial: AdvisorSettings = getAdvisorSettings();
|
|
650
|
-
|
|
1203
|
+
await ctx.ui.custom<void>(
|
|
651
1204
|
(tui, theme, _keybindings, done) =>
|
|
652
1205
|
new AdvisorSettingsSelector({
|
|
653
1206
|
effortLevels: EFFORT_LEVELS,
|
|
654
1207
|
initial,
|
|
655
|
-
onCancel: () => done(
|
|
656
|
-
|
|
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
|
+
},
|
|
657
1222
|
presets: CONTEXT_PRESETS,
|
|
658
1223
|
theme,
|
|
659
1224
|
tui,
|
|
660
1225
|
})
|
|
661
1226
|
);
|
|
662
|
-
if (!settings) {
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
setAdvisorEffortRef(
|
|
667
|
-
settings.effort === "Default (Model Default)"
|
|
668
|
-
? undefined
|
|
669
|
-
: settings.effort
|
|
670
|
-
);
|
|
671
|
-
setContextMaxCharsRef(settings.contextMaxChars);
|
|
672
|
-
setAdvisorPlanGateRef(settings.planGate);
|
|
673
|
-
setAdvisorFailureGateRef(settings.failureGate);
|
|
674
|
-
setAdvisorCompletionGateRef(settings.completionGate);
|
|
675
|
-
setAdvisorCollapseResponsesRef(settings.collapseResponses);
|
|
676
|
-
setAdvisorCustomInvocationRef(settings.customRule);
|
|
677
|
-
setAdvisorBlockOnBlockedRef(settings.blockOnBlocked ?? true);
|
|
678
|
-
setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
|
|
679
|
-
setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
|
|
680
|
-
setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
|
|
681
|
-
setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
|
|
682
|
-
setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
|
|
683
|
-
setSimpleModeRef(settings.simpleMode ?? false);
|
|
684
|
-
setAlwaysOnRef(settings.alwaysOn ?? false);
|
|
685
|
-
setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
|
|
686
|
-
setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
|
|
687
|
-
setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
|
|
688
|
-
setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
|
|
689
|
-
setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
|
|
690
|
-
setAdvisorGitContextRef(settings.gitContext ?? "summary");
|
|
691
|
-
setAdvisorGitContextMaxCharsRef(settings.gitContextMaxChars ?? 20_000);
|
|
692
|
-
setAdvisorToolPoliciesRef(settings.toolPolicies ?? {});
|
|
693
|
-
setAdvisorTrackedFileContentRef(settings.trackedFileContent ?? false);
|
|
694
|
-
setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
|
|
695
|
-
setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
|
|
696
|
-
const path = saveConfig(ctx);
|
|
697
|
-
const globalPath = saveGlobalOutcomeLogging(
|
|
698
|
-
settings.outcomeLogging ?? false
|
|
699
|
-
);
|
|
700
|
-
ctx.ui.notify(
|
|
701
|
-
`Saved Advisor settings to ${path}; outcome logging globally to ${globalPath}`,
|
|
702
|
-
"info"
|
|
703
|
-
);
|
|
704
1227
|
},
|
|
705
1228
|
});
|
|
706
1229
|
|
|
@@ -718,8 +1241,12 @@ export const registerCommands = (
|
|
|
718
1241
|
// Leaving alwaysOn set would silently reactivate the flow next session.
|
|
719
1242
|
const wasAlwaysOn = alwaysOnRef;
|
|
720
1243
|
if (wasAlwaysOn) {
|
|
1244
|
+
const persisted = getPersistedModelRefs();
|
|
721
1245
|
setAlwaysOnRef(false);
|
|
722
|
-
saveConfig(ctx
|
|
1246
|
+
saveConfig(ctx, {
|
|
1247
|
+
persistAdvisor: Boolean(persisted.advisor),
|
|
1248
|
+
persistExecutor: Boolean(persisted.executor),
|
|
1249
|
+
});
|
|
723
1250
|
}
|
|
724
1251
|
notify(
|
|
725
1252
|
ctx,
|