@juicesharp/rpiv-advisor 0.1.2 → 0.1.3

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/advisor-ui.ts ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * advisor-ui — bordered select-panel builders for the /advisor command.
3
+ *
4
+ * Two public functions (showAdvisorPicker, showEffortPicker) share a private
5
+ * buildSelectPanel helper that owns the bordered-container layout and the
6
+ * SelectList theme wiring.
7
+ */
8
+
9
+ import { DynamicBorder, type ExtensionContext, type Theme } from "@mariozechner/pi-coding-agent";
10
+ import type { ThinkingLevel } from "@mariozechner/pi-ai";
11
+ import { Container, SelectList, Spacer, Text, type SelectItem } from "@mariozechner/pi-tui";
12
+
13
+ const MAX_VISIBLE_ROWS = 10;
14
+ const NAV_HINT = "↑↓ navigate • enter select • esc cancel";
15
+
16
+ const ADVISOR_HEADER_TITLE = "Advisor Tool";
17
+ const ADVISOR_HEADER_PROSE_1 =
18
+ "When the active model needs stronger judgment — a complex decision, an ambiguous " +
19
+ "failure, a problem it's circling without progress — it escalates to the " +
20
+ "advisor model for guidance, then resumes. The advisor runs server-side " +
21
+ "and uses additional tokens.";
22
+ const ADVISOR_HEADER_PROSE_2 =
23
+ "For certain workloads, pairing a faster model as the main model with a " +
24
+ "more capable one as the advisor gives near-top-tier performance with " +
25
+ "reduced token usage.";
26
+
27
+ const EFFORT_HEADER_TITLE = "Reasoning Level";
28
+ const EFFORT_HEADER_PROSE =
29
+ "Choose the reasoning effort level for the advisor. " +
30
+ "Higher levels produce stronger judgment but use more tokens.";
31
+
32
+ function selectListTheme(theme: Theme) {
33
+ return {
34
+ selectedPrefix: (t: string) => theme.bg("selectedBg", theme.fg("accent", t)),
35
+ selectedText: (t: string) => theme.bg("selectedBg", theme.bold(t)),
36
+ description: (t: string) => theme.fg("muted", t),
37
+ scrollInfo: (t: string) => theme.fg("dim", t),
38
+ noMatch: (t: string) => theme.fg("warning", t),
39
+ };
40
+ }
41
+
42
+ function buildSelectPanel(
43
+ theme: Theme,
44
+ title: string,
45
+ proseLines: string[],
46
+ selectList: SelectList,
47
+ ): Container {
48
+ const container = new Container();
49
+ const border = () => new DynamicBorder((s: string) => theme.fg("accent", s));
50
+
51
+ container.addChild(border());
52
+ container.addChild(new Spacer(1));
53
+ container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
54
+ container.addChild(new Spacer(1));
55
+ for (const line of proseLines) {
56
+ container.addChild(new Text(line, 1, 0));
57
+ container.addChild(new Spacer(1));
58
+ }
59
+ container.addChild(selectList);
60
+ container.addChild(new Spacer(1));
61
+ container.addChild(new Text(theme.fg("dim", NAV_HINT), 1, 0));
62
+ container.addChild(new Spacer(1));
63
+ container.addChild(border());
64
+ return container;
65
+ }
66
+
67
+ export async function showAdvisorPicker(
68
+ ctx: ExtensionContext,
69
+ items: SelectItem[],
70
+ ): Promise<string | null> {
71
+ return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
72
+ const selectList = new SelectList(
73
+ items,
74
+ Math.min(items.length, MAX_VISIBLE_ROWS),
75
+ selectListTheme(theme),
76
+ );
77
+ selectList.onSelect = (item) => done(item.value);
78
+ selectList.onCancel = () => done(null);
79
+
80
+ const container = buildSelectPanel(
81
+ theme,
82
+ ADVISOR_HEADER_TITLE,
83
+ [ADVISOR_HEADER_PROSE_1, ADVISOR_HEADER_PROSE_2],
84
+ selectList,
85
+ );
86
+
87
+ return {
88
+ render: (w) => container.render(w),
89
+ invalidate: () => container.invalidate(),
90
+ handleInput: (data) => {
91
+ selectList.handleInput(data);
92
+ tui.requestRender();
93
+ },
94
+ };
95
+ });
96
+ }
97
+
98
+ export async function showEffortPicker(
99
+ ctx: ExtensionContext,
100
+ items: SelectItem[],
101
+ currentEffort: ThinkingLevel | undefined,
102
+ defaultEffort: ThinkingLevel,
103
+ ): Promise<string | null> {
104
+ return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
105
+ const selectList = new SelectList(
106
+ items,
107
+ Math.min(items.length, MAX_VISIBLE_ROWS),
108
+ selectListTheme(theme),
109
+ );
110
+ const preferredIdx = currentEffort
111
+ ? items.findIndex((item) => item.value === currentEffort)
112
+ : -1;
113
+ selectList.setSelectedIndex(
114
+ preferredIdx >= 0
115
+ ? preferredIdx
116
+ : items.findIndex((item) => item.value === defaultEffort),
117
+ );
118
+ selectList.onSelect = (item) => done(item.value);
119
+ selectList.onCancel = () => done(null);
120
+
121
+ const container = buildSelectPanel(
122
+ theme,
123
+ EFFORT_HEADER_TITLE,
124
+ [EFFORT_HEADER_PROSE],
125
+ selectList,
126
+ );
127
+
128
+ return {
129
+ render: (w) => container.render(w),
130
+ invalidate: () => container.invalidate(),
131
+ handleInput: (data) => {
132
+ selectList.handleInput(data);
133
+ tui.requestRender();
134
+ },
135
+ };
136
+ });
137
+ }
package/advisor.ts CHANGED
@@ -16,10 +16,10 @@
16
16
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { dirname, join } from "node:path";
18
18
  import { homedir } from "node:os";
