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.
Files changed (59) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +3 -2
  3. package/extensions/index.ts +7 -7
  4. package/package.json +2 -1
  5. package/src/commands/activation-preparation.ts +117 -0
  6. package/src/commands/activation.ts +135 -0
  7. package/src/commands/lifecycle.ts +83 -0
  8. package/src/commands/manual-command.ts +107 -0
  9. package/src/commands/manual-consultation.ts +144 -0
  10. package/src/commands/manual-progress.ts +93 -0
  11. package/src/commands/model-commands.ts +66 -0
  12. package/src/commands/model-options.ts +181 -0
  13. package/src/commands/model-picker.ts +110 -0
  14. package/src/commands/registration.ts +27 -0
  15. package/src/commands/renderers.ts +76 -0
  16. package/src/commands/runtime.ts +114 -0
  17. package/src/commands/settings-commands.ts +80 -0
  18. package/src/commands/settings-persistence.ts +81 -0
  19. package/src/commands/types.ts +78 -0
  20. package/src/commands.ts +9 -1259
  21. package/src/config/args.ts +37 -0
  22. package/src/config/defaults.ts +190 -0
  23. package/src/config/state.ts +199 -0
  24. package/src/config/storage.ts +224 -0
  25. package/src/config/types.ts +63 -0
  26. package/src/config/validation.ts +234 -0
  27. package/src/config.ts +104 -814
  28. package/src/conversation.ts +4 -2
  29. package/src/herdr.ts +1 -1
  30. package/src/model-stream.ts +15 -1
  31. package/src/scout-context.ts +2 -2
  32. package/src/scout.ts +1 -1
  33. package/src/tools/consultation.ts +337 -0
  34. package/src/tools/gate-policy.ts +132 -0
  35. package/src/tools/gate-protocol.ts +127 -0
  36. package/src/tools/loop-gate.ts +210 -0
  37. package/src/tools/prompts.ts +159 -0
  38. package/src/tools/register-ask-advisor.ts +247 -0
  39. package/src/tools/register-lifecycle.ts +119 -0
  40. package/src/tools/register-outcome.ts +75 -0
  41. package/src/tools/register-renderers.ts +88 -0
  42. package/src/tools/registration.ts +35 -0
  43. package/src/tools/render-advisor-result.ts +168 -0
  44. package/src/tools/render-common.ts +120 -0
  45. package/src/tools/scout-status.ts +206 -0
  46. package/src/tools/session.ts +3 -0
  47. package/src/tools/types.ts +119 -0
  48. package/src/tools.ts +42 -1865
  49. package/src/ui/manual-dialog-render.ts +150 -0
  50. package/src/ui/manual-dialog.ts +305 -0
  51. package/src/ui/model-selector.ts +166 -0
  52. package/src/ui/settings-formatting.ts +133 -0
  53. package/src/ui/settings-items.ts +323 -0
  54. package/src/ui/settings-list-adapter.ts +103 -0
  55. package/src/ui/settings-mutations.ts +80 -0
  56. package/src/ui/settings-selector.ts +161 -0
  57. package/src/ui/text-setting-submenu.ts +66 -0
  58. package/src/ui/types.ts +111 -0
  59. package/src/ui.ts +9 -1418
package/src/commands.ts CHANGED
@@ -1,1259 +1,9 @@
1
- import {
2
- type ExtensionAPI,
3
- type ExtensionContext,
4
- getMarkdownTheme,
5
- type Theme,
6
- } from "@earendil-works/pi-coding-agent";
7
- import { Box, type Component, Markdown, Text } from "@earendil-works/pi-tui";
8
- import {
9
- advisorEffortRef,
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";