pi-advisor-flow 0.3.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/commands.ts CHANGED
@@ -2,16 +2,17 @@ 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
- advisorMaxCallsPerSessionRef,
10
10
  advisorRef,
11
11
  alwaysOnRef,
12
12
  contextMaxCharsRef,
13
13
  executorEffortRef,
14
14
  executorRef,
15
+ getAdvisorMaxCallsPerSession,
15
16
  getAdvisorSettings,
16
17
  isSimpleMode,
17
18
  loadConfig,
@@ -46,13 +47,15 @@ import {
46
47
  setContextMaxCharsRef,
47
48
  setExecutorEffortRef,
48
49
  setExecutorRef,
50
+ setShowUsageDetailsRef,
51
+ setShowUsageFooterRef,
49
52
  setSimpleModeRef,
50
53
  splitRef,
51
54
  } from "./config.js";
55
+ import type { GitContextLevel } from "./git.js";
52
56
  import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "./herdr.js";
53
57
  import type { ScoutLifecycleEvent } from "./scout.js";
54
58
  import type { AdvisorSessionState } from "./session-state.js";
55
- import type { BenchmarkTelemetry } from "./telemetry.js";
56
59
  import {
57
60
  adviceForDisplay,
58
61
  appendScoutLifecycleEntry,
@@ -61,15 +64,25 @@ import {
61
64
  hasSoundVerdict,
62
65
  renderAdvisorCallBox,
63
66
  renderAdvisorResponseHeader,
67
+ renderScoutDetails,
64
68
  resolveAdvisorRequest,
65
69
  ScoutStatusManager,
70
+ type ScoutToolDetails,
71
+ SPINNER_FRAMES,
66
72
  } from "./tools.js";
67
73
  import {
68
74
  type AdvisorSettings,
69
75
  AdvisorSettingsSelector,
70
76
  type ContextPreset,
77
+ ManualAdvisorDialog,
78
+ type ManualAdvisorRequest,
71
79
  SearchableModelSelector,
72
80
  } from "./ui.js";
81
+ import {
82
+ advisorUsageCost,
83
+ formatAdvisorUsage,
84
+ snapshotAdvisorUsage,
85
+ } from "./usage.js";
73
86
 
74
87
  const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
75
88
  const SELECTED_PREFIX = "✓ ";
@@ -139,12 +152,14 @@ type ManualConsult = (
139
152
  question?: string,
140
153
  signal?: AbortSignal,
141
154
  onChunk?: (thinking: string, text: string) => void,
142
- onScout?: (event: ScoutLifecycleEvent) => void
155
+ onScout?: (event: ScoutLifecycleEvent) => void,
156
+ gitContext?: GitContextLevel
143
157
  ) => Promise<{
144
158
  markdown: string;
145
159
  thinkingText: string;
146
160
  draftBytes?: number;
147
161
  preferenceBytes?: number;
162
+ usage?: unknown;
148
163
  }>;
149
164
  type ThinkingLevel = Parameters<ExtensionAPI["setThinkingLevel"]>[0];
150
165
 
@@ -158,54 +173,215 @@ const notify = (
158
173
  }
159
174
  };
160
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
+
161
279
  const findConfiguredModel = (ctx: ExtensionContext, ref: string) => {
162
280
  const [provider, modelId] = splitRef(ref);
163
281
  return ctx.modelRegistry.find(provider, modelId);
164
282
  };
