pi-advisor-flow 0.7.0 → 0.8.1-dev.1

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 (93) hide show
  1. package/CHANGELOG.md +27 -18
  2. package/README.md +9 -8
  3. package/dist/index.js +1968 -1081
  4. package/extensions/index.ts +2 -1
  5. package/package.json +48 -35
  6. package/src/attachments.ts +11 -10
  7. package/src/child-session.ts +15 -0
  8. package/src/commands/activation-preparation.ts +1 -0
  9. package/src/commands/activation.ts +12 -3
  10. package/src/commands/lifecycle.ts +7 -1
  11. package/src/commands/manual-command.ts +7 -1
  12. package/src/commands/manual-consultation.ts +102 -92
  13. package/src/commands/manual-progress.ts +7 -5
  14. package/src/commands/model-commands.ts +1 -1
  15. package/src/commands/model-options.ts +14 -3
  16. package/src/commands/model-picker.ts +1 -0
  17. package/src/commands/registration.ts +1 -0
  18. package/src/commands/renderers.ts +67 -55
  19. package/src/commands/runtime.ts +16 -14
  20. package/src/commands/settings-commands.ts +10 -4
  21. package/src/commands/settings-persistence.ts +16 -2
  22. package/src/commands/types.ts +1 -0
  23. package/src/commands.ts +0 -1
  24. package/src/config/args.ts +1 -1
  25. package/src/config/defaults.ts +8 -1
  26. package/src/config/schema.ts +42 -17
  27. package/src/config/state.ts +12 -3
  28. package/src/config/storage.ts +34 -23
  29. package/src/config/types.ts +16 -3
  30. package/src/config/validation.ts +33 -21
  31. package/src/config.ts +3 -0
  32. package/src/content-utils.ts +24 -4
  33. package/src/conversation.ts +16 -15
  34. package/src/git.ts +10 -3
  35. package/src/herdr-block.ts +83 -0
  36. package/src/herdr-shared.ts +108 -0
  37. package/src/herdr.ts +30 -192
  38. package/src/jev/client.ts +75 -85
  39. package/src/jev/failure.ts +19 -0
  40. package/src/jev/key-store.ts +26 -11
  41. package/src/jev/ledger.ts +58 -64
  42. package/src/jev/questions.ts +26 -23
  43. package/src/jev/state.ts +15 -12
  44. package/src/jev/transport.ts +8 -3
  45. package/src/model-stream.ts +35 -37
  46. package/src/outcomes.ts +15 -11
  47. package/src/pi-settings.ts +4 -9
  48. package/src/preferences.ts +4 -2
  49. package/src/redaction.ts +10 -12
  50. package/src/scout-context.ts +59 -60
  51. package/src/scout-curation.ts +4 -13
  52. package/src/scout-groups.ts +178 -175
  53. package/src/scout-protocol.ts +74 -70
  54. package/src/scout-reconstruct.ts +1 -1
  55. package/src/scout.ts +128 -77
  56. package/src/session-state.ts +64 -49
  57. package/src/tool-result-cap.ts +18 -17
  58. package/src/tools/consult-context.ts +7 -15
  59. package/src/tools/consultation.ts +14 -4
  60. package/src/tools/gate-policy.ts +1 -0
  61. package/src/tools/gate-protocol.ts +36 -23
  62. package/src/tools/jev-filter.ts +43 -41
  63. package/src/tools/jev-turn-gate.ts +29 -9
  64. package/src/tools/loop-gate.ts +31 -20
  65. package/src/tools/model-access.ts +46 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +36 -16
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +146 -128
  71. package/src/tools/registration.ts +12 -14
  72. package/src/tools/render-advisor-result.ts +35 -22
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +1 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +65 -59
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +5 -11
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +38 -1
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +157 -70
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +14 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +26 -19
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import { registerCommands } from "../src/commands/registration.ts";
3
4
  import { setHerdrBlockedEmitter } from "../src/herdr.ts";
4
5
  import { AdvisorSessionState } from "../src/session-state.ts";
@@ -30,7 +31,7 @@ export const runAdvisorGate = (
30
31
  ...args: Parameters<typeof runAdvisorGateImplementation>
31
32
  ) => runAdvisorGateImplementation(...args);
