pi-advisor-flow 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -45,6 +45,10 @@ Enables the Advisor flow. Switches the primary model to the configured Executor
45
45
  - _Example:_ `/advisor executor=anthropic/claude-sonnet-5 advisor=openai/gpt-5.6-sol`
46
46
  - _Context size:_ `/advisor contextMaxChars=30000` uses up to 30,000 characters of the reconstructed conversation for each consultation. `0` disables history; `Number.MAX_SAFE_INTEGER` represents the complete branch. Larger values increase request cost and can exceed the Advisor model's context window.
47
47
 
48
+ ### `/advisor-manual [focus]`
49
+
50
+ Starts an Advisor consultation in parallel without interrupting the Executor's active tool work. An optional `focus` is passed to the Advisor; when it completes, the advice is delivered to the Executor before its next model call. This works while the Executor is mid-turn.
51
+
48
52
  ### `/advisor-settings`
49
53
 
50
54
  Opens a single keyboard-navigable settings screen. It includes a Claude Code-style context slider with `0`, `10k`, `25k`, `100k`, `200k`, and `ALL`; `0` sends no reconstructed history and `ALL` sends the complete current branch, subject to the Advisor model's context limit.
@@ -89,6 +93,13 @@ All fields are optional. `executor`, `advisor`, and their effort settings are ma
89
93
 
90
94
  Disables the Advisor flow, removing the `ask_advisor` tool from the active session.
91
95
 
96
+ ## Publishing releases
97
+
98
+ Pushing a version tag (`vX.Y.Z`) runs the release workflow. It verifies that the tag matches `package.json`, type-checks, tests, then publishes:
99
+
100
+ - `pi-advisor-flow` to [npm](https://www.npmjs.com/package/pi-advisor-flow)
101
+ - `@philipbrembeck/pi-advisor-flow` to GitHub Packages, which makes the package appear in this repository’s **Packages** sidebar
102
+
92
103
  ## Local Development
93
104
 
94
105
  `pi-advisor` uses Bun for rapid testing and TypeScript. Standard commands apply:
@@ -112,5 +123,5 @@ Verify code-splitting correctness and registration logic:
112
123
 
113
124
  ```bash
114
125
  bun test # Run unit tests
115
- npm run typecheck # Perform strict TS checks
126
+ bun run typecheck # Perform strict TS checks
116
127
  ```
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+ssh://git@github.com/philipbrembeck/pi-advisor.git"
7
+ "url": "https://github.com/philipbrembeck/pi-advisor.git"
8
8
  },
9
9
  "homepage": "https://github.com/philipbrembeck/pi-advisor",
10
10
  "author": "Philip Brembeck",
