@mrclrchtr/supi-review 2.7.0 → 2.8.0

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
@@ -134,9 +134,29 @@ Every `/supi-review` command run asks you to choose the reviewer model.
134
134
  - the picker only shows **scoped models** from Pi's `enabledModels` configuration
135
135
  - the current session model is preselected only when it is inside that scoped set
136
136
  - the selected model is used for both brief synthesis and the final review
137
- - no review model is persisted in settings
137
+ - the command selection is not persisted
138
138
 
139
- Agent-driven tool runs use the current session model for preparation and all focused reviewers. The selected model is retained in the prepared plan so a model change between tool calls cannot silently alter the run.
139
+ Agent-driven tool runs use the **Review Agent tool model** setting in `/supi-settings` when `@mrclrchtr/supi-settings` is installed:
140
+
141
+ - `current session model` is the default and preserves the active-session behavior
142
+ - an explicit choice stores a canonical `provider/model-id` from Pi's scoped `enabledModels` set
143
+ - the configured model is used for brief synthesis and every focused reviewer
144
+ - project settings override global settings through the normal SuPi settings scopes
145
+ - a configured model that is no longer scoped or available causes preparation to fail with a corrective error
146
+
147
+ The resolved model is retained in the prepared plan, so changing the setting or active session model between tool calls cannot silently alter the run.
148
+
149
+ For a standalone `supi-review` install without `supi-settings`, set the same value directly in the global `~/.pi/agent/supi/config.json` or project `.pi/supi/config.json` file:
150
+
151
+ ```json
152
+ {
153
+ "review": {
154
+ "agentModel": "openai/gpt-5"
155
+ }
156
+ }
157
+ ```
158
+
159
+ Use `"current"` instead of a canonical model id to follow the active session model.
140
160
 
141
161
  ## Result shape
142
162
 
@@ -175,7 +195,8 @@ When a successful review contains review items, `supi-review` also injects an ag
175
195
  ## Source
176
196
 
177
197
  - `src/review.ts` — command orchestration and interactive flow
178
- - `src/model.ts` — explicit model selection helpers
198
+ - `src/config.ts` — persisted agent-tool model setting and `/supi-settings` registration
199
+ - `src/model.ts` — explicit and configured model selection helpers
179
200
  - `src/git.ts` — git snapshot resolution
180
201
  - `src/history/collect.ts` — compaction-style session-context serialization
181
202
  - `src/history/synthesize.ts` — brief synthesis orchestration
@@ -43,7 +43,7 @@ Config file locations:
43
43
  - `registerSettingsCommand(pi)` — register `/supi-settings` (used by `@mrclrchtr/supi-settings`)
44
44
  - `openSettingsOverlay(pi, ctx)` — open the shared settings UI directly
45
45
  - `createInputSubmenu()` — helper for simple text-entry submenus
46
- - `createModelPickerSubmenu()` — helper for scoped model selection submenus
46
+ - `createModelPickerSubmenu()` — helper for scoped model selection submenus, with optional host-owned choices and `disabled` control
47
47
 
48
48
  The built-in settings UI supports:
49
49
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -324,6 +324,7 @@ export class ScopedSettingsList {
324
324
  this.tui.requestRender();
325
325
  },
326
326
  this.ctx,
327
+ row.field.field,
327
328
  ),