32
33
 
33
- export default function (pi: ExtensionAPI) {
34
+ export default function registerPiAdvisor(pi: ExtensionAPI) {
34
35
  const sessionState = new AdvisorSessionState();
35
36
  const scoutStatus = new ScoutStatusManager();
36
37
  setHerdrBlockedEmitter((active, label) =>
package/package.json CHANGED
@@ -1,26 +1,23 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.7.0",
3
+ "version": "0.8.1-dev.1",
4
4
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
5
  "keywords": [
6
- "pi-package",
7
- "pi-extension",
8
- "pi-coding-agent",
9
6
  "advisor",
10
- "pi-advisor",
11
7
  "herdr",
12
- "jev"
8
+ "jev",
9
+ "pi-advisor",
10
+ "pi-coding-agent",
11
+ "pi-extension",
12
+ "pi-package"
13
13
  ],
14
14
  "homepage": "https://github.com/philipbrembeck/pi-advisor",
15
+ "license": "MIT",
16
+ "author": "Philip Brembeck",
15
17
  "repository": {
16
18
  "type": "git",
17
19
  "url": "https://github.com/philipbrembeck/pi-advisor.git"
18
20
  },
19
- "license": "MIT",
20
- "author": "Philip Brembeck",
21
- "type": "module",
22
- "main": "dist/index.js",
23
- "types": "extensions/index.ts",
24
21
  "files": [
25
22
  "dist",
26
23
  "extensions",
@@ -29,6 +26,9 @@
29
26
  "LICENSE",
30
27
  "README.md"
31
28
  ],
29
+ "type": "module",
30
+ "main": "dist/index.js",
31
+ "types": "extensions/index.ts",
32
32
  "scripts": {
33
33
  "bench:decisions": "bun bench/src/cli.ts decisions",
34
34
  "bench:evaluate": "bun bench/src/cli.ts evaluate",
@@ -36,10 +36,10 @@
36
36
  "bench:screen": "bun bench/src/cli.ts screen",
37
37
  "build": "bun build ./extensions/index.ts --outfile dist/index.js --target node --format esm --external @earendil-works/pi-ai --external @earendil-works/pi-coding-agent --external @earendil-works/pi-tui --external typebox",
38
38
  "check:boundaries": "node scripts/check-module-boundaries.mjs",
39
- "format": "bunx ultracite fix --linter-enabled=false",
39
+ "format": "oxfmt",
40
40
  "knip": "knip",
41
- "lint": "bunx ultracite check",
42
- "lint:fix": "bunx ultracite fix",
41
+ "lint": "oxlint",
42
+ "lint:fix": "oxlint --fix",
43
43
  "prepack": "bun run build >/dev/null",
44
44
  "package:check": "node scripts/check-package.mjs",
45
45
  "prepare": "husky",
@@ -47,40 +47,30 @@
47
47
  "test": "bun test && bun run test:node-loader",
48
48
  "test:bench": "bun --config=./bunfig.bench.toml test ./bench/test/",
49
49
  "test:node-loader": "node --test test/default-resource-loader.node.mjs",
50
- "typecheck": "tsc --noEmit"
51
- },
52
- "lint-staged": {
53
- "*.{json,jsonc,ts}": "bun run lint:fix --"
54
- },
55
- "resolutions": {
56
- "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
57
- "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9"
58
- },
59
- "overrides": {
60
- "brace-expansion": "5.0.9",
61
- "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
62
- "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9",
63
- "undici": "8.9.0"
50
+ "typecheck": "tsc --noEmit",
51
+ "format:check": "oxfmt --check"
64
52
  },
65
53
  "devDependencies": {
66
- "@biomejs/biome": "2.5.13",
67
- "@earendil-works/pi-ai": "^0.85.1",
68
- "@earendil-works/pi-coding-agent": "^0.85.1",
69
- "@earendil-works/pi-tui": "^0.85.1",
54
+ "@earendil-works/pi-ai": "0.87.1",
55
+ "@earendil-works/pi-coding-agent": "0.87.1",
56
+ "@earendil-works/pi-tui": "0.87.1",
57
+ "@oxlint/plugins": "1.85.0",
70
58
  "@types/node": "^26.6.2",
71
59
  "@typesafe-ai/sdk": "^0.6.0",
72
60
  "bun-types": "1.4.2",
73
61
  "husky": "^9.1.7",
74
62
  "knip": "^6.37.0",
75
63
  "lint-staged": "^17.5.1",
64
+ "oxfmt": "0.70.0",
65
+ "oxlint": "1.85.0",
76
66
  "typebox": "^1.3.34",
77
67
  "typescript": "^7.0.2",
78
68
  "ultracite": "7.12.0"
79
69
  },
80
70
  "peerDependencies": {
81
- "@earendil-works/pi-ai": "^0.84.1 || ^0.85.1",
82
- "@earendil-works/pi-coding-agent": "^0.84.1 || ^0.85.1",
83
- "@earendil-works/pi-tui": "^0.84.1 || ^0.85.1",
71
+ "@earendil-works/pi-ai": "^0.85.1 || ^0.87.1",
72
+ "@earendil-works/pi-coding-agent": "^0.85.1 || ^0.87.1",
73
+ "@earendil-works/pi-tui": "^0.85.1 || ^0.87.1",
84
74
  "typebox": "^1.1.38"
85
75
  },
86
76
  "peerDependenciesMeta": {
@@ -97,6 +87,29 @@
97
87
  "optional": true
98
88
  }
99
89
  },
90
+ "resolutions": {
91
+ "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
92
+ "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9"
93
+ },
94
+ "overrides": {
95
+ "brace-expansion": "5.0.9",
96
+ "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
97
+ "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9",
98
+ "undici": "8.9.0"
99
+ },
100
+ "lint-staged": {
101
+ "*.{ts,tsx}": [
102
+ "oxfmt",
103
+ "oxlint --fix"
104
+ ],
105
+ "{src,test,scripts,tools,extensions}/**/*.{ts,tsx}": [
106
+ "oxfmt",
107
+ "oxlint --fix"
108
+ ],
109
+ "*.{json,jsonc}": [
110
+ "oxfmt"
111
+ ]
112
+ },
100
113
  "pi": {
101
114
  "extensions": [
102
115
  "./dist/index.js"
@@ -2,6 +2,8 @@ import { execFileSync } from "node:child_process";
2
2
  import { constants } from "node:fs";
3
3
  import { lstat, open, realpath } from "node:fs/promises";
4
4
  import { isAbsolute, relative, resolve } from "node:path";
5
+
6
+ import { isString } from "./content-utils.ts";
5
7
  import { redactAndCapText } from "./redaction.ts";
6
8
 
7
9
  export const ADVISOR_FILE_MAX_BYTES = 8 * 1024;
@@ -12,7 +14,7 @@ export interface UntrackedAttachment {
12
14
  text: string;
13
15
  }
14
16
 
15
- const PATH_SEGMENTS = /[\\/]/;
17
+ const PATH_SEGMENTS = /[\\/]/u;
16
18
 
17
19
  const within = (root: string, candidate: string) => {
18
20
  const path = relative(root, candidate);
@@ -21,9 +23,9 @@ const within = (root: string, candidate: string) => {
21
23
  const normalizeRelativePath = (root: string, path: string) =>
22
24
  relative(root, resolve(root, path));
23
25
 
24
- const normalizeRequestedPath = (root: string, value: unknown) => {
26
+ const normalizeRequestedPath = (root: string, value: string) => {
25
27
  if (
26
- typeof value !== "string" ||
28
+ !isString(value) ||
27
29
  !value ||
28
30
  isAbsolute(value) ||
29
31
  value.split(PATH_SEGMENTS).includes("..")
@@ -36,7 +38,7 @@ const normalizeRequestedPath = (root: string, value: unknown) => {
36
38
  const git = (cwd: string, args: string[]) =>
37
39
  execFileSync("git", args, {
38
40
  cwd,
39
- encoding: "utf8",
41
+ encoding: "utf-8",
40
42
  maxBuffer: 16 * 1024 * 1024,
41
43
  shell: false,
42
44
  stdio: ["ignore", "pipe", "pipe"],
@@ -48,7 +50,7 @@ const repositoryRoot = (cwd: string) => {
48
50
  try {
49
51
  return realpath(git(cwd, ["rev-parse", "--show-toplevel"]).trim());
50
52
  } catch {
51
- return Promise.resolve(undefined);
53
+ return Promise.resolve();
52
54
  }
53
55
  };
54
56
 
@@ -108,9 +110,9 @@ const readAttachment = async (
108
110
  if (!within(root, resolved)) {
109
111
  return;
110
112
  }
113
+ // fs.open needs combined numeric flags for O_NOFOLLOW; O_RDONLY (0) and O_NOFOLLOW occupy disjoint bits, so + is exact.
111
114
  const flags = constants.O_NOFOLLOW
112
- ? // biome-ignore lint/suspicious/noBitwiseOperators: fs.open requires numeric flags for O_NOFOLLOW.
113
- constants.O_RDONLY | constants.O_NOFOLLOW
115
+ ? constants.O_RDONLY + constants.O_NOFOLLOW
114
116
  : constants.O_RDONLY;
115
117
  const file = await open(resolved, flags);
116
118
  try {
@@ -120,13 +122,13 @@ const readAttachment = async (
120
122
  }
121
123
  const buffer = Buffer.alloc(available + 1);
122
124
  const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
123
- const raw = buffer.subarray(0, bytesRead).toString("utf8");
125
+ const raw = buffer.subarray(0, bytesRead).toString("utf-8");
124
126
  if (raw.includes("\0")) {
125
127
  return;
126
128
  }
127
129
  const text = redactAndCapText(raw, available, redact);
128
130
  return {
129
- bytes: Buffer.byteLength(text, "utf8"),
131
+ bytes: Buffer.byteLength(text, "utf-8"),
130
132
  path: normalizedName,
131
133
  text,
132
134
  };
@@ -169,7 +171,6 @@ const readFiles = async (
169
171
  break;
170
172
  }
171
173
  // Sequentially enforce the aggregate disclosure budget.
172
- // biome-ignore lint/performance/noAwaitInLoops: each accepted file consumes the remaining budget.
173
174
  const attachment = await readAttachment(
174
175
  root,
175
176
  normalizedName,
@@ -0,0 +1,15 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { executorEffortRef, executorRef } from "./config/state.ts";
4
+
5
+ export const isMarkedSubagent = () => process.env.PI_SUBAGENT_CHILD === "1";
6
+
7
+ export const effectiveExecutorRef = (ctx: ExtensionContext) => {
8
+ if (isMarkedSubagent() && ctx.model) {
9
+ return `${ctx.model.provider}/${ctx.model.id}`;
10
+ }
11
+ return executorRef;
12
+ };
13
+
14
+ export const effectiveExecutorEffort = (ctx: ExtensionContext) =>
15
+ isMarkedSubagent() ? ctx.thinkingLevel : executorEffortRef;
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import {
3
4
  advisorEffortRef,
4
5
  advisorRef,
@@ -1,4 +1,10 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import {
4
+ effectiveExecutorEffort,
5
+ effectiveExecutorRef,
6
+ isMarkedSubagent,
7
+ } from "../child-session.ts";
2
8
  import { parseArgs } from "../config/args.ts";
3
9
  import {
4
10
  advisorEffortRef,
@@ -50,7 +56,7 @@ const resolveActivationModels = async (
50
56
  if (!(advisorAuth.ok && advisorAuth.apiKey)) {
51
57
  return { error: `No API key for Advisor ${advisorRef}` };
52
58
  }
53
- if (!(await runtime.setExecutorModel(executor))) {
59
+ if (!isMarkedSubagent() && !(await runtime.setExecutorModel(executor))) {
54
60
  return { error: `No API key for Executor ${executorRef}` };
55
61
  }
56
62
  return {};
@@ -115,7 +121,8 @@ export const activateAdvisor = async (
115
121
  // an older inactive selection override an explicit activation argument on a
116
122
  // later attempt.
117
123
  runtime.pendingExecutorModelRef = undefined;
118
- if (executorEffortRef) {
124
+ if (executorEffortRef && !isMarkedSubagent()) {
125
+ // SAFETY: executorEffortRef is operator-configured and trusted to name a ThinkingLevel.
119
126
  runtime.pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
120
127
  }
121
128
  if (!runtime.flowEnabled()) {
@@ -126,9 +133,11 @@ export const activateAdvisor = async (
126
133
  ]);
127
134
  }
128
135
  if (announce) {
136
+ const activeExecutorRef = effectiveExecutorRef(ctx);
137
+ const activeExecutorEffort = effectiveExecutorEffort(ctx);
129
138
  notify(
130
139
  ctx,
131
- `${ADVISOR_ACTIVATION_EXPLANATION}\n\nAdvisor flow ready — Executor: ${executorRef} (thinking: ${executorEffortRef || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`,
140
+ `${ADVISOR_ACTIVATION_EXPLANATION}\n\nAdvisor flow ready — Executor: ${activeExecutorRef} (thinking: ${activeExecutorEffort || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`,
132
141
  "info"
133
142
  );
134
143
  }
@@ -1,4 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { isMarkedSubagent } from "../child-session.ts";
2
4
  import {
3
5
  alwaysOnRef,
4
6
  executorRef,
@@ -39,7 +41,11 @@ export const registerCommandLifecycle = (
39
41
  // "restore" replays a stored session model and "cycle" changes the active
40
42
  // model without an explicit `/model` choice. Neither should redefine the
41
43
  // configured Executor.
42
- if (event.source !== "set" || runtime.suppressModelSelectionSync) {
44
+ if (
45
+ event.source !== "set" ||
46
+ runtime.suppressModelSelectionSync ||
47
+ isMarkedSubagent()
48
+ ) {
43
49
  return;
44
50
  }
45
51
  const selected = `${event.model.provider}/${event.model.id}`;
@@ -4,11 +4,13 @@ import {
4
4
  isSimpleMode,
5
5
  } from "../config/state.ts";
6
6
  import type { GitContextLevel } from "../git.ts";
7
+ import { advisorModelAccessReason } from "../tools/model-access.ts";
7
8
  import { resolveAdvisorRequest } from "../tools/render-common.ts";
8
9
  import { ManualAdvisorDialog } from "../ui/manual-dialog.ts";
9
10
  import type { ManualAdvisorRequest } from "../ui/types.ts";
10
11
  import { loadCommandConfig } from "./activation-preparation.ts";
11
12
  import { startManualConsultation } from "./manual-consultation.ts";
13
+ import { notify } from "./runtime.ts";
12
14
  import type { CommandRuntime, ManualAdvisorProgressState } from "./types.ts";
13
15
 
14
16
  export const registerManualCommand = (runtime: CommandRuntime) => {
@@ -19,6 +21,11 @@ export const registerManualCommand = (runtime: CommandRuntime) => {
19
21
  if (!loadCommandConfig(ctx)) {
20
22
  return;
21
23
  }
24
+ const accessReason = advisorModelAccessReason(ctx);
25
+ if (accessReason) {
26
+ notify(ctx, accessReason, "warning");
27
+ return;
28
+ }
22
29
  if (
23
30
  !(
24
31
  isSimpleMode() ||
@@ -96,7 +103,6 @@ export const registerManualCommand = (runtime: CommandRuntime) => {
96
103
  // Intentional fire-and-forget: the consultation streams after the command
97
104
  // handler returns. void satisfies noFloatingPromises; noVoid is ignored here
98
105
  // because this Biome version offers no ignoreVoidAsExpression option.
99
- // biome-ignore lint/complexity/noVoid: marks an intentional fire-and-forget promise
100
106
  void startManualConsultation(
101
107
  runtime,
102
108
  ctx,
@@ -1,15 +1,25 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import { advisorRef } from "../config/state.ts";
4
+ import { isString } from "../content-utils.ts";
3
5
  import type { GitContextLevel } from "../git.ts";
4
6
  import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "../herdr.ts";
5
7
  import { normalizeScreeningQuestion } from "../tools/jev-filter.ts";
6
8
  import { appendScoutLifecycleEntry } from "../tools/scout-status.ts";
7
9
  import type { ScoutToolDetails } from "../tools/types.ts";
8
10
  import { advisorUsageCost, snapshotAdvisorUsage } from "../usage.ts";
11
+ import type { AdvisorUsageSnapshot } from "../usage.ts";
9
12
  import { notify } from "./runtime.ts";
10
13
  import type { CommandRuntime, ManualAdvisorProgressState } from "./types.ts";
11
14
 
12
- export const startManualConsultation = (
15
+ interface ManualResultDetails {
16
+ advisor: string;
17
+ question?: string;
18
+ text: string;
19
+ usage?: AdvisorUsageSnapshot;
20
+ }
21
+
22
+ export const startManualConsultation = async (
13
23
  runtime: CommandRuntime,
14
24
  ctx: ExtensionContext,
15
25
  question: string | undefined,
@@ -35,8 +45,8 @@ export const startManualConsultation = (
35
45
  runtime.manualProgressTimers.set(controller, timer);
36
46
  }
37
47
  let scoutDetails: ScoutToolDetails | undefined;
38
- return runtime
39
- .requestAdvisor(
48
+ try {
49
+ const { adviceId, markdown, usage } = await runtime.requestAdvisor(
40
50
  ctx,
41
51
  question,
42
52
  controller.signal,
@@ -63,96 +73,96 @@ export const startManualConsultation = (
63
73
  }
64
74
  },
65
75
  gitContext
66
- )
67
- .then(({ adviceId, markdown, usage }) => {
68
- if (controller.signal.aborted) {
69
- return;
70
- }
71
- progress.phase = "complete";
72
- runtime.advisorSessionState.resetTurnsSinceConsultation();
73
- runtime.advisorSessionState.recordInvocation({
74
- cost: advisorUsageCost(usage),
75
- executionEffect: "continued",
76
- kind: "markdown",
77
- model: advisorRef,
78
- trigger: "manual",
79
- usage,
80
- });
81
- if (typeof adviceId === "string") {
82
- // Registering the manual question lets an identical later ask_advisor
83
- // hit repeat detection instead of re-consulting.
84
- runtime.advisorSessionState.issueAdvice(
85
- adviceId,
86
- markdown,
87
- "manual",
88
- false,
89
- normalizeScreeningQuestion(question)
90
- );
91
- }
92
- runtime.updateAdvisorUsageStatus(ctx);
93
- const normalizedUsage = snapshotAdvisorUsage(usage);
94
- runtime.pi.sendMessage(
95
- {
96
- content: `Manual Advisor consultation${question ? ` (${question})` : ""} — for your awareness; no action or follow-up consultation is needed unless the user asks:\n\n${markdown}`,
97
- customType: "advisor-manual-result",
98
- details: {
99
- advisor: advisorRef,
100
- question,
101
- text: markdown,
102
- ...(normalizedUsage ? { usage: normalizedUsage } : {}),
103
- },
104
- display: true,
105
- },
106
- {
107
- // Steer lets the current turn finish its active work; the Executor sees
108
- // the result before its next model call rather than being interrupted.
109
- deliverAs: "steer",
110
- triggerTurn: true,
111
- }
112
- );
113
- })
114
- .catch((error: unknown) => {
115
- if (controller.signal.aborted) {
116
- return;
117
- }
118
- progress.phase = "error";
119
- const message = error instanceof Error ? error.message : String(error);
120
- runtime.advisorSessionState.resetTurnsSinceConsultation();
121
- runtime.advisorSessionState.recordInvocation({
122
- executionEffect: "continued",
123
- failure: "provider-error",
124
- kind: "markdown",
125
- model: advisorRef,
126
- trigger: "manual",
127
- });
128
- runtime.updateAdvisorUsageStatus(ctx);
129
- runtime.pi.sendMessage(
130
- {
131
- content: `Manual Advisor consultation failed: ${message}`,
132
- customType: "advisor-manual-result",
133
- details: {
134
- advisor: advisorRef,
135
- text: `**Advisor consultation failed:** ${message}`,
136
- },
137
- display: true,
138
- },
139
- { deliverAs: "steer", triggerTurn: true }
76
+ );
77
+ if (controller.signal.aborted) {
78
+ return;
79
+ }
80
+ progress.phase = "complete";
81
+ runtime.advisorSessionState.resetTurnsSinceConsultation();
82
+ runtime.advisorSessionState.recordInvocation({
83
+ cost: advisorUsageCost(usage),
84
+ executionEffect: "continued",
85
+ kind: "markdown",
86
+ model: advisorRef,
87
+ trigger: "manual",
88
+ usage,
89
+ });
90
+ if (isString(adviceId)) {
91
+ // Registering the manual question lets an identical later ask_advisor
92
+ // hit repeat detection instead of re-consulting.
93
+ runtime.advisorSessionState.issueAdvice(
94
+ adviceId,
95
+ markdown,
96
+ "manual",
97
+ false,
98
+ normalizeScreeningQuestion(question)
140
99
  );
141
- notify(ctx, `Advisor consultation failed: ${message}`, "error");
142
- notifyHerdrAdvisorFailure("Advisor consultation failed", message);
143
- })
144
- .finally(() => {
145
- if (controller.signal.aborted) {
146
- progress.phase = "cancelled";
147
- }
148
- const timer = runtime.manualProgressTimers.get(controller);
149
- if (timer) {
150
- clearInterval(timer);
151
- runtime.manualProgressTimers.delete(controller);
100
+ }
101
+ runtime.updateAdvisorUsageStatus(ctx);
102
+ const details: ManualResultDetails = {
103
+ advisor: advisorRef,
104
+ question,
105
+ text: markdown,
106
+ };
107
+ const normalizedUsage = snapshotAdvisorUsage(usage);
108
+ if (normalizedUsage) {
109
+ details.usage = normalizedUsage;
110
+ }
111
+ runtime.pi.sendMessage(
112
+ {
113
+ content: `Manual Advisor consultation${question ? ` (${question})` : ""} — for your awareness; no action or follow-up consultation is needed unless the user asks:\n\n${markdown}`,
114
+ customType: "advisor-manual-result",
115
+ details,
116
+ display: true,
117
+ },
118
+ {
119
+ // Steer lets the current turn finish its active work; the Executor sees
120
+ // the result before its next model call rather than being interrupted.
121
+ deliverAs: "steer",
122
+ triggerTurn: true,
152
123
  }
153
- runtime.requestManualRender(ctx);
154
- runtime.scoutStatus.release(ctx, scoutStatusToken);
155
- runtime.manualConsultations.delete(controller);
156
- herdrAdvisorActivity.finish();
124
+ );
125
+ } catch (error) {
126
+ if (controller.signal.aborted) {
127
+ return;
128
+ }
129
+ progress.phase = "error";
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ runtime.advisorSessionState.resetTurnsSinceConsultation();
132
+ runtime.advisorSessionState.recordInvocation({
133
+ executionEffect: "continued",
134
+ failure: "provider-error",
135
+ kind: "markdown",
136
+ model: advisorRef,
137
+ trigger: "manual",
157
138
  });
139
+ runtime.updateAdvisorUsageStatus(ctx);
140
+ runtime.pi.sendMessage(
141
+ {
142
+ content: `Manual Advisor consultation failed: ${message}`,
143
+ customType: "advisor-manual-result",
144
+ details: {
145
+ advisor: advisorRef,
146
+ text: `**Advisor consultation failed:** ${message}`,
147
+ },
148
+ display: true,
149
+ },
150
+ { deliverAs: "steer", triggerTurn: true }
151
+ );
152
+ notify(ctx, `Advisor consultation failed: ${message}`, "error");
153
+ notifyHerdrAdvisorFailure("Advisor consultation failed", message);
154
+ } finally {
155
+ if (controller.signal.aborted) {
156
+ progress.phase = "cancelled";
157
+ }
158
+ const timer = runtime.manualProgressTimers.get(controller);
159
+ if (timer) {
160
+ clearInterval(timer);
161
+ runtime.manualProgressTimers.delete(controller);
162
+ }
163
+ runtime.requestManualRender(ctx);
164
+ runtime.scoutStatus.release(ctx, scoutStatusToken);
165
+ runtime.manualConsultations.delete(controller);
166
+ herdrAdvisorActivity.finish();
167
+ }
158
168
  };
@@ -1,7 +1,11 @@
1
- import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
2
- import { type Component, Markdown, Text } from "@earendil-works/pi-tui";
1
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { Markdown, Text } from "@earendil-works/pi-tui";
4
+ import type { Component } from "@earendil-works/pi-tui";
5
+
3
6
  import {
4
7
  adviceForDisplay,
8
+ noop,
5
9
  renderAdvisorCallBox,
6
10
  renderThinkingMarkdown,
7
11
  SPINNER_FRAMES,
@@ -87,7 +91,5 @@ export class ManualAdvisorProgressComponent implements Component {
87
91
  return box.render(width);
88
92
  }
89
93
 
90
- invalidate(): void {
91
- // The live progress state is read during each render.
92
- }
94
+ readonly invalidate = noop;
93
95
  }
@@ -10,8 +10,8 @@ import {
10
10
  setExecutorRef,
11
11
  } from "../config/state.ts";
12
12
  import { saveConfig } from "../config/storage.ts";
13
- import { activateAdvisor } from "./activation.ts";
14
13
  import { loadCommandConfig } from "./activation-preparation.ts";
14
+ import { activateAdvisor } from "./activation.ts";
15
15
  import { selectAdvisorModels } from "./model-picker.ts";
16
16
  import type { CommandRuntime } from "./types.ts";
17
17
 
@@ -1,5 +1,7 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { type getPersistedModelRefs, splitRef } from "../config/state.ts";
2
+
3
+ import { splitRef } from "../config/state.ts";
4
+ import type { getPersistedModelRefs } from "../config/state.ts";
3
5
  import type { ContextPreset } from "../ui/types.ts";
4
6
 
5
7
  export const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
@@ -33,7 +35,7 @@ export const selectedEffort = (choice: string): string | undefined => {
33
35
  export const ADVISOR_ACTIVATION_EXPLANATION =
34
36
  "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
37
 
36
- const ARGUMENT_WHITESPACE = /\s+/;
38
+ const ARGUMENT_WHITESPACE = /\s+/u;
37
39
  const hasModelOverride = (args: string, key: "advisor" | "executor") =>
38
40
  args
39
41
  .trim()
@@ -91,13 +93,14 @@ export const findConfiguredModel = (
91
93
  return;
92
94
  }
93
95
  const [provider, modelId] = splitRef(ref);
96
+ // oxlint-disable-next-line unicorn/no-array-method-this-argument -- Pi ModelRegistry.find(provider, id), not Array#find.
94
97
  return ctx.modelRegistry.find(provider, modelId);
95
98
  };
96
99
 
97
100
  export const getAvailableModelRefs = (
98
101
  ctx: ExtensionContext
99
102
  ): string[] | undefined => {
100
- if (typeof ctx.modelRegistry.getAvailable !== "function") {
103
+ if (!ctx.modelRegistry.getAvailable) {
101
104
  return undefined;
102
105
  }
103
106
  return ctx.modelRegistry
@@ -105,6 +108,14 @@ export const getAvailableModelRefs = (
105
108
  .map((model) => `${model.provider}/${model.id}`);
106
109
  };
107
110
 
111
+ export const getConfiguredModelRefs = (ctx: ExtensionContext): string[] => {
112
+ const registry = ctx.modelRegistry;
113
+ const models = registry?.getAvailable ? registry.getAvailable() : [];
114
+ return [
115
+ ...new Set(models.map((model) => `${model.provider}/${model.id}`)),
116
+ ].toSorted((left, right) => left.localeCompare(right));
117
+ };
118
+
108
119
  const isSelectableModel = (
109
120
  ctx: ExtensionContext,
110
121
  ref: string | undefined,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import { SearchableModelSelector } from "../ui/model-selector.ts";
3
4
  import {
4
5
  effortChoices,