pi-advisor-flow 0.5.1 → 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 +14 -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 +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 -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
@@ -0,0 +1,93 @@
1
+ import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import { type Component, Markdown, Text } from "@earendil-works/pi-tui";
3
+ import {
4
+ adviceForDisplay,
5
+ renderAdvisorCallBox,
6
+ renderThinkingMarkdown,
7
+ SPINNER_FRAMES,
8
+ } from "../tools/render-common.js";
9
+ import { renderScoutDetails } from "../tools/scout-status.js";
10
+ import type { ManualAdvisorProgressState } from "./types.js";
11
+
12
+ export class ManualAdvisorProgressComponent implements Component {
13
+ private readonly question: string | undefined;
14
+ private readonly state: ManualAdvisorProgressState;
15
+ private readonly expanded: boolean;
16
+ private readonly theme: Theme;
17
+
18
+ constructor(
19
+ question: string | undefined,
20
+ state: ManualAdvisorProgressState,
21
+ expanded: boolean,
22
+ theme: Theme
23
+ ) {
24
+ this.question = question;
25
+ this.state = state;
26
+ this.expanded = expanded;
27
+ this.theme = theme;
28
+ }
29
+
30
+ render(width: number): string[] {
31
+ const box = renderAdvisorCallBox(this.question, this.theme);
32
+ if (this.state.phase === "complete" || this.state.phase === "cancelled") {
33
+ return box.render(width);
34
+ }
35
+
36
+ const { scout } = this.state;
37
+ const scoutActive =
38
+ scout?.status === "calling" || scout?.status === "streaming";
39
+ if (scoutActive) {
40
+ renderScoutDetails(box, scout, this.expanded, this.theme);
41
+ return box.render(width);
42
+ }
43
+ if (scout?.status === "cancelled") {
44
+ return box.render(width);
45
+ }
46
+ if (this.state.phase === "error") {
47
+ box.addChild(
48
+ new Text(
49
+ this.theme.fg("error", this.theme.bold("◆ ADVISOR · FAILED")),
50
+ 0,
51
+ 0
52
+ )
53
+ );
54
+ return box.render(width);
55
+ }
56
+
57
+ const frame =
58
+ SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
59
+ let status = "Working…";
60
+ if (this.state.phase === "preparing") {
61
+ status = "Preparing…";
62
+ } else if (this.state.text?.trim()) {
63
+ status = "Responding…";
64
+ }
65
+ box.addChild(
66
+ new Text(
67
+ `${this.theme.fg("warning", this.theme.bold(`◆ ADVISOR ${frame}`))} ${this.theme.fg("dim", `· ${status}`)}`,
68
+ 0,
69
+ 0
70
+ )
71
+ );
72
+ if (this.state.thinking?.trim()) {
73
+ box.addChild(
74
+ renderThinkingMarkdown(this.state.thinking.slice(-200), this.theme)
75
+ );
76
+ }
77
+ if (this.state.text) {
78
+ box.addChild(
79
+ new Markdown(
80
+ adviceForDisplay(this.state.text, this.expanded),
81
+ 0,
82
+ 0,
83
+ getMarkdownTheme()
84
+ )
85
+ );
86
+ }
87
+ return box.render(width);
88
+ }
89
+
90
+ invalidate(): void {
91
+ // The live progress state is read during each render.
92
+ }
93
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ advisorEffortRef,
3
+ advisorRef,
4
+ executorEffortRef,
5
+ executorRef,
6
+ getPersistedModelRefs,
7
+ setAdvisorEffortRef,
8
+ setAdvisorRef,
9
+ setExecutorEffortRef,
10
+ setExecutorRef,
11
+ } from "../config/state.js";
12
+ import { saveConfig } from "../config/storage.js";
13
+ import { activateAdvisor } from "./activation.js";
14
+ import { loadCommandConfig } from "./activation-preparation.js";
15
+ import { selectAdvisorModels } from "./model-picker.js";
16
+ import type { CommandRuntime } from "./types.js";
17
+
18
+ export const registerModelCommands = (runtime: CommandRuntime) => {
19
+ runtime.pi.registerCommand("advisor", {
20
+ description:
21
+ "Enable the Executor/Advisor flow and switch to the configured or explicitly selected Executor model; accepts contextMaxChars=N",
22
+ handler: (args, ctx) => activateAdvisor(runtime, args, ctx),
23
+ });
24
+
25
+ runtime.pi.registerCommand("advisor-models", {
26
+ description:
27
+ "Select and persist the Executor and Advisor models with reasoning levels",
28
+ handler: async (_args, ctx) => {
29
+ if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
30
+ return;
31
+ }
32
+ // When `/model` was used before activation, show that session choice as
33
+ // the Executor's current option instead of making the persisted Executor
34
+ // look like the active selection.
35
+ const persisted = getPersistedModelRefs();
36
+ const selection = await selectAdvisorModels(ctx, {
37
+ advisor: persisted.advisor ? advisorRef : "",
38
+ advisorEffort: advisorEffortRef,
39
+ executor:
40
+ runtime.pendingExecutorModelRef ??
41
+ (persisted.executor ? executorRef : ""),
42
+ executorEffort: executorEffortRef,
43
+ selectAdvisor: true,
44
+ selectExecutor: true,
45
+ });
46
+ if (!selection) {
47
+ return;
48
+ }
49
+
50
+ setExecutorRef(selection.executor);
51
+ setAdvisorRef(selection.advisor);
52
+ setExecutorEffortRef(selection.executorEffort);
53
+ setAdvisorEffortRef(selection.advisorEffort);
54
+
55
+ const path = saveConfig(ctx, {
56
+ persistAdvisor: true,
57
+ persistExecutor: true,
58
+ });
59
+ runtime.pendingExecutorModelRef = undefined;
60
+ ctx.ui.notify(
61
+ `Saved Executor + Advisor configurations to ${path}`,
62
+ "info"
63
+ );
64
+ },
65
+ });
66
+ };
@@ -0,0 +1,181 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { type getPersistedModelRefs, splitRef } from "../config/state.js";
3
+ import type { ContextPreset } from "../ui/types.js";
4
+
5
+ export const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
6
+ export const SELECTED_PREFIX = "✓ ";
7
+ export const EFFORT_LEVELS = [
8
+ DEFAULT_EFFORT_LEVEL,
9
+ "off",
10
+ "minimal",
11
+ "low",
12
+ "medium",
13
+ "high",
14
+ "xhigh",
15
+ "max",
16
+ ];
17
+
18
+ export const effortChoices = (configured: string | undefined): string[] => {
19
+ const current = configured ?? DEFAULT_EFFORT_LEVEL;
20
+ return [
21
+ `${SELECTED_PREFIX}${current}`,
22
+ ...EFFORT_LEVELS.filter((level) => level !== current),
23
+ ];
24
+ };
25
+
26
+ export const selectedEffort = (choice: string): string | undefined => {
27
+ const effort = choice.startsWith(SELECTED_PREFIX)
28
+ ? choice.slice(SELECTED_PREFIX.length)
29
+ : choice;
30
+ return effort === DEFAULT_EFFORT_LEVEL ? undefined : effort;
31
+ };
32
+
33
+ export const ADVISOR_ACTIVATION_EXPLANATION =
34
+ "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.";
35
+
36
+ const ARGUMENT_WHITESPACE = /\s+/;
37
+ const hasModelOverride = (args: string, key: "advisor" | "executor") =>
38
+ args
39
+ .trim()
40
+ .split(ARGUMENT_WHITESPACE)
41
+ .some((token) => {
42
+ const [tokenKey, value] = token.split("=");
43
+ return tokenKey === key && Boolean(value);
44
+ });
45
+
46
+ export const hasExecutorOverride = (args: string) =>
47
+ hasModelOverride(args, "executor");
48
+ export const hasAdvisorOverride = (args: string) =>
49
+ hasModelOverride(args, "advisor");
50
+
51
+ export const CONTEXT_PRESETS: ContextPreset[] = [
52
+ {
53
+ description:
54
+ "No conversation history. The Advisor receives only its standing instructions.",
55
+ label: "0",
56
+ value: 0,
57
+ },
58
+ {
59
+ description: "The most recent 10,000 characters of the current branch.",
60
+ label: "10k",
61
+ value: 10_000,
62
+ },
63
+ {
64
+ description: "The most recent 25,000 characters of the current branch.",
65
+ label: "25k",
66
+ value: 25_000,
67
+ },
68
+ {
69
+ description: "The most recent 100,000 characters of the current branch.",
70
+ label: "100k",
71
+ value: 100_000,
72
+ },
73
+ {
74
+ description: "The most recent 200,000 characters of the current branch.",
75
+ label: "200k",
76
+ value: 200_000,
77
+ },
78
+ {
79
+ description:
80
+ "The complete reconstructed conversation branch. Cost and model context limits apply.",
81
+ label: "ALL",
82
+ value: Number.MAX_SAFE_INTEGER,
83
+ },
84
+ ];
85
+
86
+ export const findConfiguredModel = (
87
+ ctx: ExtensionContext,
88
+ ref: string | undefined
89
+ ) => {
90
+ if (!ref) {
91
+ return;
92
+ }
93
+ const [provider, modelId] = splitRef(ref);
94
+ return ctx.modelRegistry.find(provider, modelId);
95
+ };
96
+
97
+ export const getAvailableModelRefs = (
98
+ ctx: ExtensionContext
99
+ ): string[] | undefined => {
100
+ if (typeof ctx.modelRegistry.getAvailable !== "function") {
101
+ return undefined;
102
+ }
103
+ return ctx.modelRegistry
104
+ .getAvailable()
105
+ .map((model) => `${model.provider}/${model.id}`);
106
+ };
107
+
108
+ export const isSelectableModel = (
109
+ ctx: ExtensionContext,
110
+ ref: string | undefined,
111
+ availableRefs: Set<string> | undefined
112
+ ) => {
113
+ if (!ref) {
114
+ return false;
115
+ }
116
+ if (availableRefs) {
117
+ const [provider, modelId] = splitRef(ref);
118
+ return availableRefs.has(`${provider}/${modelId}`);
119
+ }
120
+ return Boolean(findConfiguredModel(ctx, ref));
121
+ };
122
+
123
+ export const getExplicitModelError = (
124
+ ctx: ExtensionContext,
125
+ ref: string | undefined,
126
+ label: "Advisor" | "Executor",
127
+ overridden: boolean,
128
+ availableRefs: Set<string> | undefined
129
+ ) => {
130
+ if (overridden) {
131
+ if (!ref) {
132
+ return `${label} model not configured`;
133
+ }
134
+ if (!findConfiguredModel(ctx, ref)) {
135
+ return `${label} model not found: ${ref}`;
136
+ }
137
+ if (availableRefs && !isSelectableModel(ctx, ref, availableRefs)) {
138
+ return `${label} model unavailable: ${ref}`;
139
+ }
140
+ }
141
+ };
142
+
143
+ export interface ActivationModelPlan {
144
+ pendingExecutor: string | undefined;
145
+ selectAdvisor: boolean;
146
+ selectExecutor: boolean;
147
+ }
148
+
149
+ export const planActivationModels = (
150
+ ctx: ExtensionContext,
151
+ executor: string | undefined,
152
+ advisor: string | undefined,
153
+ pendingExecutorRef: string | undefined,
154
+ persisted: ReturnType<typeof getPersistedModelRefs>,
155
+ executorOverride: boolean,
156
+ advisorOverride: boolean,
157
+ availableRefs: Set<string> | undefined
158
+ ): ActivationModelPlan => {
159
+ const pendingExecutor =
160
+ !executorOverride &&
161
+ pendingExecutorRef &&
162
+ isSelectableModel(ctx, pendingExecutorRef, availableRefs)
163
+ ? pendingExecutorRef
164
+ : undefined;
165
+ const executorConfigured =
166
+ executorOverride ||
167
+ Boolean(pendingExecutor) ||
168
+ Boolean(
169
+ persisted.executor && isSelectableModel(ctx, executor, availableRefs)
170
+ );
171
+ const advisorConfigured =
172
+ advisorOverride ||
173
+ Boolean(
174
+ persisted.advisor && isSelectableModel(ctx, advisor, availableRefs)
175
+ );
176
+ return {
177
+ pendingExecutor,
178
+ selectAdvisor: !advisorConfigured,
179
+ selectExecutor: !executorConfigured,
180
+ };
181
+ };
@@ -0,0 +1,110 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { SearchableModelSelector } from "../ui/model-selector.js";
3
+ import {
4
+ effortChoices,
5
+ getAvailableModelRefs,
6
+ selectedEffort,
7
+ } from "./model-options.js";
8
+
9
+ export interface AdvisorModelSelection {
10
+ advisor: string;
11
+ advisorEffort: string | undefined;
12
+ executor: string;
13
+ executorEffort: string | undefined;
14
+ }
15
+
16
+ export interface AdvisorModelPickerOptions {
17
+ advisor: string | undefined;
18
+ advisorEffort: string | undefined;
19
+ executor: string | undefined;
20
+ executorEffort: string | undefined;
21
+ selectAdvisor: boolean;
22
+ selectExecutor: boolean;
23
+ }
24
+
25
+ export const selectAdvisorModels = async (
26
+ ctx: ExtensionContext,
27
+ options: AdvisorModelPickerOptions
28
+ ): Promise<AdvisorModelSelection | undefined> => {
29
+ if (!ctx.hasUI) {
30
+ return undefined;
31
+ }
32
+ const refs = getAvailableModelRefs(ctx);
33
+ if (refs && refs.length === 0) {
34
+ ctx.ui.notify(
35
+ "No selectable models are available. Configure a provider with /login or models.json, then retry.",
36
+ "error"
37
+ );
38
+ return undefined;
39
+ }
40
+ const allOptions = [
41
+ ...new Set(
42
+ refs ??
43
+ [options.executor, options.advisor].filter((ref): ref is string =>
44
+ Boolean(ref)
45
+ )
46
+ ),
47
+ ];
48
+ let { advisor, advisorEffort, executor, executorEffort } = options;
49
+
50
+ if (options.selectExecutor) {
51
+ const selectedExecutor = await ctx.ui.custom<string | undefined>(
52
+ (tui, theme, keybindings, done) =>
53
+ new SearchableModelSelector({
54
+ allOptions,
55
+ currentOption: executor || undefined,
56
+ keybindings,
57
+ onCancel: () => done(undefined),
58
+ onSelect: done,
59
+ theme,
60
+ title: "Select Executor Model",
61
+ tui,
62
+ })
63
+ );
64
+ if (!selectedExecutor) {
65
+ return undefined;
66
+ }
67
+ const selectedExecutorEffort = await ctx.ui.select(
68
+ "Select Executor Reasoning/Thinking Level",
69
+ effortChoices(executorEffort)
70
+ );
71
+ if (!selectedExecutorEffort) {
72
+ return undefined;
73
+ }
74
+ executor = selectedExecutor;
75
+ executorEffort = selectedEffort(selectedExecutorEffort);
76
+ }
77
+
78
+ if (options.selectAdvisor) {
79
+ const selectedAdvisor = await ctx.ui.custom<string | undefined>(
80
+ (tui, theme, keybindings, done) =>
81
+ new SearchableModelSelector({
82
+ allOptions,
83
+ currentOption: advisor || undefined,
84
+ keybindings,
85
+ onCancel: () => done(undefined),
86
+ onSelect: done,
87
+ theme,
88
+ title: "Select Advisor Model",
89
+ tui,
90
+ })
91
+ );
92
+ if (!selectedAdvisor) {
93
+ return undefined;
94
+ }
95
+ const selectedAdvisorEffort = await ctx.ui.select(
96
+ "Select Advisor Reasoning/Thinking Level",
97
+ effortChoices(advisorEffort)
98
+ );
99
+ if (!selectedAdvisorEffort) {
100
+ return undefined;
101
+ }
102
+ advisor = selectedAdvisor;
103
+ advisorEffort = selectedEffort(selectedAdvisorEffort);
104
+ }
105
+
106
+ if (!(advisor && executor)) {
107
+ return undefined;
108
+ }
109
+ return { advisor, advisorEffort, executor, executorEffort };
110
+ };
@@ -0,0 +1,27 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { activateAdvisor } from "./activation.js";
6
+ import { registerCommandLifecycle } from "./lifecycle.js";
7
+ import { registerManualCommand } from "./manual-command.js";
8
+ import { registerModelCommands } from "./model-commands.js";
9
+ import { registerCommandRenderers } from "./renderers.js";
10
+ import { createCommandRuntime } from "./runtime.js";
11
+ import { registerSettingsCommands } from "./settings-commands.js";
12
+ import type { CommandDependencies } from "./types.js";
13
+
14
+ export const registerCommands = (
15
+ pi: ExtensionAPI,
16
+ dependencies: CommandDependencies = {}
17
+ ) => {
18
+ const runtime = createCommandRuntime(pi, dependencies);
19
+ const activate = (args: string, ctx: ExtensionContext, announce = true) =>
20
+ activateAdvisor(runtime, args, ctx, announce);
21
+
22
+ registerCommandRenderers(runtime);
23
+ registerCommandLifecycle(runtime, activate);
24
+ registerManualCommand(runtime);
25
+ registerModelCommands(runtime);
26
+ registerSettingsCommands(runtime);
27
+ };
@@ -0,0 +1,76 @@
1
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Markdown, Text } from "@earendil-works/pi-tui";
3
+ import { getAdvisorSettings } from "../config/state.js";
4
+ import {
5
+ adviceForDisplay,
6
+ hasSoundVerdict,
7
+ renderAdvisorCallBox,
8
+ renderAdvisorResponseHeader,
9
+ } from "../tools/render-common.js";
10
+ import { formatAdvisorUsage } from "../usage.js";
11
+ import { ManualAdvisorProgressComponent } from "./manual-progress.js";
12
+ import type { CommandRuntime } from "./types.js";
13
+
14
+ export const registerCommandRenderers = (runtime: CommandRuntime) => {
15
+ runtime.pi.registerEntryRenderer?.(
16
+ "advisor-manual-call",
17
+ (entry, { expanded }, theme) => {
18
+ const { progressId, question } = (entry.data ?? {}) as {
19
+ progressId?: string;
20
+ question?: string;
21
+ };
22
+ const progress = progressId
23
+ ? runtime.manualProgress.get(progressId)
24
+ : undefined;
25
+ return progress
26
+ ? new ManualAdvisorProgressComponent(
27
+ question,
28
+ progress,
29
+ Boolean(expanded),
30
+ theme
31
+ )
32
+ : renderAdvisorCallBox(question, theme);
33
+ }
34
+ );
35
+
36
+ runtime.pi.registerMessageRenderer?.(
37
+ "advisor-manual-result",
38
+ (message, { expanded }, theme) => {
39
+ const details = message.details as
40
+ | { advisor?: string; text?: string; usage?: unknown }
41
+ | undefined;
42
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
43
+ const advice =
44
+ details?.text ??
45
+ (typeof message.content === "string"
46
+ ? message.content
47
+ : "(Advisor returned no advice.)");
48
+ // Manual consultations must render exactly like an Executor ask_advisor call.
49
+ box.addChild(
50
+ new Text(
51
+ renderAdvisorResponseHeader(hasSoundVerdict(advice), theme),
52
+ 0,
53
+ 0
54
+ )
55
+ );
56
+ if (details?.advisor) {
57
+ box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
58
+ }
59
+ if (getAdvisorSettings().showUsageDetails) {
60
+ const usage = formatAdvisorUsage(details?.usage);
61
+ if (usage) {
62
+ box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
63
+ }
64
+ }
65
+ box.addChild(
66
+ new Markdown(
67
+ adviceForDisplay(advice, expanded),
68
+ 0,
69
+ 0,
70
+ getMarkdownTheme()
71
+ )
72
+ );
73
+ return box;
74
+ }
75
+ );
76
+ };
@@ -0,0 +1,114 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { getAdvisorSettings } from "../config/state.js";
6
+ import { notifyHerdrAdvisorFailure } from "../herdr.js";
7
+ import type { AdvisorSessionState } from "../session-state.js";
8
+ import { consultAdvisor } from "../tools/consultation.js";
9
+ import { ScoutStatusManager } from "../tools/scout-status.js";
10
+ import { advisorSessionState as defaultAdvisorSessionState } from "../tools/session.js";
11
+ import type {
12
+ CommandDependencies,
13
+ CommandRuntime as CommandRuntimeContract,
14
+ ManualAdvisorProgressState,
15
+ ManualConsult,
16
+ } from "./types.js";
17
+
18
+ export const notify = (
19
+ ctx: ExtensionContext,
20
+ message: string,
21
+ level: "error" | "info" | "warning"
22
+ ) => {
23
+ if (ctx.hasUI) {
24
+ ctx.ui.notify(message, level);
25
+ }
26
+ };
27
+
28
+ export class CommandRuntime implements CommandRuntimeContract {
29
+ readonly advisorSessionState: AdvisorSessionState;
30
+ readonly manualConsultations = new Map<AbortController, symbol>();
31
+ readonly manualProgress = new Map<string, ManualAdvisorProgressState>();
32
+ readonly manualProgressTimers = new Map<
33
+ AbortController,
34
+ ReturnType<typeof setInterval>
35
+ >();
36
+ readonly pi: ExtensionAPI;
37
+ readonly requestAdvisor: ManualConsult;
38
+ readonly scoutStatus: ScoutStatusManager;
39
+ manualProgressSequence = 0;
40
+ pendingExecutorModelRef: string | undefined;
41
+ suppressModelSelectionSync = false;
42
+
43
+ constructor(pi: ExtensionAPI, dependencies: CommandDependencies = {}) {
44
+ this.pi = pi;
45
+ this.advisorSessionState =
46
+ dependencies.sessionState ?? defaultAdvisorSessionState;
47
+ this.scoutStatus =
48
+ dependencies.statusManager ?? new ScoutStatusManager(false);
49
+ this.requestAdvisor =
50
+ dependencies.consult ??
51
+ ((ctx, question, signal, onChunk, onScout, gitContext) =>
52
+ consultAdvisor(
53
+ ctx,
54
+ question,
55
+ signal,
56
+ onChunk,
57
+ "manual",
58
+ gitContext,
59
+ undefined,
60
+ undefined,
61
+ undefined,
62
+ onScout,
63
+ undefined
64
+ ));
65
+ }
66
+
67
+ flowEnabled() {
68
+ return this.pi.getActiveTools().includes("ask_advisor");
69
+ }
70
+
71
+ nextManualProgressId() {
72
+ this.manualProgressSequence += 1;
73
+ return `manual-${this.manualProgressSequence}`;
74
+ }
75
+
76
+ async setExecutorModel(
77
+ model: Parameters<ExtensionAPI["setModel"]>[0]
78
+ ): Promise<boolean> {
79
+ this.suppressModelSelectionSync = true;
80
+ try {
81
+ return await this.pi.setModel(model);
82
+ } finally {
83
+ this.suppressModelSelectionSync = false;
84
+ }
85
+ }
86
+
87
+ updateAdvisorUsageStatus(ctx: ExtensionContext) {
88
+ if (ctx.hasUI) {
89
+ ctx.ui.setStatus(
90
+ "advisor-usage",
91
+ getAdvisorSettings().showUsageFooter
92
+ ? this.advisorSessionState.usageStatus()
93
+ : undefined
94
+ );
95
+ }
96
+ }
97
+
98
+ reportManualBudgetExhausted(ctx: ExtensionContext) {
99
+ const message = "Advisor call budget exhausted for this session.";
100
+ notify(ctx, message, "warning");
101
+ notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
102
+ }
103
+
104
+ requestManualRender(ctx: ExtensionContext) {
105
+ if (ctx.hasUI) {
106
+ ctx.ui.setStatus("advisor-manual", undefined);
107
+ }
108
+ }
109
+ }
110
+
111
+ export const createCommandRuntime = (
112
+ pi: ExtensionAPI,
113
+ dependencies: CommandDependencies = {}
114
+ ) => new CommandRuntime(pi, dependencies);