328
329
  onDone: () => {
329
330
  this.submenu = null;
@@ -91,9 +91,23 @@ export interface StringListField extends BaseField {
91
91
  kind: "stringList";
92
92
  }
93
93
 
94
+ /** One non-model choice shown before the scoped models in a model picker. */
95
+ export interface ModelPickerStaticOption {
96
+ /** Persisted value for the choice. */
97
+ value: string;
98
+ /** Human-readable picker label. */
99
+ label: string;
100
+ /** Optional explanation shown alongside the label. */
101
+ description?: string;
102
+ }
103
+
94
104
  /** Model picker backed by the scoped model set. */
95
105
  export interface ModelPickerField extends BaseField {
96
106
  kind: "modelPicker";
107
+ /** Additional host-owned choices shown before scoped models. */
108
+ staticOptions?: ModelPickerStaticOption[];
109
+ /** Whether to include the built-in `disabled` choice. Defaults to true. */
110
+ includeDisabled?: boolean;
97
111
  }
98
112
 
99
113
  /**
@@ -14,6 +14,7 @@ import {
14
14
  Text,
15
15
  } from "@earendil-works/pi-tui";
16
16
  import { getSelectableModels } from "../model-selection.ts";
17
+ import type { ModelPickerField } from "./settings-schema.ts";
17
18
 
18
19
  /**
19
20
  * Creates a pi-tui Input-backed submenu component with enter-to-confirm
@@ -57,25 +58,26 @@ export function createInputSubmenu(
57
58
  }
58
59
 
59
60
  /**
60
- * Creates a model picker submenu for settings with "disabled" as the first option.
61
+ * Creates a model picker submenu backed by the scoped model set.
62
+ *
63
+ * The built-in `disabled` choice remains enabled by default. Callers can add
64
+ * host-owned static choices or omit `disabled` through the field options.
61
65
  */
62
66
  export function createModelPickerSubmenu(
63
67
  currentValue: string,
64
68
  done: (selectedValue?: string) => void,
65
69
  ctx?: ExtensionContext,
70
+ options: Pick<ModelPickerField, "includeDisabled" | "staticOptions"> = {},
66
71
  ): {
67
72
  render: (width: number) => string[];
68
73
  invalidate: () => void;
69
74
  handleInput: (data: string) => boolean;
70
75
  } {
71
- const items = buildModelItems(ctx);
72
- const initialIndex =
73
- currentValue === "disabled"
74
- ? 0
75
- : Math.max(
76
- 0,
77
- items.findIndex((item) => item.value === currentValue),
78
- );
76
+ const items = buildModelItems(ctx, options);
77
+ const initialIndex = Math.max(
78
+ 0,
79
+ items.findIndex((item) => item.value === currentValue),
80
+ );
79
81
 
80
82
  const container = new Container();
81
83
  container.addChild(new Text(" Select model", 1, 0));
@@ -105,20 +107,35 @@ export function createModelPickerSubmenu(
105
107
  };
106
108
  }
107
109
 
108
- /** Build selectable model items with "disabled" as the first option. */
109
- function buildModelItems(ctx?: ExtensionContext): SelectItem[] {
110
- const items: SelectItem[] = [
111
- { value: "disabled", label: "disabled", description: "No model selected" },
112
- ];
110
+ /** Build static choices followed by the selectable scoped models. */
111
+ function buildModelItems(
112
+ ctx: ExtensionContext | undefined,
113
+ options: Pick<ModelPickerField, "includeDisabled" | "staticOptions">,
114
+ ): SelectItem[] {
115
+ const items: SelectItem[] = [];
116
+ const seen = new Set<string>();
117
+ for (const option of options.staticOptions ?? []) {
118
+ if (seen.has(option.value)) continue;
119
+ items.push({ ...option });
120
+ seen.add(option.value);
121
+ }
122
+
123
+ if (options.includeDisabled !== false && !seen.has("disabled")) {
124
+ items.push({ value: "disabled", label: "disabled", description: "No model selected" });
125
+ seen.add("disabled");
126
+ }
127
+
113
128
  if (!ctx) return items;
114
129
  const models = getSelectableModels(ctx);
115
130
  for (const model of models) {
131
+ if (seen.has(model.canonicalId)) continue;
116
132
  const suffix = model.isCurrent ? " [current]" : "";
117
133
  items.push({
118
134
  value: model.canonicalId,
119
135
  label: `${model.canonicalId}${suffix}`,
120
136
  description: model.label !== model.canonicalId ? model.label : undefined,
121
137
  });
138
+ seen.add(model.canonicalId);
122
139
  }
123
140
  return items;
124
141
  }
@@ -20,6 +20,7 @@ export type {
20
20
  DeclarativeSettingsOptions,
21
21
  EnumField,
22
22
  ModelPickerField,
23
+ ModelPickerStaticOption,
23
24
  NumberField,
24
25
  ScopedFieldValue,
25
26
  SettingsField,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-review",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "SuPi Review extension — session-aware review command and agent tools",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@mrclrchtr/supi-core": "2.7.0"
34
+ "@mrclrchtr/supi-core": "2.8.0"
35
35
  },
36
36
  "bundledDependencies": [
37
37
  "@mrclrchtr/supi-core"
package/src/config.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadSupiConfig } from "@mrclrchtr/supi-core/config";
3
+ import { registerDeclarativeSettings } from "@mrclrchtr/supi-core/settings";
4
+ import { CURRENT_SESSION_REVIEW_MODEL } from "./model.ts";
5
+
6
+ /** Persisted configuration for agent-driven Session-Aware Review. */
7
+ export interface ReviewConfig extends Record<string, unknown> {
8
+ /** Canonical `provider/model-id`, or `current` to use the active session model. */
9
+ agentModel: string;
10
+ }
11
+
12
+ /** Shared SuPi config section owned by supi-review. */
13
+ export const REVIEW_CONFIG_SECTION = "review";
14
+
15
+ /** Package defaults for supi-review configuration. */
16
+ export const REVIEW_DEFAULTS: ReviewConfig = {
17
+ agentModel: CURRENT_SESSION_REVIEW_MODEL,
18
+ };
19
+
20
+ /** Load merged, validated supi-review configuration for a workspace. */
21
+ export function loadReviewConfig(cwd: string, homeDir?: string): ReviewConfig {
22
+ const raw = loadSupiConfig(REVIEW_CONFIG_SECTION, cwd, REVIEW_DEFAULTS, { homeDir });
23
+ const agentModel =
24
+ typeof raw.agentModel === "string" && raw.agentModel.trim()
25
+ ? raw.agentModel.trim()
26
+ : REVIEW_DEFAULTS.agentModel;
27
+ return { agentModel };
28
+ }
29
+
30
+ /** Register the Review section contributed to `/supi-settings`. */
31
+ export function registerReviewSettings(pi: ExtensionAPI, homeDir?: string): void {
32
+ registerDeclarativeSettings(pi, {
33
+ id: REVIEW_CONFIG_SECTION,
34
+ label: "Review",
35
+ section: REVIEW_CONFIG_SECTION,
36
+ defaults: REVIEW_DEFAULTS,
37
+ fields: [
38
+ {
39
+ kind: "modelPicker",
40
+ key: "agentModel",
41
+ label: "Agent tool model",
42
+ description: "Model used for brief synthesis and reviewers started by supi_review_prepare.",
43
+ includeDisabled: false,
44
+ staticOptions: [
45
+ {
46
+ value: CURRENT_SESSION_REVIEW_MODEL,
47
+ label: "current session model",
48
+ description: "Use the model active when supi_review_prepare starts",
49
+ },
50
+ ],
51
+ },
52
+ ],
53
+ ...(homeDir ? { homeDir } : {}),
54
+ });
55
+ }
package/src/model.ts CHANGED
@@ -2,6 +2,9 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { getSelectableModels } from "@mrclrchtr/supi-core/model-selection";
3
3
  import type { ReviewModelSelection } from "./types.ts";
4
4
 
5
+ /** Sentinel that resolves to the model active when review preparation starts. */
6
+ export const CURRENT_SESSION_REVIEW_MODEL = "current";
7
+
5
8
  /** Build the canonical `provider/modelId` string used throughout the review flow. */
6
9
  export { toCanonicalModelId } from "@mrclrchtr/supi-core/model-selection";
7
10
 
@@ -35,3 +38,33 @@ export function getCurrentReviewModel(
35
38
  isCurrent: true,
36
39
  };
37
40
  }
41
+
42
+ /**
43
+ * Resolve the configured model for an agent-driven review.
44
+ *
45
+ * `current` preserves the historical behavior. Explicit canonical model ids
46
+ * must be both available and present in Pi's current scoped model set.
47
+ */
48
+ export function resolveAgentReviewModel(
49
+ ctx: Pick<ExtensionContext, "cwd" | "modelRegistry" | "model">,
50
+ configuredModelId: string,
51
+ enabledModelPatterns?: string[],
52
+ ): ReviewModelSelection | undefined {
53
+ const modelId = configuredModelId.trim();
54
+ if (modelId === CURRENT_SESSION_REVIEW_MODEL) {
55
+ return getCurrentReviewModel(ctx);
56
+ }
57
+
58
+ const selection = getSelectableReviewModels(
59
+ { cwd: ctx.cwd, modelRegistry: ctx.modelRegistry, model: undefined },
60
+ enabledModelPatterns,
61
+ ).find((candidate) => candidate.canonicalId === modelId);
62
+ if (!selection) return undefined;
63
+
64
+ return {
65
+ ...selection,
66
+ isCurrent: ctx.model
67
+ ? `${ctx.model.provider}/${ctx.model.id}` === selection.canonicalId
68
+ : false,
69
+ };
70
+ }
package/src/review.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { buildSessionContext, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { WidgetProgress } from "@mrclrchtr/supi-core/progress-widget";
3
3
  import { runWithProgressWidget } from "@mrclrchtr/supi-core/tool-framework";
4
+ import { registerReviewSettings } from "./config.ts";
4
5
  import { resolveBranchSnapshot, resolveCommitSnapshot, resolveWorkingTreeSnapshot } from "./git.ts";
5
6
  import { serializeSessionContext } from "./history/collect.ts";
6
7
  import { synthesizeReviewBrief } from "./history/synthesize.ts";
@@ -29,6 +30,7 @@ type CommandContext = Parameters<Parameters<ExtensionAPI["registerCommand"]>[1][
29
30
 
30
31
  export default function reviewExtension(pi: ExtensionAPI) {
31
32
  registerReviewRenderer(pi);
33
+ registerReviewSettings(pi);
32
34
  registerAgentReviewTools(pi);
33
35
 
34
36
  pi.registerCommand("supi-review", {
@@ -10,9 +10,10 @@ import {
10
10
  truncateHead,
11
11
  } from "@earendil-works/pi-coding-agent";
12
12
  import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
13
+ import { loadReviewConfig } from "../config.ts";
13
14
  import { isCommitObjectId, summarizeReviewSnapshot } from "../git.ts";
14
15
  import { serializeSessionContext } from "../history/collect.ts";
15
- import { getCurrentReviewModel } from "../model.ts";
16
+ import { CURRENT_SESSION_REVIEW_MODEL, resolveAgentReviewModel } from "../model.ts";
16
17
  import { ReviewPlanStore } from "../session/review-plan-store.ts";
17
18
  import type {
18
19
  BriefEvaluation,
@@ -64,8 +65,16 @@ export function registerAgentReviewTools(
64
65
  // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
65
66
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
66
67
  const input = params as PrepareAgentReviewInput;
67
- const model = getCurrentReviewModel(ctx);
68
- if (!model) throw new Error("No current session model is available for review preparation.");
68
+ const configuredModelId = loadReviewConfig(ctx.cwd).agentModel;
69
+ const model = resolveAgentReviewModel(ctx, configuredModelId);
70
+ if (!model) {
71
+ if (configuredModelId === CURRENT_SESSION_REVIEW_MODEL) {
72
+ throw new Error("No current session model is available for review preparation.");
73
+ }
74
+ throw new Error(
75
+ `Configured agent review model "${configuredModelId}" is not available in Pi's scoped model set. Choose another model in /supi-settings.`,
76
+ );
77
+ }
69
78
 
70
79
  const target = parseTarget(input);
71
80
  const sessionContext = buildSessionContext(