19
+ import { fileURLToPath } from "node:url";
19
20
  import { completeSimple, supportsXhigh, type Message, type ThinkingLevel } from "@mariozechner/pi-ai";
20
21
  import type { Api, Model, StopReason, Usage } from "@mariozechner/pi-ai";
21
22
  import {
22
- DynamicBorder,
23
23
  convertToLlm,
24
24
  serializeConversation,
25
25
  type AgentToolResult,
@@ -28,20 +28,66 @@ import {
28
28
  type ExtensionContext,
29
29
  type SessionEntry,
30
30
  } from "@mariozechner/pi-coding-agent";
31
- import {
32
- Container,
33
- SelectList,
34
- Spacer,
35
- Text,
36
- type SelectItem,
37
- } from "@mariozechner/pi-tui";
31
+ import type { SelectItem } from "@mariozechner/pi-tui";
38
32
  import { Type } from "@sinclair/typebox";
33
+ import { showAdvisorPicker, showEffortPicker } from "./advisor-ui.js";
39
34
 
40
35
  // ---------------------------------------------------------------------------
41
- // Constants
36
+ // Constants — grouped by concern, flat named consts (no namespaced objects)
42
37
  // ---------------------------------------------------------------------------
43
38
 
39
+ // Tool identity
44
40
  export const ADVISOR_TOOL_NAME = "advisor";
41
+ const TOOL_LABEL = "Advisor";
42
+
43
+ // Persistence
44
+ const CONFIG_DIR = join(homedir(), ".config", "rpiv-advisor");
45
+ const ADVISOR_CONFIG_PATH = join(CONFIG_DIR, "advisor.json");
46
+ const CONFIG_FILE_MODE = 0o600;
47
+
48
+ // Selector sentinels — double-underscore form is collision-proof against real provider:id keys
49
+ const NO_ADVISOR_VALUE = "__no_advisor__";
50
+ const OFF_VALUE = "__off__";
51
+
52
+ // Effort levels
53
+ const BASE_EFFORT_LEVELS: ThinkingLevel[] = ["minimal", "low", "medium", "high"];
54
+ const XHIGH_EFFORT_LEVEL: ThinkingLevel = "xhigh";
55
+ const DEFAULT_EFFORT: ThinkingLevel = "high";
56
+ const RECOMMENDED_EFFORT_SUFFIX = " (recommended)";
57
+
58
+ // UI — labels used by command flow; panel prose/titles live in advisor-ui.ts
59
+ const CHECKMARK = " ✓";
60
+
61
+ // Messages (static)
62
+ const MSG_ADVISOR_DISABLED = "Advisor disabled";
63
+ const MSG_REQUIRES_INTERACTIVE = "/advisor requires interactive mode";
64
+
65
+ // Errors (static)
66
+ const ERR_NO_MODEL =
67
+ "No advisor model is configured. The user can enable one with the /advisor command.";
68
+ const ERR_CALL_ABORTED = "Advisor call was cancelled before it completed.";
69
+ const ERR_EMPTY_RESPONSE = "Advisor returned no text content.";
70
+ const ERR_NO_MODEL_SELECTED = "no advisor model selected";
71
+ const ERR_EMPTY_RESPONSE_DETAIL = "empty response";
72
+ const ERR_ABORTED_DETAIL = "aborted";
73
+ const ERR_UNKNOWN = "unknown error";
74
+
75
+ // Errors/messages (parameterized)
76
+ const errMisconfigured = (label: string, err: string) =>
77
+ `Advisor (${label}) is misconfigured: ${err}`;
78
+ const errNoApiKey = (label: string) => `Advisor (${label}) has no API key available.`;
79
+ const errNoApiKeyDetail = (provider: string) => `no API key for ${provider}`;
80
+ const errCallFailed = (err: string | undefined) => `Advisor call failed: ${err ?? ERR_UNKNOWN}`;
81
+ const errCallThrew = (msg: string) => `Advisor call threw: ${msg}`;
82
+ const errSelectionNotFound = (choice: string) => `Advisor selection not found: ${choice}`;
83
+ const errModelUnavailable = (key: string) =>
84
+ `Previously configured advisor model ${key} is no longer available`;
85
+ const msgAdvisorEnabled = (label: string, effort: ThinkingLevel | undefined) =>
86
+ `Advisor: ${label}${effort ? `, ${effort}` : ""}`;
87
+ const msgAdvisorRestored = (label: string, effort: ThinkingLevel | undefined) =>
88
+ `Advisor restored: ${label}${effort ? `, ${effort}` : ""}`;
89
+ const msgConsulting = (label: string, effort: ThinkingLevel | undefined) =>
90
+ `Consulting advisor (${label}${effort ? `, ${effort}` : ""})…`;
45
91
 
46
92
  // ---------------------------------------------------------------------------
47
93
  // Config file persistence (cross-session)
@@ -52,8 +98,6 @@ interface AdvisorConfig {
52
98
  effort?: ThinkingLevel;
53
99
  }
54
100
 
55
- const ADVISOR_CONFIG_PATH = join(homedir(), ".config", "rpiv-advisor", "advisor.json");
56
-
57
101
  function loadAdvisorConfig(): AdvisorConfig {
58
102
  if (!existsSync(ADVISOR_CONFIG_PATH)) return {};
59
103
  try {
@@ -74,7 +118,7 @@ function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | unde
74
118
  // write may fail on disk-full or permission errors — best effort only
75
119
  }
76
120
  try {
77
- chmodSync(ADVISOR_CONFIG_PATH, 0o600);
121
+ chmodSync(ADVISOR_CONFIG_PATH, CONFIG_FILE_MODE);
78
122
  } catch {
79
123
  // chmod may fail on some filesystems — best effort only
80
124
  }
@@ -86,26 +130,14 @@ function parseModelKey(key: string): { provider: string; modelId: string } | und
86
130
  return { provider: key.slice(0, idx), modelId: key.slice(idx + 1) };
87
131
  }
88
132
 
89
- export const ADVISOR_SYSTEM_PROMPT = `You are an advisor model in an advisor-strategy pattern. An executor model is running a task end-to-end — calling tools, reading results, iterating toward a solution. When the executor hits a decision it cannot reasonably solve alone, it consults you for guidance.
90
-
91
- You read the shared conversation context and return ONE of:
92
- - a plan (concrete next steps the executor should take),
93
- - a correction (the executor is going down a wrong path — redirect it),
94
- - a stop signal (the executor should halt and escalate to the user).
95
-
96
- You NEVER call tools. You NEVER produce user-facing output. Be concise, directive, and grounded in the shared context. Name files, functions, and line numbers where possible. No preamble, no apologies, no meta-commentary about being an advisor — just the guidance the executor needs.`;
97
-
98
133
  // ---------------------------------------------------------------------------
99
- // Types
134
+ // System prompt — loaded once at module init from prompts/advisor-system.txt
100
135
  // ---------------------------------------------------------------------------
101
136
 
102
- export interface AdvisorDetails {
103
- advisorModel?: string;
104
- effort?: ThinkingLevel;
105
- usage?: Usage;
106
- stopReason?: StopReason;
107
- errorMessage?: string;
108
- }
137
+ export const ADVISOR_SYSTEM_PROMPT = readFileSync(
138
+ fileURLToPath(new URL("./prompts/advisor-system.txt", import.meta.url)),
139
+ "utf-8",
140
+ ).trimEnd();
109
141
 
110
142
  // ---------------------------------------------------------------------------
111
143
  // Module state — in-memory, resets each session
@@ -144,10 +176,7 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
144
176
  const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
145
177
  if (!model) {
146
178
  if (ctx.hasUI) {
147
- ctx.ui.notify(
148
- `Previously configured advisor model ${config.modelKey} is no longer available`,
149
- "warning",
150
- );
179
+ ctx.ui.notify(errModelUnavailable(config.modelKey), "warning");
151
180
  }
152
181
  return;
153
182
  }
@@ -163,10 +192,7 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
163
192
  }