165
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
+
166
327
  export const registerCommands = (
167
328
  pi: ExtensionAPI,
168
329
  dependencies: {
169
330
  consult?: ManualConsult;
170
331
  sessionState?: AdvisorSessionState;
171
332
  statusManager?: ScoutStatusManager;
172
- telemetry?: BenchmarkTelemetry;
173
333
  } = {}
174
334
  ) => {
175
335
  const advisorSessionState =
176
336
  dependencies.sessionState ?? defaultAdvisorSessionState;
177
- const scoutStatus = dependencies.statusManager ?? new ScoutStatusManager();
337
+ const scoutStatus =
338
+ dependencies.statusManager ?? new ScoutStatusManager(false);
178
339
  const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
179
340
  const requestAdvisor =
180
341
  dependencies.consult ??
181
- ((ctx, question, signal, onChunk, onScout) =>
342
+ ((ctx, question, signal, onChunk, onScout, gitContext) =>
182
343
  consultAdvisor(
183
344
  ctx,
184
345
  question,
185
346
  signal,
186
347
  onChunk,
187
348
  "manual",
188
- undefined,
349
+ gitContext,
189
350
  undefined,
190
351
  undefined,
191
352
  undefined,
192
353
  onScout,
193
- undefined,
194
- dependencies.telemetry
354
+ undefined
195
355
  ));
196
356
  const manualConsultations = new Map<AbortController, symbol>();
197
- const setManualStatus = (
198
- ctx: ExtensionContext,
199
- controller: AbortController,
200
- token: symbol,
201
- status: string | undefined
202
- ) => {
203
- if (
204
- ctx.hasUI &&
205
- manualConsultations.get(controller) === token &&
206
- !controller.signal.aborted
207
- ) {
208
- ctx.ui.setStatus("advisor-manual", status);
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);
209
385
  }
210
386
  };
211
387
 
@@ -213,10 +389,26 @@ export const registerCommands = (
213
389
  ctx: ExtensionContext,
214
390
  question: string | undefined,
215
391
  controller: AbortController,
216
- scoutStatusToken: symbol
392
+ scoutStatusToken: symbol,
393
+ progress: ManualAdvisorProgressState,
394
+ gitContext?: GitContextLevel
217
395
  ) => {
218
396
  herdrAdvisorActivity.start();
219
- setManualStatus(ctx, controller, scoutStatusToken, "Advisor preparing…");
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
+ }
220
412
  let scoutDetails: Parameters<typeof appendScoutLifecycleEntry>[2];
221
413
  return requestAdvisor(
222
414
  ctx,
@@ -226,52 +418,47 @@ export const registerCommands = (
226
418
  if (controller.signal.aborted) {
227
419
  return;
228
420
  }
229
- let status = "Advisor working…";
230
- if (thinking.trim()) {
231
- status = "Advisor thinking…";
232
- }
233
- if (text.trim()) {
234
- status = "Advisor responding…";
235
- }
236
- setManualStatus(ctx, controller, scoutStatusToken, status);
421
+ progress.phase = "active";
422
+ progress.thinking = thinking;
423
+ progress.text = text;
424
+ requestManualRender(ctx);
237
425
  },
238
426
  (event) => {
239
427
  if (!controller.signal.aborted) {
240
428
  scoutStatus.update(ctx, scoutStatusToken, event);
241
- if (event.type === "call" || event.type === "chunk") {
242
- setManualStatus(
243
- ctx,
244
- controller,
245
- scoutStatusToken,
246
- "Advisor Scout curating…"
247
- );
248
- } else if (event.type === "success" || event.type === "fallback") {
249
- setManualStatus(
250
- ctx,
251
- controller,
252
- scoutStatusToken,
253
- "Advisor working…"
254
- );
255
- }
256
429
  scoutDetails = appendScoutLifecycleEntry(pi, event, scoutDetails);
430
+ progress.scout = scoutDetails;
431
+ progress.phase = "active";
432
+ requestManualRender(ctx);
257
433
  }
258
- }
434
+ },
435
+ gitContext
259
436
  )
