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
@@ -0,0 +1,107 @@
1
+ import {
2
+ getAdvisorMaxCallsPerSession,
3
+ getAdvisorSettings,
4
+ isSimpleMode,
5
+ } from "../config/state.js";
6
+ import type { GitContextLevel } from "../git.js";
7
+ import { resolveAdvisorRequest } from "../tools/render-common.js";
8
+ import { ManualAdvisorDialog } from "../ui/manual-dialog.js";
9
+ import type { ManualAdvisorRequest } from "../ui/types.js";
10
+ import { loadCommandConfig } from "./activation-preparation.js";
11
+ import { startManualConsultation } from "./manual-consultation.js";
12
+ import type { CommandRuntime, ManualAdvisorProgressState } from "./types.js";
13
+
14
+ export const registerManualCommand = (runtime: CommandRuntime) => {
15
+ runtime.pi.registerCommand("advisor-manual", {
16
+ description:
17
+ "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
18
+ handler: async (args, ctx) => {
19
+ if (!loadCommandConfig(ctx)) {
20
+ return;
21
+ }
22
+ if (
23
+ !(
24
+ isSimpleMode() ||
25
+ runtime.advisorSessionState.canConsult(getAdvisorMaxCallsPerSession())
26
+ )
27
+ ) {
28
+ runtime.reportManualBudgetExhausted(ctx);
29
+ return;
30
+ }
31
+
32
+ let gitContext: GitContextLevel | undefined;
33
+ let question: string | undefined;
34
+ if (ctx.mode === "tui") {
35
+ const request = await ctx.ui.custom<ManualAdvisorRequest | undefined>(
36
+ (tui, theme, keybindings, done) =>
37
+ new ManualAdvisorDialog({
38
+ gitContext: getAdvisorSettings().gitContext,
39
+ initialMessage: args,
40
+ keybindings,
41
+ onCancel: () => done(undefined),
42
+ onSubmit: done,
43
+ theme,
44
+ tui,
45
+ }),
46
+ {
47
+ overlay: true,
48
+ overlayOptions: {
49
+ anchor: "center",
50
+ margin: 2,
51
+ maxHeight: "80%",
52
+ minWidth: 56,
53
+ width: 76,
54
+ },
55
+ }
56
+ );
57
+ if (!request) {
58
+ return;
59
+ }
60
+ if (
61
+ !(
62
+ isSimpleMode() ||
63
+ runtime.advisorSessionState.canConsult(
64
+ getAdvisorMaxCallsPerSession()
65
+ )
66
+ )
67
+ ) {
68
+ runtime.reportManualBudgetExhausted(ctx);
69
+ return;
70
+ }
71
+ const { gitContext: selectedGitContext, message } = request;
72
+ gitContext = selectedGitContext;
73
+ question = resolveAdvisorRequest(message);
74
+ } else {
75
+ question = resolveAdvisorRequest(args);
76
+ }
77
+
78
+ if (!isSimpleMode()) {
79
+ runtime.advisorSessionState.consumeCall();
80
+ }
81
+ // A single visible progress surface avoids competing consultations overwriting
82
+ // each other's streamed state. A newer manual request replaces the previous one.
83
+ for (const [pending, token] of runtime.manualConsultations) {
84
+ pending.abort();
85
+ runtime.scoutStatus.release(ctx, token);
86
+ }
87
+ runtime.manualConsultations.clear();
88
+ const controller = new AbortController();
89
+ const scoutStatusToken = Symbol("manual-scout");
90
+ const progressId = runtime.nextManualProgressId();
91
+ const progress: ManualAdvisorProgressState = { phase: "preparing" };
92
+ runtime.manualProgress.set(progressId, progress);
93
+ runtime.scoutStatus.register(scoutStatusToken);
94
+ runtime.manualConsultations.set(controller, scoutStatusToken);
95
+ runtime.pi.appendEntry?.("advisor-manual-call", { progressId, question });
96
+ startManualConsultation(
97
+ runtime,
98
+ ctx,
99
+ question,
100
+ controller,
101
+ scoutStatusToken,
102
+ progress,
103
+ gitContext
104
+ );
105
+ },
106
+ });
107
+ };
@@ -0,0 +1,144 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { advisorRef } from "../config/state.js";
3
+ import type { GitContextLevel } from "../git.js";
4
+ import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "../herdr.js";
5
+ import { appendScoutLifecycleEntry } from "../tools/scout-status.js";
6
+ import type { ScoutToolDetails } from "../tools/types.js";
7
+ import { advisorUsageCost, snapshotAdvisorUsage } from "../usage.js";
8
+ import { notify } from "./runtime.js";
9
+ import type { CommandRuntime, ManualAdvisorProgressState } from "./types.js";
10
+
11
+ export const startManualConsultation = (
12
+ runtime: CommandRuntime,
13
+ ctx: ExtensionContext,
14
+ question: string | undefined,
15
+ controller: AbortController,
16
+ scoutStatusToken: symbol,
17
+ progress: ManualAdvisorProgressState,
18
+ gitContext?: GitContextLevel
19
+ ) => {
20
+ herdrAdvisorActivity.start();
21
+ progress.phase = "preparing";
22
+ runtime.requestManualRender(ctx);
23
+ if (ctx.hasUI) {
24
+ const timer = setInterval(() => {
25
+ if (
26
+ controller.signal.aborted ||
27
+ runtime.manualConsultations.get(controller) !== scoutStatusToken
28
+ ) {
29
+ clearInterval(timer);
30
+ return;
31
+ }
32
+ runtime.requestManualRender(ctx);
33
+ }, 80);
34
+ runtime.manualProgressTimers.set(controller, timer);
35
+ }
36
+ let scoutDetails: ScoutToolDetails | undefined;
37
+ return runtime
38
+ .requestAdvisor(
39
+ ctx,
40
+ question,
41
+ controller.signal,
42
+ (thinking, text) => {
43
+ if (controller.signal.aborted) {
44
+ return;
45
+ }
46
+ progress.phase = "active";
47
+ progress.thinking = thinking;
48
+ progress.text = text;
49
+ runtime.requestManualRender(ctx);
50
+ },
51
+ (event) => {
52
+ if (!controller.signal.aborted) {
53
+ runtime.scoutStatus.update(ctx, scoutStatusToken, event);
54
+ scoutDetails = appendScoutLifecycleEntry(
55
+ runtime.pi,
56
+ event,
57
+ scoutDetails
58
+ );
59
+ progress.scout = scoutDetails;
60
+ progress.phase = "active";
61
+ runtime.requestManualRender(ctx);
62
+ }
63
+ },
64
+ gitContext
65
+ )
66
+ .then(({ markdown, usage }) => {
67
+ if (controller.signal.aborted) {
68
+ return;
69
+ }
70
+ progress.phase = "complete";
71
+ runtime.advisorSessionState.recordInvocation({
72
+ cost: advisorUsageCost(usage),
73
+ executionEffect: "continued",
74
+ kind: "markdown",
75
+ model: advisorRef,
76
+ trigger: "manual",
77
+ usage,
78
+ });
79
+ runtime.updateAdvisorUsageStatus(ctx);
80
+ const normalizedUsage = snapshotAdvisorUsage(usage);
81
+ runtime.pi.sendMessage(
82
+ {
83
+ content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${markdown}`,
84
+ customType: "advisor-manual-result",
85
+ details: {
86
+ advisor: advisorRef,
87
+ question,
88
+ text: markdown,
89
+ ...(normalizedUsage ? { usage: normalizedUsage } : {}),
90
+ },
91
+ display: true,
92
+ },
93
+ {
94
+ // Steer lets the current turn finish its active work; the Executor sees
95
+ // the result before its next model call rather than being interrupted.
96
+ deliverAs: "steer",
97
+ triggerTurn: true,
98
+ }
99
+ );
100
+ })
101
+ .catch((error: unknown) => {
102
+ if (controller.signal.aborted) {
103
+ return;
104
+ }
105
+ progress.phase = "error";
106
+ const message = error instanceof Error ? error.message : String(error);
107
+ runtime.advisorSessionState.recordInvocation({
108
+ executionEffect: "continued",
109
+ failure: "provider-error",
110
+ kind: "markdown",
111
+ model: advisorRef,
112
+ trigger: "manual",
113
+ });
114
+ runtime.updateAdvisorUsageStatus(ctx);
115
+ runtime.pi.sendMessage(
116
+ {
117
+ content: `Manual Advisor consultation failed: ${message}`,
118
+ customType: "advisor-manual-result",
119
+ details: {
120
+ advisor: advisorRef,
121
+ text: `**Advisor consultation failed:** ${message}`,
122
+ },
123
+ display: true,
124
+ },
125
+ { deliverAs: "steer", triggerTurn: true }
126
+ );
127
+ notify(ctx, `Advisor consultation failed: ${message}`, "error");
128
+ notifyHerdrAdvisorFailure("Advisor consultation failed", message);
129
+ })
130
+ .finally(() => {
131
+ if (controller.signal.aborted) {
132
+ progress.phase = "cancelled";
133
+ }
134
+ const timer = runtime.manualProgressTimers.get(controller);
135
+ if (timer) {
136
+ clearInterval(timer);
137
+ runtime.manualProgressTimers.delete(controller);
138
+ }
139
+ runtime.requestManualRender(ctx);
140
+ runtime.scoutStatus.release(ctx, scoutStatusToken);
141
+ runtime.manualConsultations.delete(controller);
142
+ herdrAdvisorActivity.finish();
143
+ });
144
+ };
@@ -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
+ };