164
193
 
165
194
  if (ctx.hasUI) {
166
- ctx.ui.notify(
167
- `Advisor restored: ${model.provider}:${model.id}${config.effort ? `, ${config.effort}` : ""}`,
168
- "info",
169
- );
195
+ ctx.ui.notify(msgAdvisorRestored(`${model.provider}:${model.id}`, config.effort), "info");
170
196
  }
171
197
  }
172
198
 
@@ -174,6 +200,14 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
174
200
  // Core execute logic — curate context, call advisor, return structured result
175
201
  // ---------------------------------------------------------------------------
176
202
 
203
+ export interface AdvisorDetails {
204
+ advisorModel?: string;
205
+ effort?: ThinkingLevel;
206
+ usage?: Usage;
207
+ stopReason?: StopReason;
208
+ errorMessage?: string;
209
+ }
210
+
177
211
  function buildErrorResult(
178
212
  advisorLabel: string | undefined,
179
213
  userText: string,
@@ -195,29 +229,20 @@ async function executeAdvisor(
195
229
  ): Promise<AgentToolResult<AdvisorDetails>> {
196
230
  const advisor = getAdvisorModel();
197
231
  if (!advisor) {
198
- return buildErrorResult(
199
- undefined,
200
- "No advisor model is configured. The user can enable one with the /advisor command.",
201
- "no advisor model selected",
202
- );
232
+ return buildErrorResult(undefined, ERR_NO_MODEL, ERR_NO_MODEL_SELECTED);
203
233
  }
204
234
  const advisorLabel = `${advisor.provider}:${advisor.id}`;
205
235
  const effort = getAdvisorEffort();
206
236
 
207
237
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
208
238
  if (!auth.ok) {
209
- return buildErrorResult(
210
- advisorLabel,
211
- `Advisor (${advisorLabel}) is misconfigured: ${auth.error}`,
212
- auth.error,
213
- );
239
+ return buildErrorResult(advisorLabel, errMisconfigured(advisorLabel, auth.error), auth.error);
214
240
  }
215
241
  if (!auth.apiKey) {
216
- const msg = `no API key for ${advisor.provider}`;
217
242
  return buildErrorResult(
218
243
  advisorLabel,
219
- `Advisor (${advisorLabel}) has no API key available.`,
220
- msg,
244
+ errNoApiKey(advisorLabel),
245
+ errNoApiKeyDetail(advisor.provider),
221
246
  );
222
247
  }
223
248
 
@@ -239,7 +264,7 @@ async function executeAdvisor(
239
264
  };
240
265
 
241
266
  onUpdate?.({
242
- content: [{ type: "text", text: `Consulting advisor (${advisorLabel}${effort ? `, ${effort}` : ""})…` }],
267
+ content: [{ type: "text", text: msgConsulting(advisorLabel, effort) }],
243
268
  details: { advisorModel: advisorLabel, effort },
244
269
  });
245
270
 
@@ -252,27 +277,20 @@ async function executeAdvisor(
252
277
 
253
278
  if (response.stopReason === "aborted") {
254
279
  return {
255
- content: [
256
- { type: "text", text: "Advisor call was cancelled before it completed." },
257
- ],
280
+ content: [{ type: "text", text: ERR_CALL_ABORTED }],
258
281
  details: {
259
282
  advisorModel: advisorLabel,
260
283
  effort,
261
284
  usage: response.usage,
262
285
  stopReason: response.stopReason,
263
- errorMessage: response.errorMessage ?? "aborted",
286
+ errorMessage: response.errorMessage ?? ERR_ABORTED_DETAIL,
264
287
  },
265
288
  };
266
289
  }
267
290
 
268
291
  if (response.stopReason === "error") {
269
292
  return {
270
- content: [
271
- {
272
- type: "text",
273
- text: `Advisor call failed: ${response.errorMessage ?? "unknown error"}`,
274
- },
275
- ],
293
+ content: [{ type: "text", text: errCallFailed(response.errorMessage) }],
276
294
  details: {
277
295
  advisorModel: advisorLabel,
278
296
  effort,
@@ -291,13 +309,13 @@ async function executeAdvisor(
291
309
 
292
310
  if (!advisorText) {
293
311
  return {
294
- content: [{ type: "text", text: "Advisor returned no text content." }],
312
+ content: [{ type: "text", text: ERR_EMPTY_RESPONSE }],
295
313
  details: {
296
314
  advisorModel: advisorLabel,
297
315
  effort,
298
316
  usage: response.usage,
299
317
  stopReason: response.stopReason,
300
- errorMessage: "empty response",
318
+ errorMessage: ERR_EMPTY_RESPONSE_DETAIL,
301
319
  },
302
320
  };
303
321
  }
@@ -313,11 +331,7 @@ async function executeAdvisor(
313
331
  };
314
332
  } catch (err) {
315
333
  const message = err instanceof Error ? err.message : String(err);
316
- return buildErrorResult(
317
- advisorLabel,
318
- `Advisor call threw: ${message}`,
319
- message,
320
- );
334
+ return buildErrorResult(advisorLabel, errCallThrew(message), message);
321
335
  }
322
336
  }
323
337
 
@@ -350,7 +364,7 @@ const ADVISOR_PROMPT_GUIDELINES: string[] = [
350
364
  export function registerAdvisorTool(pi: ExtensionAPI): void {
351
365
  pi.registerTool({
352
366
  name: ADVISOR_TOOL_NAME,
353
- label: "Advisor",
367
+ label: TOOL_LABEL,
354
368
  description: ADVISOR_DESCRIPTION,
355
369
  promptSnippet: ADVISOR_PROMPT_SNIPPET,
356
370
  promptGuidelines: ADVISOR_PROMPT_GUIDELINES,
@@ -381,27 +395,6 @@ export function registerAdvisorBeforeAgentStart(pi: ExtensionAPI): void {
381
395
  // /advisor slash command — opens selector panel for picking the advisor model
382
396
  // ---------------------------------------------------------------------------
383
397
 
384
- const ADVISOR_HEADER_TITLE = "Advisor Tool";
385
-
386
- const ADVISOR_HEADER_PROSE_1 =
387
- "When the active model needs stronger judgment — a complex decision, an ambiguous " +
388
- "failure, a problem it's circling without progress — it escalates to the " +
389
- "advisor model for guidance, then resumes. The advisor runs server-side " +
390
- "and uses additional tokens.";
391
-
392
- const ADVISOR_HEADER_PROSE_2 =
393
- "For certain workloads, pairing a faster model as the main model with a " +
394
- "more capable one as the advisor gives near-top-tier performance with " +
395
- "reduced token usage.";
396
-
397
- const NO_ADVISOR_VALUE = "__no_advisor__";
398
-
399
- const EFFORT_HEADER_TITLE = "Reasoning Level";
400
-
401
- const EFFORT_HEADER_PROSE =
402
- "Choose the reasoning effort level for the advisor. " +
403
- "Higher levels produce stronger judgment but use more tokens.";
404
-
405
398
  function modelKey(m: { provider: string; id: string }): string {
406
399
  return `${m.provider}:${m.id}`;
407
400
  }
@@ -411,7 +404,7 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
411
404
  description: "Configure the advisor model for the advisor-strategy pattern",
412
405
  handler: async (_args, ctx) => {
413
406
  if (!ctx.hasUI) {
414
- ctx.ui.notify("/advisor requires interactive mode", "error");
407
+ ctx.ui.notify(MSG_REQUIRES_INTERACTIVE, "error");
415
408
  return;
416
409
  }
417
410
 
@@ -421,74 +414,15 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
421
414
 
422
415
  const items: SelectItem[] = availableModels.map((m) => {
423
416
  const key = modelKey(m);
424
- const check = key === currentKey ? " ✓" : "";
417
+ const check = key === currentKey ? CHECKMARK : "";
425
418
  return { value: key, label: `${m.name} (${m.provider})${check}` };
426
419
  });
427
420
  items.push({
428
421
  value: NO_ADVISOR_VALUE,
429
- label: currentKey === undefined ? "No advisor ✓" : "No advisor",
422
+ label: currentKey === undefined ? `No advisor${CHECKMARK}` : "No advisor",
430
423
  });
431
424
 
432
- const choice = await ctx.ui.custom<string | null>(
433
- (tui, theme, _kb, done) => {
434
- const container = new Container();
435
-
436
- container.addChild(
437
- new DynamicBorder((s: string) => theme.fg("accent", s)),
438
- );
439
- container.addChild(new Spacer(1));
440
- container.addChild(
441
- new Text(
442
- theme.fg("accent", theme.bold(ADVISOR_HEADER_TITLE)),
443
- 1,
444
- 0,
445
- ),
446
- );
447
- container.addChild(new Spacer(1));
448
- container.addChild(new Text(ADVISOR_HEADER_PROSE_1, 1, 0));
449
- container.addChild(new Spacer(1));
450
- container.addChild(new Text(ADVISOR_HEADER_PROSE_2, 1, 0));
451
- container.addChild(new Spacer(1));
452
-
453
- const selectList = new SelectList(
454
- items,
455
- Math.min(items.length, 10),
456
- {
457
- selectedPrefix: (t) => theme.bg("selectedBg", theme.fg("accent", t)),
458
- selectedText: (t) => theme.bg("selectedBg", theme.bold(t)),
459
- description: (t) => theme.fg("muted", t),
460
- scrollInfo: (t) => theme.fg("dim", t),
461
- noMatch: (t) => theme.fg("warning", t),
462
- },
463
- );
464
- selectList.onSelect = (item) => done(item.value);
465
- selectList.onCancel = () => done(null);
466
- container.addChild(selectList);
467
-
468
- container.addChild(new Spacer(1));
469
- container.addChild(
470
- new Text(
471
- theme.fg("dim", "↑↓ navigate • enter select • esc cancel"),
472
- 1,
473
- 0,
474
- ),
475
- );
476
- container.addChild(new Spacer(1));
477
- container.addChild(
478
- new DynamicBorder((s: string) => theme.fg("accent", s)),
479
- );
480
-
481
- return {
482
- render: (w) => container.render(w),
483
- invalidate: () => container.invalidate(),
484
- handleInput: (data) => {
485
- selectList.handleInput(data);
486
- tui.requestRender();
487
- },
488
- };
489
- },
490
- );
491
-
425
+ const choice = await showAdvisorPicker(ctx, items);
492
426
  if (!choice) {
493
427
  return;
494
428
  }
@@ -505,100 +439,41 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
505
439
  activeTools.filter((n) => n !== ADVISOR_TOOL_NAME),
506
440
  );
507
441
  }
508
- ctx.ui.notify("Advisor disabled", "info");
442
+ ctx.ui.notify(MSG_ADVISOR_DISABLED, "info");
509
443
  return;
510
444
  }
511
445
 
512
446
  const picked = availableModels.find((m) => modelKey(m) === choice);
513
447
  if (!picked) {
514
- ctx.ui.notify(`Advisor selection not found: ${choice}`, "error");
448
+ ctx.ui.notify(errSelectionNotFound(choice), "error");
515
449
  return;
516
450
  }
517
451
 
518
452
  // Effort picker — only for reasoning-capable models
519
453
  let effortChoice: ThinkingLevel | undefined;
520
454
  if (picked.reasoning) {
521
- const OFF_VALUE = "__off__";
522
- const baseLevels: ThinkingLevel[] = ["minimal", "low", "medium", "high"];
523
455
  const levels = supportsXhigh(picked)
524
- ? [...baseLevels, "xhigh" as ThinkingLevel]
525
- : baseLevels;
456
+ ? [...BASE_EFFORT_LEVELS, XHIGH_EFFORT_LEVEL]
457
+ : BASE_EFFORT_LEVELS;
526
458
 
527
459
  const effortItems: SelectItem[] = [
528
460
  { value: OFF_VALUE, label: "off" },
529
461
  ...levels.map((level) => ({
530
462
  value: level,
531
- label: level === "high" ? `${level} (recommended)` : level,
463
+ label: level === DEFAULT_EFFORT ? `${level}${RECOMMENDED_EFFORT_SUFFIX}` : level,
532
464
  })),
533
465
  ];
534
466
 
535
- const effortResult = await ctx.ui.custom<string | null>(
536
- (tui, theme, _kb, done) => {
537
- const container = new Container();
538
-
539
- container.addChild(
540
- new DynamicBorder((s: string) => theme.fg("accent", s)),
541
- );
542
- container.addChild(new Spacer(1));
543
- container.addChild(
544
- new Text(
545
- theme.fg("accent", theme.bold(EFFORT_HEADER_TITLE)),
546
- 1,
547
- 0,
548
- ),
549
- );
550
- container.addChild(new Spacer(1));
551
- container.addChild(new Text(EFFORT_HEADER_PROSE, 1, 0));
552
- container.addChild(new Spacer(1));
553
-
554
- const selectList = new SelectList(
555
- effortItems,
556
- Math.min(effortItems.length, 10),
557
- {
558
- selectedPrefix: (t) => theme.bg("selectedBg", theme.fg("accent", t)),
559
- selectedText: (t) => theme.bg("selectedBg", theme.bold(t)),
560
- description: (t) => theme.fg("muted", t),
561
- scrollInfo: (t) => theme.fg("dim", t),
562
- noMatch: (t) => theme.fg("warning", t),
563
- },
564
- );
565
- const currentEffort = getAdvisorEffort();
566
- const defaultIdx = currentEffort
567
- ? effortItems.findIndex((item) => item.value === currentEffort)
568
- : -1;
569
- selectList.setSelectedIndex(defaultIdx >= 0 ? defaultIdx : effortItems.findIndex((item) => item.value === "high"));
570
- selectList.onSelect = (item) => done(item.value);
571
- selectList.onCancel = () => done(null);
572
- container.addChild(selectList);
573
-
574
- container.addChild(new Spacer(1));
575
- container.addChild(
576
- new Text(
577
- theme.fg("dim", "↑↓ navigate • enter select • esc cancel"),
578
- 1,
579
- 0,
580
- ),
581
- );
582
- container.addChild(new Spacer(1));
583
- container.addChild(
584
- new DynamicBorder((s: string) => theme.fg("accent", s)),
585
- );
586
-
587
- return {
588
- render: (w) => container.render(w),
589
- invalidate: () => container.invalidate(),
590
- handleInput: (data) => {
591
- selectList.handleInput(data);
592
- tui.requestRender();
593
- },
594
- };
595
- },
467
+ const effortResult = await showEffortPicker(
468
+ ctx,
469
+ effortItems,
470
+ getAdvisorEffort(),
471
+ DEFAULT_EFFORT,
596
472
  );
597
-
598
473
  if (!effortResult) {
599
474
  return;
600
475
  }
601
- effortChoice = effortResult === OFF_VALUE ? undefined : effortResult as ThinkingLevel;
476
+ effortChoice = effortResult === OFF_VALUE ? undefined : (effortResult as ThinkingLevel);
602
477
  }
603
478
 
604
479
  setAdvisorEffort(effortChoice);
@@ -607,10 +482,7 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
607
482
  if (!activeHas) {
608
483
  pi.setActiveTools([...activeTools, ADVISOR_TOOL_NAME]);
609
484
  }
610
- ctx.ui.notify(
611
- `Advisor: ${picked.provider}:${picked.id}${effortChoice ? `, ${effortChoice}` : ""}`,
612
- "info",
613
- );
485
+ ctx.ui.notify(msgAdvisorEnabled(modelKey(picked), effortChoice), "info");
614
486
  },
615
487
  });
616
488
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-advisor",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Pi extension: advisor-strategy pattern — escalate to a stronger reviewer model",
5
5
  "keywords": ["pi-package", "pi-extension", "rpiv", "advisor"],
6
6
  "type": "module",
@@ -17,6 +17,13 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
+ "files": [
21
+ "index.ts",
22
+ "advisor.ts",
23
+ "advisor-ui.ts",
24
+ "prompts/",
25
+ "README.md"
26
+ ],
20
27
  "pi": {
21
28
  "extensions": ["./index.ts"]
22
29
  },
@@ -0,0 +1,8 @@
1
+ You are an advisor model in an advisor-strategy pattern. An executor model is running a task end-to-end — calling tools, reading results, iterating toward a solution. When the executor hits a decision it cannot reasonably solve alone, it consults you for guidance.
2
+
3
+ You read the shared conversation context and return ONE of:
4
+ - a plan (concrete next steps the executor should take),
5
+ - a correction (the executor is going down a wrong path — redirect it),
6
+ - a stop signal (the executor should halt and escalate to the user).
7
+
8
+ You NEVER call tools. You NEVER produce user-facing output. Be concise, directive, and grounded in the shared context. Name files, functions, and line numbers where possible. No preamble, no apologies, no meta-commentary about being an advisor — just the guidance the executor needs.
package/docs/advisor.jpg DELETED
Binary file