260
- .then(({ markdown }) => {
437
+ .then(({ markdown, usage }) => {
261
438
  if (controller.signal.aborted) {
262
439
  return;
263
440
  }
441
+ progress.phase = "complete";
264
442
  advisorSessionState.recordInvocation({
443
+ cost: advisorUsageCost(usage),
265
444
  executionEffect: "continued",
266
445
  kind: "markdown",
267
446
  model: advisorRef,
268
447
  trigger: "manual",
448
+ usage,
269
449
  });
450
+ updateAdvisorUsageStatus(ctx);
451
+ const normalizedUsage = snapshotAdvisorUsage(usage);
270
452
  pi.sendMessage(
271
453
  {
272
454
  content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${markdown}`,
273
455
  customType: "advisor-manual-result",
274
- details: { advisor: advisorRef, question, text: markdown },
456
+ details: {
457
+ advisor: advisorRef,
458
+ question,
459
+ text: markdown,
460
+ ...(normalizedUsage ? { usage: normalizedUsage } : {}),
461
+ },
275
462
  display: true,
276
463
  },
277
464
  {
@@ -286,6 +473,7 @@ export const registerCommands = (
286
473
  if (controller.signal.aborted) {
287
474
  return;
288
475
  }
476
+ progress.phase = "error";
289
477
  const message = error instanceof Error ? error.message : String(error);
290
478
  advisorSessionState.recordInvocation({
291
479
  executionEffect: "continued",
@@ -294,6 +482,7 @@ export const registerCommands = (
294
482
  model: advisorRef,
295
483
  trigger: "manual",
296
484
  });
485
+ updateAdvisorUsageStatus(ctx);
297
486
  pi.sendMessage(
298
487
  {
299
488
  content: `Manual Advisor consultation failed: ${message}`,
@@ -310,12 +499,15 @@ export const registerCommands = (
310
499
  notifyHerdrAdvisorFailure("Advisor consultation failed", message);
311
500
  })
312
501
  .finally(() => {
313
- if (
314
- ctx.hasUI &&
315
- manualConsultations.get(controller) === scoutStatusToken
316
- ) {
317
- ctx.ui.setStatus("advisor-manual", undefined);
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);
318
509
  }
510
+ requestManualRender(ctx);
319
511
  scoutStatus.release(ctx, scoutStatusToken);
320
512
  manualConsultations.delete(controller);
321
513
  herdrAdvisorActivity.finish();
@@ -414,9 +606,20 @@ export const registerCommands = (
414
606
 
415
607
  pi.registerEntryRenderer?.(
416
608
  "advisor-manual-call",
417
- (entry, _options, theme) => {
418
- const { question } = (entry.data ?? {}) as { question?: string };
419
- return renderAdvisorCallBox(question, theme);
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);
420
623
  }
421
624
  );
422
625
 
@@ -424,7 +627,7 @@ export const registerCommands = (
424
627
  "advisor-manual-result",
425
628
  (message, { expanded }, theme) => {
426
629
  const details = message.details as
427
- | { advisor?: string; text?: string }
630
+ | { advisor?: string; text?: string; usage?: unknown }
428
631
  | undefined;
429
632
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
430
633
  const advice =
@@ -443,6 +646,12 @@ export const registerCommands = (
443
646
  if (details?.advisor) {
444
647
  box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
445
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
+ }
446
655
  box.addChild(
447
656
  new Markdown(
448
657
  adviceForDisplay(advice, expanded),
@@ -485,39 +694,88 @@ export const registerCommands = (
485
694
 
486
695
  pi.on("session_shutdown", (_event, ctx) => {
487
696
  if (ctx.hasUI) {
488
- ctx.ui.setStatus("advisor-manual", undefined);
697
+ ctx.ui.setStatus("advisor-usage", undefined);
489
698
  }
490
699
  for (const [controller, token] of manualConsultations) {
491
700
  controller.abort();
701
+ const timer = manualProgressTimers.get(controller);
702
+ if (timer) {
703
+ clearInterval(timer);
704
+ manualProgressTimers.delete(controller);
705
+ }
492
706
  scoutStatus.release(ctx, token);
493
707
  }
494
708
  scoutStatus.clear(ctx);
495
709
  manualConsultations.clear();
710
+ manualProgressTimers.clear();
711
+ manualProgress.clear();
496
712
  herdrAdvisorActivity.clear();
497
713
  });
498
714
 
499
715
  pi.registerCommand("advisor-manual", {
500
716
  description:
501
717
  "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
502
- handler: (args, ctx) => {
718
+ handler: async (args, ctx) => {
503
719
  if (!loadCommandConfig(ctx)) {
504
- return Promise.resolve();
720
+ return;
505
721
  }
506
722
  if (
507
723
  !(
508
724
  isSimpleMode() ||
509
- advisorSessionState.canConsult(advisorMaxCallsPerSessionRef)
725
+ advisorSessionState.canConsult(getAdvisorMaxCallsPerSession())
510
726
  )
511
727
  ) {
512
- const message = "Advisor call budget exhausted for this session.";
513
- notify(ctx, message, "warning");
514
- notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
515
- return Promise.resolve();
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);
516
774
  }
775
+
517
776
  if (!isSimpleMode()) {
518
777
  advisorSessionState.consumeCall();
519
778
  }
520
- const question = resolveAdvisorRequest(args);
521
779
  // A single visible progress surface avoids competing consultations overwriting
522
780
  // each other's streamed state. A newer manual request replaces the previous one.
523
781
  for (const [pending, token] of manualConsultations) {
@@ -527,11 +785,21 @@ export const registerCommands = (
527
785
  manualConsultations.clear();
528
786
  const controller = new AbortController();
529
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);
530
792
  scoutStatus.register(scoutStatusToken);
531
793
  manualConsultations.set(controller, scoutStatusToken);
532
- pi.appendEntry?.("advisor-manual-call", { question });
533
- startManualConsultation(ctx, question, controller, scoutStatusToken);
534
- return Promise.resolve();
794
+ pi.appendEntry?.("advisor-manual-call", { progressId, question });
795
+ startManualConsultation(
796
+ ctx,
797
+ question,
798
+ controller,
799
+ scoutStatusToken,
800
+ progress,
801
+ gitContext
802
+ );
535
803
  },
536
804
  });
537
805
 
@@ -616,68 +884,37 @@ export const registerCommands = (
616
884
  });
617
885
 
618
886
  pi.registerCommand("advisor-settings", {
619
- description: "Configure Advisor context and reasoning effort",
620
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one settings form maps every persisted control.
887
+ description: "Configure Advisor settings",
621
888
  handler: async (_args, ctx) => {
622
889
  if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
623
890
  return;
624
891
  }
625
892
 
626
893
  const initial: AdvisorSettings = getAdvisorSettings();
627
- const settings = await ctx.ui.custom<AdvisorSettings | undefined>(
894
+ await ctx.ui.custom<void>(
628
895
  (tui, theme, _keybindings, done) =>
629
896
  new AdvisorSettingsSelector({
630
897
  effortLevels: EFFORT_LEVELS,
631
898
  initial,
632
- onCancel: () => done(undefined),
633
- onSave: done,
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
+ },
634
913
  presets: CONTEXT_PRESETS,
635
914
  theme,
636
915
  tui,
637
916
  })
638
917
  );
639
- if (!settings) {
640
- return;
641
- }
642
-
643
- setAdvisorEffortRef(
644
- settings.effort === "Default (Model Default)"
645
- ? undefined
646
- : settings.effort
647
- );
648
- setContextMaxCharsRef(settings.contextMaxChars);
649
- setAdvisorPlanGateRef(settings.planGate);
650
- setAdvisorFailureGateRef(settings.failureGate);
651
- setAdvisorCompletionGateRef(settings.completionGate);
652
- setAdvisorCollapseResponsesRef(settings.collapseResponses);
653
- setAdvisorCustomInvocationRef(settings.customRule);
654
- setAdvisorBlockOnBlockedRef(settings.blockOnBlocked ?? true);
655
- setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
656
- setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
657
- setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
658
- setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
659
- setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
660
- setSimpleModeRef(settings.simpleMode ?? false);
661
- setAlwaysOnRef(settings.alwaysOn ?? false);
662
- setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
663
- setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
664
- setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
665
- setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
666
- setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
667
- setAdvisorGitContextRef(settings.gitContext ?? "summary");
668
- setAdvisorGitContextMaxCharsRef(settings.gitContextMaxChars ?? 20_000);
669
- setAdvisorToolPoliciesRef(settings.toolPolicies ?? {});
670
- setAdvisorTrackedFileContentRef(settings.trackedFileContent ?? false);
671
- setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
672
- setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
673
- const path = saveConfig(ctx);
674
- const globalPath = saveGlobalOutcomeLogging(
675
- settings.outcomeLogging ?? false
676
- );
677
- ctx.ui.notify(
678
- `Saved Advisor settings to ${path}; outcome logging globally to ${globalPath}`,
679
- "info"
680
- );
681
918
  },
682
919
  });
683
920