package/src/commands.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import { getMarkdownTheme, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Markdown, Text } from "@earendil-works/pi-tui";
2
3
  import {
3
4
  advisorCollapseResponsesRef, advisorCompletionGateRef, advisorCustomInvocationRef, advisorFailureGateRef, advisorPlanGateRef,
4
5
  advisorRef, advisorEffortRef, executorRef, executorEffortRef,
@@ -7,6 +8,7 @@ import {
7
8
  contextMaxCharsRef, loadConfig, saveConfig, parseArgs, splitRef,
8
9
  } from "./config.js";
9
10
  import { AdvisorSettingsSelector, SearchableModelSelector, type AdvisorSettings, type ContextPreset } from "./ui.js";
11
+ import { adviceForDisplay, consult, renderAdvisorCallBox, resolveAdvisorRequest } from "./tools.js";
10
12
 
11
13
  const EFFORT_LEVELS = ["Default (Model Default)", "off", "minimal", "low", "medium", "high", "xhigh", "max"];
12
14
 
@@ -19,8 +21,66 @@ const CONTEXT_PRESETS: ContextPreset[] = [
19
21
  { label: "ALL", value: Number.MAX_SAFE_INTEGER, description: "The complete reconstructed conversation branch. Cost and model context limits apply." },
20
22
  ];
21
23
 
22
- export const registerCommands = (pi: ExtensionAPI) => {
24
+ export const registerCommands = (pi: ExtensionAPI, dependencies: { consult?: typeof consult } = {}) => {
23
25
  const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
26
+ const requestAdvisor = dependencies.consult ?? consult;
27
+ const manualConsultations = new Set<AbortController>();
28
+
29
+ pi.registerEntryRenderer?.("advisor-manual-call", (entry, _options, theme) => {
30
+ const { question } = (entry.data ?? {}) as { question?: string };
31
+ return renderAdvisorCallBox(question, theme);
32
+ });
33
+
34
+ pi.registerMessageRenderer?.("advisor-manual-result", (message, { expanded }, theme) => {
35
+ const details = message.details as { advisor?: string; text?: string } | undefined;
36
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
37
+ box.addChild(new Text(theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE")), 0, 0));
38
+ if (details?.advisor) box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
39
+ const advice = details?.text ?? (typeof message.content === "string" ? message.content : "(Advisor returned no advice.)");
40
+ box.addChild(new Markdown(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme()));
41
+ return box;
42
+ });
43
+
44
+ pi.on("session_shutdown", () => {
45
+ for (const controller of manualConsultations) controller.abort();
46
+ manualConsultations.clear();
47
+ });
48
+
49
+ pi.registerCommand("advisor-manual", {
50
+ description: "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
51
+ handler: async (args, ctx) => {
52
+ const question = resolveAdvisorRequest(args);
53
+ // A single visible progress surface avoids competing consultations overwriting
54
+ // each other's streamed state. A newer manual request replaces the previous one.
55
+ for (const pending of manualConsultations) pending.abort();
56
+ manualConsultations.clear();
57
+ const controller = new AbortController();
58
+ manualConsultations.add(controller);
59
+ pi.appendEntry?.("advisor-manual-call", { question });
60
+
61
+ void requestAdvisor(ctx, question, controller.signal)
62
+ .then(({ advice }) => {
63
+ if (controller.signal.aborted) return;
64
+ pi.sendMessage({
65
+ customType: "advisor-manual-result",
66
+ content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${advice}`,
67
+ display: true,
68
+ details: { advisor: advisorRef, question, text: advice },
69
+ }, {
70
+ // Steer lets the current turn finish its active work; the Executor sees
71
+ // the result before its next model call rather than being interrupted.
72
+ deliverAs: "steer",
73
+ triggerTurn: true,
74
+ });
75
+ })
76
+ .catch((error: unknown) => {
77
+ if (controller.signal.aborted) return;
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ if (ctx.hasUI) ctx.ui.notify(`Advisor consultation failed: ${message}`, "error");
80
+ })
81
+ .finally(() => manualConsultations.delete(controller));
82
+ },
83
+ });
24
84
 
25
85
  pi.registerCommand("advisor", {
26
86
  description: "Enable the Executor/Advisor flow and switch to the configured Executor model; accepts contextMaxChars=N",
package/src/tools.ts CHANGED
@@ -13,6 +13,14 @@ export const resolveAdvisorRequest = (question?: string) => question?.trim() ||
13
13
  export const advisorMessageText = (conversation: string, question?: string) =>
14
14
  `${conversation ? `<conversation>\n${conversation}\n</conversation>` : ""}${question ? `\n\nTargeted focus:\n${question}` : ""}`;
15
15
 
16
+ export const renderAdvisorCallBox = (question: string | undefined, theme: any) => {
17
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
18
+ const label = theme.fg("customMessageLabel", theme.bold("[advisor]"));
19
+ const title = theme.fg("customMessageText", "Executor → Advisor");
20
+ box.addChild(new Text(question ? `${label} ${title}\n${theme.fg("dim", ` ${question}`)}` : `${label} ${title}`, 0, 0));
21
+ return box;
22
+ };
23
+
16
24
  const COLLAPSED_ADVICE_LINES = 12;
17
25
 
18
26
  export const adviceForDisplay = (advice: string, expanded: boolean) => {
@@ -112,15 +120,8 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
112
120
  ],
113
121
  parameters: Type.Object({ question: Type.Optional(Type.String({ description: "The specific question or decision to get advice on. Omit this for normal reviews: the Advisor already has the conversation context." })) }),
114
122
  renderShell: "self",
115
- renderCall(args, theme, context) {
116
- const box = context.lastComponent instanceof Box ? context.lastComponent : new Box(1, 1, (text) => theme.bg("customMessageBg", text));
117
- box.setBgFn((text) => theme.bg("customMessageBg", text));
118
- box.clear();
119
- const request = args.question?.trim();
120
- const label = theme.fg("customMessageLabel", theme.bold("[advisor]"));
121
- const title = theme.fg("customMessageText", "Executor → Advisor");
122
- box.addChild(new Text(request ? `${label} ${title}\n${theme.fg("dim", ` ${request}`)}` : `${label} ${title}`, 0, 0));
123
- return box;
123
+ renderCall(args, theme, _context) {
124
+ return renderAdvisorCallBox(args.question?.trim(), theme);
124
125
  },
125
126
  renderResult(result, { isPartial, expanded }, theme, context) {
126
127
  const box = context.lastComponent instanceof Box ? context.lastComponent : new Box(1, 1, (text) => theme.bg("customMessageBg", text));