pi-advisor-flow 0.5.0 → 0.5.2

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 +54 -0
  2. package/README.md +37 -71
  3. package/extensions/index.ts +7 -7
  4. package/package.json +19 -6
  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 -946
  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 -791
  28. package/src/conversation.ts +4 -2
  29. package/src/herdr.ts +1 -1
  30. package/src/model-stream.ts +25 -2
  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 +225 -0
  39. package/src/tools/register-lifecycle.ts +89 -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 -1811
  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,946 +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
- isSimpleMode,
18
- loadConfig,
19
- parseArgs,
20
- saveConfig,
21
- saveGlobalOutcomeLogging,
22
- setAdvisorAutoLoopGateRef,
23
- setAdvisorBlockOnBlockedRef,
24
- setAdvisorCollapseResponsesRef,
25
- setAdvisorCompletionGateRef,
26
- setAdvisorCustomInvocationRef,
27
- setAdvisorEffortRef,
28
- setAdvisorFailureGateRef,
29
- setAdvisorFailureModeRef,
30
- setAdvisorGitContextMaxCharsRef,
31
- setAdvisorGitContextRef,
32
- setAdvisorHerdrIntegrationRef,
33
- setAdvisorLoopThresholdRef,
34
- setAdvisorMaxCallsPerSessionRef,
35
- setAdvisorOutcomeLoggingRef,
36
- setAdvisorPlanGateRef,
37
- setAdvisorRedactSecretsRef,
38
- setAdvisorRef,
39
- setAdvisorScoutEnabledRef,
40
- setAdvisorSessionSummaryRef,
41
- setAdvisorToolPoliciesRef,
42
- setAdvisorToolResultMaxBytesRef,
43
- setAdvisorToolResultMaxLinesRef,
44
- setAdvisorTrackedFileContentRef,
45
- setAdvisorUntrackedContentRef,
46
- setAlwaysOnRef,
47
- setContextMaxCharsRef,
48
- setExecutorEffortRef,
49
- setExecutorRef,
50
- setShowUsageDetailsRef,
51
- setShowUsageFooterRef,
52
- setSimpleModeRef,
53
- splitRef,
54
- } from "./config.js";
55
- import type { GitContextLevel } from "./git.js";
56
- import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "./herdr.js";
57
- import type { ScoutLifecycleEvent } from "./scout.js";
58
- import type { AdvisorSessionState } from "./session-state.js";
59
- import {
60
- adviceForDisplay,
61
- appendScoutLifecycleEntry,
62
- consultAdvisor,
63
- advisorSessionState as defaultAdvisorSessionState,
64
- hasSoundVerdict,
65
- renderAdvisorCallBox,
66
- renderAdvisorResponseHeader,
67
- renderScoutDetails,
68
- resolveAdvisorRequest,
69
- ScoutStatusManager,
70
- type ScoutToolDetails,
71
- SPINNER_FRAMES,
72
- } from "./tools.js";
73
- import {
74
- type AdvisorSettings,
75
- AdvisorSettingsSelector,
76
- type ContextPreset,
77
- ManualAdvisorDialog,
78
- type ManualAdvisorRequest,
79
- SearchableModelSelector,
80
- } from "./ui.js";
81
- import {
82
- advisorUsageCost,
83
- formatAdvisorUsage,
84
- snapshotAdvisorUsage,
85
- } from "./usage.js";
86
-
87
- const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
88
- const SELECTED_PREFIX = "✓ ";
89
- const EFFORT_LEVELS = [
90
- DEFAULT_EFFORT_LEVEL,
91
- "off",
92
- "minimal",
93
- "low",
94
- "medium",
95
- "high",
96
- "xhigh",
97
- "max",
98
- ];
99
-
100
- const effortChoices = (configured: string | undefined): string[] => {
101
- const current = configured ?? DEFAULT_EFFORT_LEVEL;
102
- return [
103
- `${SELECTED_PREFIX}${current}`,
104
- ...EFFORT_LEVELS.filter((level) => level !== current),
105
- ];
106
- };
107
-
108
- const selectedEffort = (choice: string): string | undefined => {
109
- const effort = choice.startsWith(SELECTED_PREFIX)
110
- ? choice.slice(SELECTED_PREFIX.length)
111
- : choice;
112
- return effort === DEFAULT_EFFORT_LEVEL ? undefined : effort;
113
- };
114
-
115
- const CONTEXT_PRESETS: ContextPreset[] = [
116
- {
117
- description:
118
- "No conversation history. The Advisor receives only its standing instructions.",
119
- label: "0",
120
- value: 0,
121
- },
122
- {
123
- description: "The most recent 10,000 characters of the current branch.",
124
- label: "10k",
125
- value: 10_000,
126
- },
127
- {
128
- description: "The most recent 25,000 characters of the current branch.",
129
- label: "25k",
130
- value: 25_000,
131
- },
132
- {
133
- description: "The most recent 100,000 characters of the current branch.",
134
- label: "100k",
135
- value: 100_000,
136
- },
137
- {
138
- description: "The most recent 200,000 characters of the current branch.",
139
- label: "200k",
140
- value: 200_000,
141
- },
142
- {
143
- description:
144
- "The complete reconstructed conversation branch. Cost and model context limits apply.",
145
- label: "ALL",
146
- value: Number.MAX_SAFE_INTEGER,
147
- },
148
- ];
149
-
150
- type ManualConsult = (
151
- ctx: ExtensionContext,
152
- question?: string,
153
- signal?: AbortSignal,
154
- onChunk?: (thinking: string, text: string) => void,
155
- onScout?: (event: ScoutLifecycleEvent) => void,
156
- gitContext?: GitContextLevel
157
- ) => Promise<{
158
- markdown: string;
159
- thinkingText: string;
160
- draftBytes?: number;
161
- preferenceBytes?: number;
162
- usage?: unknown;
163
- }>;
164
- type ThinkingLevel = Parameters<ExtensionAPI["setThinkingLevel"]>[0];
165
-
166
- const notify = (
167
- ctx: ExtensionContext,
168
- message: string,
169
- level: "error" | "info" | "warning"
170
- ) => {
171
- if (ctx.hasUI) {
172
- ctx.ui.notify(message, level);
173
- }
174
- };
175
-
176
- type ManualAdvisorProgressPhase =
177
- | "preparing"
178
- | "active"
179
- | "complete"
180
- | "cancelled"
181
- | "error";
182
- interface ManualAdvisorProgressState {
183
- phase: ManualAdvisorProgressPhase;
184
- scout?: ScoutToolDetails;
185
- text?: string;
186
- thinking?: string;
187
- }
188
-
189
- class ManualAdvisorProgressComponent implements Component {
190
- private readonly question: string | undefined;
191
- private readonly state: ManualAdvisorProgressState;
192
- private readonly expanded: boolean;
193
- private readonly theme: Theme;
194
-
195
- constructor(
196
- question: string | undefined,
197
- state: ManualAdvisorProgressState,
198
- expanded: boolean,
199
- theme: Theme
200
- ) {
201
- this.question = question;
202
- this.state = state;
203
- this.expanded = expanded;
204
- this.theme = theme;
205
- }
206
-
207
- render(width: number): string[] {
208
- const box = renderAdvisorCallBox(this.question, this.theme);
209
- if (this.state.phase === "complete" || this.state.phase === "cancelled") {
210
- return box.render(width);
211
- }
212
-
213
- const { scout } = this.state;
214
- const scoutActive =
215
- scout?.status === "calling" || scout?.status === "streaming";
216
- if (scoutActive) {
217
- renderScoutDetails(box, scout, this.expanded, this.theme);
218
- return box.render(width);
219
- }
220
- if (scout?.status === "cancelled") {
221
- return box.render(width);
222
- }
223
- if (this.state.phase === "error") {
224
- box.addChild(
225
- new Text(
226
- this.theme.fg("error", this.theme.bold("◆ ADVISOR · FAILED")),
227
- 0,
228
- 0
229
- )
230
- );
231
- return box.render(width);
232
- }
233
-
234
- const frame =
235
- SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
236
- let status = "Working…";
237
- if (this.state.phase === "preparing") {
238
- status = "Preparing…";
239
- } else if (this.state.text?.trim()) {
240
- status = "Responding…";
241
- }
242
- box.addChild(
243
- new Text(
244
- `${this.theme.fg("warning", this.theme.bold(`◆ ADVISOR ${frame}`))} ${this.theme.fg("dim", `· ${status}`)}`,
245
- 0,
246
- 0
247
- )
248
- );
249
- if (this.state.thinking) {
250
- box.addChild(
251
- new Text(
252
- this.theme.fg(
253
- "thinkingText",
254
- ` 💭 ${this.state.thinking.replace(/\n/g, " ").slice(-200)}`
255
- ),
256
- 0,
257
- 0
258
- )
259
- );
260
- }
261
- if (this.state.text) {
262
- box.addChild(
263
- new Markdown(
264
- adviceForDisplay(this.state.text, this.expanded),
265
- 0,
266
- 0,
267
- getMarkdownTheme()
268
- )
269
- );
270
- }
271
- return box.render(width);
272
- }
273
-
274
- invalidate(): void {
275
- // The live progress state is read during each render.
276
- }
277
- }
278
-
279
- const findConfiguredModel = (ctx: ExtensionContext, ref: string) => {
280
- const [provider, modelId] = splitRef(ref);
281
- return ctx.modelRegistry.find(provider, modelId);
282
- };
283
-
284
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one settings form maps every persisted control.
285
- const applyAdvisorSettings = (settings: AdvisorSettings) => {
286
- setAdvisorEffortRef(
287
- settings.effort === "Default (Model Default)" ? undefined : settings.effort
288
- );
289
- setContextMaxCharsRef(settings.contextMaxChars);
290
- setAdvisorPlanGateRef(settings.planGate);
291
- setAdvisorFailureGateRef(settings.failureGate);
292
- setAdvisorCompletionGateRef(settings.completionGate);
293
- setAdvisorCollapseResponsesRef(settings.collapseResponses);
294
- setAdvisorCustomInvocationRef(settings.customRule);
295
- setAdvisorBlockOnBlockedRef(settings.blockOnBlocked ?? true);
296
- setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
297
- setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
298
- setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
299
- setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
300
- setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
301
- setShowUsageDetailsRef(settings.showUsageDetails ?? true);
302
- setShowUsageFooterRef(settings.showUsageFooter ?? false);
303
- setSimpleModeRef(settings.simpleMode ?? false);
304
- setAlwaysOnRef(settings.alwaysOn ?? false);
305
- setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
306
- setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
307
- setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
308
- setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
309
- setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
310
- setAdvisorGitContextRef(settings.gitContext ?? "summary");
311
- setAdvisorGitContextMaxCharsRef(settings.gitContextMaxChars ?? 20_000);
312
- setAdvisorToolPoliciesRef(settings.toolPolicies ?? {});
313
- setAdvisorTrackedFileContentRef(settings.trackedFileContent ?? false);
314
- setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
315
- setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
316
- };
317
-
318
- const saveAdvisorSettings = (
319
- ctx: ExtensionContext,
320
- settings: AdvisorSettings
321
- ) => {
322
- applyAdvisorSettings(settings);
323
- saveConfig(ctx);
324
- saveGlobalOutcomeLogging(settings.outcomeLogging ?? false);
325
- };
326
-
327
- export const registerCommands = (
328
- pi: ExtensionAPI,
329
- dependencies: {
330
- consult?: ManualConsult;
331
- sessionState?: AdvisorSessionState;
332
- statusManager?: ScoutStatusManager;
333
- } = {}
334
- ) => {
335
- const advisorSessionState =
336
- dependencies.sessionState ?? defaultAdvisorSessionState;
337
- const scoutStatus =
338
- dependencies.statusManager ?? new ScoutStatusManager(false);
339
- const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
340
- const requestAdvisor =
341
- dependencies.consult ??
342
- ((ctx, question, signal, onChunk, onScout, gitContext) =>
343
- consultAdvisor(
344
- ctx,
345
- question,
346
- signal,
347
- onChunk,
348
- "manual",
349
- gitContext,
350
- undefined,
351
- undefined,
352
- undefined,
353
- onScout,
354
- undefined
355
- ));
356
- const manualConsultations = new Map<AbortController, symbol>();
357
- const manualProgressTimers = new Map<
358
- AbortController,
359
- ReturnType<typeof setInterval>
360
- >();
361
- const manualProgress = new Map<string, ManualAdvisorProgressState>();
362
- let manualProgressSequence = 0;
363
- const updateAdvisorUsageStatus = (ctx: ExtensionContext) => {
364
- if (ctx.hasUI) {
365
- ctx.ui.setStatus(
366
- "advisor-usage",
367
- getAdvisorSettings().showUsageFooter
368
- ? advisorSessionState.usageStatus()
369
- : undefined
370
- );
371
- }
372
- };
373
- const reportManualBudgetExhausted = (ctx: ExtensionContext) => {
374
- const message = "Advisor call budget exhausted for this session.";
375
- notify(ctx, message, "warning");
376
- notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
377
- };
378
-
379
- // A status update with an undefined value is an invisible render pulse. The
380
- // transcript owns manual progress now, so no consultation text is kept in the
381
- // footer while this pulse still drives the live spinner.
382
- const requestManualRender = (ctx: ExtensionContext) => {
383
- if (ctx.hasUI) {
384
- ctx.ui.setStatus("advisor-manual", undefined);
385
- }
386
- };
387
-
388
- const startManualConsultation = (
389
- ctx: ExtensionContext,
390
- question: string | undefined,
391
- controller: AbortController,
392
- scoutStatusToken: symbol,
393
- progress: ManualAdvisorProgressState,
394
- gitContext?: GitContextLevel
395
- ) => {
396
- herdrAdvisorActivity.start();
397
- progress.phase = "preparing";
398
- requestManualRender(ctx);
399
- if (ctx.hasUI) {
400
- const timer = setInterval(() => {
401
- if (
402
- controller.signal.aborted ||
403
- manualConsultations.get(controller) !== scoutStatusToken
404
- ) {
405
- clearInterval(timer);
406
- return;
407
- }
408
- requestManualRender(ctx);
409
- }, 80);
410
- manualProgressTimers.set(controller, timer);
411
- }
412
- let scoutDetails: Parameters<typeof appendScoutLifecycleEntry>[2];
413
- return requestAdvisor(
414
- ctx,
415
- question,
416
- controller.signal,
417
- (thinking, text) => {
418
- if (controller.signal.aborted) {
419
- return;
420
- }
421
- progress.phase = "active";
422
- progress.thinking = thinking;
423
- progress.text = text;
424
- requestManualRender(ctx);
425
- },
426
- (event) => {
427
- if (!controller.signal.aborted) {
428
- scoutStatus.update(ctx, scoutStatusToken, event);
429
- scoutDetails = appendScoutLifecycleEntry(pi, event, scoutDetails);
430
- progress.scout = scoutDetails;
431
- progress.phase = "active";
432
- requestManualRender(ctx);
433
- }
434
- },
435
- gitContext
436
- )
437
- .then(({ markdown, usage }) => {
438
- if (controller.signal.aborted) {
439
- return;
440
- }
441
- progress.phase = "complete";
442
- advisorSessionState.recordInvocation({
443
- cost: advisorUsageCost(usage),
444
- executionEffect: "continued",
445
- kind: "markdown",
446
- model: advisorRef,
447
- trigger: "manual",
448
- usage,
449
- });
450
- updateAdvisorUsageStatus(ctx);
451
- const normalizedUsage = snapshotAdvisorUsage(usage);
452
- pi.sendMessage(
453
- {
454
- content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${markdown}`,
455
- customType: "advisor-manual-result",
456
- details: {
457
- advisor: advisorRef,
458
- question,
459
- text: markdown,
460
- ...(normalizedUsage ? { usage: normalizedUsage } : {}),
461
- },
462
- display: true,
463
- },
464
- {
465
- // Steer lets the current turn finish its active work; the Executor sees
466
- // the result before its next model call rather than being interrupted.
467
- deliverAs: "steer",
468
- triggerTurn: true,
469
- }
470
- );
471
- })
472
- .catch((error: unknown) => {
473
- if (controller.signal.aborted) {
474
- return;
475
- }
476
- progress.phase = "error";
477
- const message = error instanceof Error ? error.message : String(error);
478
- advisorSessionState.recordInvocation({
479
- executionEffect: "continued",
480
- failure: "provider-error",
481
- kind: "markdown",
482
- model: advisorRef,
483
- trigger: "manual",
484
- });
485
- updateAdvisorUsageStatus(ctx);
486
- pi.sendMessage(
487
- {
488
- content: `Manual Advisor consultation failed: ${message}`,
489
- customType: "advisor-manual-result",
490
- details: {
491
- advisor: advisorRef,
492
- text: `**Advisor consultation failed:** ${message}`,
493
- },
494
- display: true,
495
- },
496
- { deliverAs: "steer", triggerTurn: true }
497
- );
498
- notify(ctx, `Advisor consultation failed: ${message}`, "error");
499
- notifyHerdrAdvisorFailure("Advisor consultation failed", message);
500
- })
501
- .finally(() => {
502
- if (controller.signal.aborted) {
503
- progress.phase = "cancelled";
504
- }
505
- const timer = manualProgressTimers.get(controller);
506
- if (timer) {
507
- clearInterval(timer);
508
- manualProgressTimers.delete(controller);
509
- }
510
- requestManualRender(ctx);
511
- scoutStatus.release(ctx, scoutStatusToken);
512
- manualConsultations.delete(controller);
513
- herdrAdvisorActivity.finish();
514
- });
515
- };
516
-
517
- /** Resolves both models and their auth, or reports why activation cannot proceed. */
518
- const resolveActivationModels = async (ctx: ExtensionContext) => {
519
- const executor = findConfiguredModel(ctx, executorRef);
520
- if (!executor) {
521
- return { error: `Executor model not found: ${executorRef}` };
522
- }
523
- const advisor = findConfiguredModel(ctx, advisorRef);
524
- if (!advisor) {
525
- return { error: `Advisor model not found: ${advisorRef}` };
526
- }
527
- const advisorAuth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
528
- if (!(advisorAuth.ok && advisorAuth.apiKey)) {
529
- return { error: `No API key for Advisor ${advisorRef}` };
530
- }
531
- if (!(await pi.setModel(executor))) {
532
- return { error: `No API key for Executor ${executorRef}` };
533
- }
534
- return {};
535
- };
536
-
537
- const loadCommandConfig = (ctx: ExtensionContext) => {
538
- try {
539
- loadConfig(ctx);
540
- return true;
541
- } catch (error) {
542
- const message = error instanceof Error ? error.message : String(error);
543
- notify(
544
- ctx,
545
- `Advisor command could not load configuration: ${message} Fix advisor.json and retry.`,
546
- "error"
547
- );
548
- return false;
549
- }
550
- };
551
-
552
- const activateAdvisor = async (
553
- args: string,
554
- ctx: ExtensionContext,
555
- announce = true
556
- ) => {
557
- if (!loadCommandConfig(ctx)) {
558
- return;
559
- }
560
- const previous = {
561
- advisor: advisorRef,
562
- contextMaxChars: contextMaxCharsRef,
563
- executor: executorRef,
564
- };
565
- const restoreRefs = () => {
566
- setAdvisorRef(previous.advisor);
567
- setContextMaxCharsRef(previous.contextMaxChars);
568
- setExecutorRef(previous.executor);
569
- };
570
- const argumentError = parseArgs(args);
571
- if (argumentError) {
572
- restoreRefs();
573
- notify(ctx, argumentError, "error");
574
- return;
575
- }
576
- const { error } = await resolveActivationModels(ctx);
577
- if (error) {
578
- restoreRefs();
579
- notify(ctx, error, "error");
580
- return;
581
- }
582
- // parseArgs only mutates in-memory refs, and every later loadConfig resets
583
- // them from disk. Persist supplied arguments once they are known to resolve,
584
- // so an unusable model reference is never written to the configuration.
585
- if (args.trim()) {
586
- saveConfig(ctx);
587
- }
588
- if (executorEffortRef) {
589
- pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
590
- }
591
- if (!flowEnabled()) {
592
- pi.setActiveTools([
593
- ...pi.getActiveTools(),
594
- "ask_advisor",
595
- "record_advisor_outcome",
596
- ]);
597
- }
598
- if (announce) {
599
- notify(
600
- ctx,
601
- `Advisor flow ready — Executor: ${executorRef} (thinking: ${executorEffortRef || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`,
602
- "info"
603
- );
604
- }
605
- };
606
-
607
- pi.registerEntryRenderer?.(
608
- "advisor-manual-call",
609
- (entry, { expanded }, theme) => {
610
- const { progressId, question } = (entry.data ?? {}) as {
611
- progressId?: string;
612
- question?: string;
613
- };
614
- const progress = progressId ? manualProgress.get(progressId) : undefined;
615
- return progress
616
- ? new ManualAdvisorProgressComponent(
617
- question,
618
- progress,
619
- Boolean(expanded),
620
- theme
621
- )
622
- : renderAdvisorCallBox(question, theme);
623
- }
624
- );
625
-
626
- pi.registerMessageRenderer?.(
627
- "advisor-manual-result",
628
- (message, { expanded }, theme) => {
629
- const details = message.details as
630
- | { advisor?: string; text?: string; usage?: unknown }
631
- | undefined;
632
- const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
633
- const advice =
634
- details?.text ??
635
- (typeof message.content === "string"
636
- ? message.content
637
- : "(Advisor returned no advice.)");
638
- // Manual consultations must render exactly like an Executor ask_advisor call.
639
- box.addChild(
640
- new Text(
641
- renderAdvisorResponseHeader(hasSoundVerdict(advice), theme),
642
- 0,
643
- 0
644
- )
645
- );
646
- if (details?.advisor) {
647
- box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
648
- }
649
- if (getAdvisorSettings().showUsageDetails) {
650
- const usage = formatAdvisorUsage(details?.usage);
651
- if (usage) {
652
- box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
653
- }
654
- }
655
- box.addChild(
656
- new Markdown(
657
- adviceForDisplay(advice, expanded),
658
- 0,
659
- 0,
660
- getMarkdownTheme()
661
- )
662
- );
663
- return box;
664
- }
665
- );
666
-
667
- pi.on("session_start", async (_event, ctx) => {
668
- // A malformed advisor.json or a provider auth failure must not reject a
669
- // lifecycle handler and break session startup.
670
- try {
671
- loadConfig(ctx);
672
- if (alwaysOnRef) {
673
- await activateAdvisor("", ctx, false);
674
- }
675
- } catch (error) {
676
- const message = error instanceof Error ? error.message : String(error);
677
- notify(ctx, `Advisor activation failed: ${message}`, "error");
678
- }
679
- });
680
-
681
- pi.on("model_select", (event, ctx) => {
682
- // Only an explicit user selection redefines the Executor. "restore" replays a
683
- // stored session model and would otherwise overwrite saved configuration.
684
- if (event.source !== "set" || !flowEnabled()) {
685
- return;
686
- }
687
- const selected = `${event.model.provider}/${event.model.id}`;
688
- if (selected === executorRef) {
689
- return;
690
- }
691
- setExecutorRef(selected);
692
- saveConfig(ctx);
693
- });
694
-
695
- pi.on("session_shutdown", (_event, ctx) => {
696
- if (ctx.hasUI) {
697
- ctx.ui.setStatus("advisor-usage", undefined);
698
- }
699
- for (const [controller, token] of manualConsultations) {
700
- controller.abort();
701
- const timer = manualProgressTimers.get(controller);
702
- if (timer) {
703
- clearInterval(timer);
704
- manualProgressTimers.delete(controller);
705
- }
706
- scoutStatus.release(ctx, token);
707
- }
708
- scoutStatus.clear(ctx);
709
- manualConsultations.clear();
710
- manualProgressTimers.clear();
711
- manualProgress.clear();
712
- herdrAdvisorActivity.clear();
713
- });
714
-
715
- pi.registerCommand("advisor-manual", {
716
- description:
717
- "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
718
- handler: async (args, ctx) => {
719
- if (!loadCommandConfig(ctx)) {
720
- return;
721
- }
722
- if (
723
- !(
724
- isSimpleMode() ||
725
- advisorSessionState.canConsult(getAdvisorMaxCallsPerSession())
726
- )
727
- ) {
728
- reportManualBudgetExhausted(ctx);
729
- return;
730
- }
731
-
732
- let gitContext: GitContextLevel | undefined;
733
- let question: string | undefined;
734
- if (ctx.mode === "tui") {
735
- const request = await ctx.ui.custom<ManualAdvisorRequest | undefined>(
736
- (tui, theme, keybindings, done) =>
737
- new ManualAdvisorDialog({
738
- gitContext: getAdvisorSettings().gitContext,
739
- initialMessage: args,
740
- keybindings,
741
- onCancel: () => done(undefined),
742
- onSubmit: done,
743
- theme,
744
- tui,
745
- }),
746
- {
747
- overlay: true,
748
- overlayOptions: {
749
- anchor: "center",
750
- margin: 2,
751
- maxHeight: "80%",
752
- minWidth: 56,
753
- width: 76,
754
- },
755
- }
756
- );
757
- if (!request) {
758
- return;
759
- }
760
- if (
761
- !(
762
- isSimpleMode() ||
763
- advisorSessionState.canConsult(getAdvisorMaxCallsPerSession())
764
- )
765
- ) {
766
- reportManualBudgetExhausted(ctx);
767
- return;
768
- }
769
- const { gitContext: selectedGitContext, message } = request;
770
- gitContext = selectedGitContext;
771
- question = resolveAdvisorRequest(message);
772
- } else {
773
- question = resolveAdvisorRequest(args);
774
- }
775
-
776
- if (!isSimpleMode()) {
777
- advisorSessionState.consumeCall();
778
- }
779
- // A single visible progress surface avoids competing consultations overwriting
780
- // each other's streamed state. A newer manual request replaces the previous one.
781
- for (const [pending, token] of manualConsultations) {
782
- pending.abort();
783
- scoutStatus.release(ctx, token);
784
- }
785
- manualConsultations.clear();
786
- const controller = new AbortController();
787
- const scoutStatusToken = Symbol("manual-scout");
788
- manualProgressSequence += 1;
789
- const progressId = `manual-${manualProgressSequence}`;
790
- const progress: ManualAdvisorProgressState = { phase: "preparing" };
791
- manualProgress.set(progressId, progress);
792
- scoutStatus.register(scoutStatusToken);
793
- manualConsultations.set(controller, scoutStatusToken);
794
- pi.appendEntry?.("advisor-manual-call", { progressId, question });
795
- startManualConsultation(
796
- ctx,
797
- question,
798
- controller,
799
- scoutStatusToken,
800
- progress,
801
- gitContext
802
- );
803
- },
804
- });
805
-
806
- pi.registerCommand("advisor", {
807
- description:
808
- "Enable the Executor/Advisor flow and switch to the configured Executor model; accepts contextMaxChars=N",
809
- handler: (args, ctx) => activateAdvisor(args, ctx),
810
- });
811
-
812
- pi.registerCommand("advisor-models", {
813
- description:
814
- "Select and persist the Executor and Advisor models with reasoning levels",
815
- handler: async (_args, ctx) => {
816
- if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
817
- return;
818
- }
819
- const refs = ctx.modelRegistry
820
- .getAvailable()
821
- .map((m) => `${m.provider}/${m.id}`);
822
-
823
- const executor = await ctx.ui.custom<string | undefined>(
824
- (tui, theme, keybindings, done) =>
825
- new SearchableModelSelector({
826
- allOptions: refs,
827
- currentOption: executorRef,
828
- keybindings,
829
- onCancel: () => done(undefined),
830
- onSelect: done,
831
- theme,
832
- title: "Select Executor Model",
833
- tui,
834
- })
835
- );
836
- if (!executor) {
837
- return;
838
- }
839
-
840
- const executorEffort = await ctx.ui.select(
841
- "Select Executor Reasoning/Thinking Level",
842
- effortChoices(executorEffortRef)
843
- );
844
- if (!executorEffort) {
845
- return;
846
- }
847
-
848
- const advisor = await ctx.ui.custom<string | undefined>(
849
- (tui, theme, keybindings, done) =>
850
- new SearchableModelSelector({
851
- allOptions: refs,
852
- currentOption: advisorRef,
853
- keybindings,
854
- onCancel: () => done(undefined),
855
- onSelect: done,
856
- theme,
857
- title: "Select Advisor Model",
858
- tui,
859
- })
860
- );
861
- if (!advisor) {
862
- return;
863
- }
864
-
865
- const advisorEffort = await ctx.ui.select(
866
- "Select Advisor Reasoning/Thinking Level",
867
- effortChoices(advisorEffortRef)
868
- );
869
- if (!advisorEffort) {
870
- return;
871
- }
872
-
873
- setExecutorRef(executor);
874
- setAdvisorRef(advisor);
875
- setExecutorEffortRef(selectedEffort(executorEffort));
876
- setAdvisorEffortRef(selectedEffort(advisorEffort));
877
-
878
- const path = saveConfig(ctx);
879
- ctx.ui.notify(
880
- `Saved Executor + Advisor configurations to ${path}`,
881
- "info"
882
- );
883
- },
884
- });
885
-
886
- pi.registerCommand("advisor-settings", {
887
- description: "Configure Advisor settings",
888
- handler: async (_args, ctx) => {
889
- if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
890
- return;
891
- }
892
-
893
- const initial: AdvisorSettings = getAdvisorSettings();
894
- await ctx.ui.custom<void>(
895
- (tui, theme, _keybindings, done) =>
896
- new AdvisorSettingsSelector({
897
- effortLevels: EFFORT_LEVELS,
898
- initial,
899
- onCancel: () => done(),
900
- onChange: (settings) => {
901
- try {
902
- saveAdvisorSettings(ctx, settings);
903
- updateAdvisorUsageStatus(ctx);
904
- } catch (error) {
905
- const message =
906
- error instanceof Error ? error.message : String(error);
907
- ctx.ui.notify(
908
- `Could not save Advisor settings: ${message}`,
909
- "error"
910
- );
911
- }
912
- },
913
- presets: CONTEXT_PRESETS,
914
- theme,
915
- tui,
916
- })
917
- );
918
- },
919
- });
920
-
921
- pi.registerCommand("advisor-off", {
922
- description: "Disable on-demand Advisor calls; keep the current model",
923
- handler: (_args, ctx) => {
924
- pi.setActiveTools(
925
- pi
926
- .getActiveTools()
927
- .filter(
928
- (name) =>
929
- name !== "ask_advisor" && name !== "record_advisor_outcome"
930
- )
931
- );
932
- // Leaving alwaysOn set would silently reactivate the flow next session.
933
- const wasAlwaysOn = alwaysOnRef;
934
- if (wasAlwaysOn) {
935
- setAlwaysOnRef(false);
936
- saveConfig(ctx);
937
- }
938
- notify(
939
- ctx,
940
- `Advisor flow disabled. Current model unchanged.${wasAlwaysOn ? " Always on turned off." : ""}`,
941
- "info"
942
- );
943
- return Promise.resolve();
944
- },
945
- });
946
- };
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";