@mrclrchtr/supi-review 1.3.0 → 1.4.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.
Files changed (26) hide show
  1. package/README.md +91 -51
  2. package/node_modules/@mrclrchtr/supi-core/README.md +52 -41
  3. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
  5. package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
  6. package/node_modules/@mrclrchtr/supi-core/src/{context-provider-registry.ts → context/context-provider-registry.ts} +1 -1
  7. package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
  8. package/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
  9. package/node_modules/@mrclrchtr/supi-core/src/{settings-registry.ts → settings/settings-registry.ts} +1 -1
  10. package/package.json +4 -3
  11. package/src/briefs.ts +101 -0
  12. package/src/profiles.ts +52 -0
  13. package/src/review.ts +100 -34
  14. package/src/{runner-types.ts → tool/runner-types.ts} +3 -1
  15. package/src/{runner.ts → tool/runner.ts} +14 -6
  16. package/src/{target-resolution.ts → tool/target-resolution.ts} +2 -2
  17. package/src/types.ts +46 -3
  18. package/src/{format-content.ts → ui/format-content.ts} +17 -6
  19. package/src/{progress-widget.ts → ui/progress-widget.ts} +10 -6
  20. package/src/{renderer.ts → ui/renderer.ts} +13 -1
  21. package/src/{ui.ts → ui/ui.ts} +93 -1
  22. /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
  23. /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
  24. /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
  25. /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
  26. /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mrclrchtr/supi-review
2
2
 
3
- Structured code review for the [pi coding agent](https://github.com/earendil-works/pi).
3
+ Adds a guided `/supi-review` command to the [pi coding agent](https://github.com/earendil-works/pi) for structured code review.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,71 +8,111 @@ Structured code review for the [pi coding agent](https://github.com/earendil-wor
8
8
  pi install npm:@mrclrchtr/supi-review
9
9
  ```
10
10
 
11
- > **🧪 Beta package** not included in the `@mrclrchtr/supi` meta-package.
12
- > Install directly when you need structured code reviews.
11
+ This is a **beta** package. It is not bundled in `@mrclrchtr/supi`.
13
12
 
14
- ## What it adds
13
+ For local development:
15
14
 
16
- This extension registers `/supi-review`, which launches an in-process managed child session to review:
15
+ ```bash
16
+ pi install ./packages/supi-review
17
+ ```
17
18
 
18
- - uncommitted changes
19
- - a base-branch diff
20
- - a specific commit
21
- - custom review instructions
19
+ After editing the source, run `/reload`.
20
+
21
+ ## What you get
22
+
23
+ After install, pi gets one command:
24
+
25
+ - `/supi-review` — launch an interactive review flow and render a structured review result
22
26
 
23
- Review results are emitted as a `supi-review` custom message. Optional auto-fix mode can send a follow-up user message to fix the findings.
27
+ The reviewer runs in a managed child agent session with read-only review tools:
28
+
29
+ - `read`
30
+ - `grep`
31
+ - `find`
32
+ - `ls`
33
+ - `submit_review` (internal result-submission tool)
24
34
 
25
35
  ## Review flow
26
36
 
27
- ```text
28
- /supi-review
29
-
30
- select preset + auto-fix mode
31
-
32
- collect diff / commit / custom target
33
-
34
- create child reviewer session
35
-
36
- submit_review tool returns structured findings
37
-
38
- render supi-review message
39
- ```
37
+ `/supi-review` walks you through:
40
38
 
41
- ## Configuration
39
+ 1. choose a review mode
40
+ 2. choose a review target
41
+ 3. build a review brief
42
+ 4. edit and approve the final review prompt
43
+ 5. run the review with a live progress widget
44
+ 6. show the result as a structured custom message
45
+ 7. optionally trigger an auto-fix follow-up turn
42
46
 
43
- If your install surface includes `/supi-settings` (for example when also installing the `@mrclrchtr/supi` meta-package), this package contributes review settings there.
47
+ ## Review modes
44
48
 
45
- ## Architecture
49
+ ### Dynamic review
46
50
 
47
- ```text
48
- src/
49
- ├── review.ts command registration and orchestration
50
- ├── runner.ts managed child-session execution
51
- ├── target-resolution.ts git-backed target hydration
52
- ├── prompts.ts review prompt generation
53
- ├── git.ts git helpers for diffs, commits, and branches
54
- ├── progress-widget.ts live TUI progress overlay
55
- ├── renderer.ts custom message renderer
56
- ├── settings.ts review model + behavior settings
57
- └── types.ts shared result and target types
58
- ```
51
+ You provide:
59
52
 
60
- ## Requirements
53
+ - what changed
54
+ - the intended outcome
55
+ - what the reviewer should focus on
61
56
 
62
- - `@earendil-works/pi-ai`
63
- - `@earendil-works/pi-coding-agent`
64
- - `@earendil-works/pi-tui`
65
- - `typebox`
66
- - `@mrclrchtr/supi-core`
57
+ The package turns that into a review brief and lets you edit the final prompt before the review starts.
67
58
 
68
- ## Development
59
+ ### Standard review
69
60
 
70
- ```bash
71
- pnpm vitest run packages/supi-review/
72
- pnpm exec tsc --noEmit -p packages/supi-review/tsconfig.json
73
- pnpm exec biome check packages/supi-review/
61
+ You choose one of the built-in profiles:
62
+
63
+ - `general`
64
+ - `security`
65
+ - `api-maintainability`
66
+
67
+ The package builds the review brief from the selected profile and again lets you edit the final prompt before running.
68
+
69
+ ## Review targets
70
+
71
+ Current target presets:
72
+
73
+ - base branch diff
74
+ - uncommitted changes
75
+ - one commit
76
+ - custom review instructions
77
+
78
+ ## Result shape
79
+
80
+ A successful review includes:
81
+
82
+ - overall correctness verdict
83
+ - overall explanation
84
+ - overall confidence score
85
+ - structured findings with title, body, priority, confidence score, and code location
86
+
87
+ The renderer also handles failed, canceled, and timed-out reviews.
88
+
89
+ ## Settings
90
+
91
+ This package registers a **Review** section in `/supi-settings`.
92
+
93
+ Available settings:
94
+
95
+ - `reviewModel` — preselect the model used by `/supi-review`; empty means inherit the active session model
96
+ - `maxDiffBytes` — maximum diff size before the prompt builder truncates the diff
97
+ - `autoFix` — automatically send a follow-up user message to fix findings after a successful review with findings
98
+
99
+ Defaults:
100
+
101
+ ```json
102
+ {
103
+ "review": {
104
+ "reviewModel": "",
105
+ "maxDiffBytes": 100000,
106
+ "autoFix": false
107
+ }
108
+ }
74
109
  ```
75
110
 
76
- ## License
111
+ ## Source
77
112
 
78
- MIT
113
+ - `src/review.ts` — command orchestration and interactive flow
114
+ - `src/ui.ts` — TUI selection and approval steps
115
+ - `src/profiles.ts` — built-in review profiles
116
+ - `src/runner.ts` — managed reviewer session
117
+ - `src/settings.ts` — `/supi-settings` integration
118
+ - `src/renderer.ts` — structured result rendering
@@ -1,65 +1,78 @@
1
1
  # @mrclrchtr/supi-core
2
2
 
3
- Shared infrastructure for SuPi packages.
3
+ Shared infrastructure for SuPi extensions.
4
+
5
+ This package is mainly for extension authors. It gives you a common config system, settings plumbing, context helpers, registries, and a small extension surface that registers `/supi-settings`.
4
6
 
5
7
  ## Install
6
8
 
7
- Use it as a dependency in another extension package:
9
+ ### As a dependency for another extension
8
10
 
9
11
  ```bash
10
12
  pnpm add @mrclrchtr/supi-core
11
13
  ```
12
14
 
13
- ## Package role
14
-
15
- `@mrclrchtr/supi-core` now has two explicit surfaces:
15
+ ### As a pi package
16
16
 
17
- - `@mrclrchtr/supi-core/api` — shared library helpers for other SuPi packages
18
- - `@mrclrchtr/supi-core/extension` — a minimal pi extension that registers `/supi-settings`
19
-
20
- `pi.extensions` still points at the real file path `./src/extension.ts` inside the package. The `/api` and `/extension` paths are consumer-facing package exports, not manifest aliases.
17
+ ```bash
18
+ pi install npm:@mrclrchtr/supi-core
19
+ ```
21
20
 
22
- ## What it provides
21
+ Installing it as a pi package adds the minimal `/supi-settings` extension surface.
23
22
 
24
- Current exports cover:
23
+ ## Package surfaces
25
24
 
26
- - shared config loading, scoped reads, writes, and key removal
27
- - config-backed settings registration helpers for `/supi-settings`
28
- - the shared settings registry, overlay UI, and `registerSettingsCommand()` helper
29
- - XML `<extension-context>` wrapping plus context-message utilities
30
- - context-provider and debug-event registries reused across SuPi packages
31
- - project root and path helpers reused by packages such as `supi-lsp`
25
+ - `@mrclrchtr/supi-core/api` reusable helpers for other packages and extensions
26
+ - `@mrclrchtr/supi-core/extension` minimal pi extension that registers `/supi-settings`
32
27
 
33
- ## Config system
28
+ ## What you get from the API
34
29
 
35
- Config resolution order:
30
+ ### Config helpers
36
31
 
37
- ```text
38
- defaults <- global <- project
39
- ```
32
+ - `loadSupiConfig()` — merged config with resolution order `defaults <- global <- project`
33
+ - `loadSupiConfigForScope()` load one scope at a time for settings UIs
34
+ - `writeSupiConfig()` — persist values
35
+ - `removeSupiConfigKey()` — remove a key or override
40
36
 
41
37
  Config file locations:
42
38
 
43
39
  - global: `~/.pi/agent/supi/config.json`
44
40
  - project: `.pi/supi/config.json`
45
41
 
46
- Main helpers:
42
+ ### Settings helpers
43
+
44
+ - `registerSettings()` — register an arbitrary settings section
45
+ - `registerConfigSettings()` — register a config-backed settings section with scoped persistence helpers
46
+ - `registerSettingsCommand()` — register `/supi-settings`
47
+ - `openSettingsOverlay()` — open the shared settings UI directly
48
+ - `createInputSubmenu()` — helper for simple text-entry submenus
49
+
50
+ The built-in settings UI supports:
47
51
 
48
- - `loadSupiConfig()` — effective merged config (`defaults <- global <- project`)
49
- - `loadSupiConfigForScope()` raw single-scope config for settings UIs (`defaults <- selected scope`)
50
- - `writeSupiConfig()`
51
- - `removeSupiConfigKey()`
52
- - `registerConfigSettings()`
52
+ - project/global scope toggle
53
+ - grouped extension sections
54
+ - searchable setting lists
53
55
 
54
- ## Context and settings helpers
56
+ ### Context helpers
55
57
 
56
- - `wrapExtensionContext()`
58
+ - `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
57
59
  - `findLastUserMessageIndex()`
58
60
  - `getContextToken()`
61
+ - `getPromptContent()`
59
62
  - `pruneAndReorderContextMessages()`
60
- - `registerSettings()`
61
- - `registerSettingsCommand()`
62
- - `openSettingsOverlay()`
63
+ - `restorePromptContent()`
64
+
65
+ ### Shared registries
66
+
67
+ - context-provider registry for `/supi-context`
68
+ - debug-event registry for producers that want shared debug capture
69
+ - settings registry used by `/supi-settings`
70
+
71
+ ### Project and session helpers
72
+
73
+ - project-root detection and directory walking helpers such as `findProjectRoot()` and `walkProject()`
74
+ - active-branch session helper: `getActiveBranchEntries()`
75
+ - terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
63
76
 
64
77
  ## Example
65
78
 
@@ -80,17 +93,15 @@ registerConfigSettings({
80
93
  });
81
94
 
82
95
  const message = wrapExtensionContext("my-extension", "hello", {
83
- turn: 1,
84
96
  file: "CLAUDE.md",
97
+ turn: 1,
85
98
  });
86
99
  ```
87
100
 
88
- ## Requirements
89
-
90
- - `@earendil-works/pi-coding-agent`
91
- - `@earendil-works/pi-tui`
92
-
93
101
  ## Source
94
102
 
95
- - Library surface: `src/api.ts`
96
- - Extension surface: `src/extension.ts`
103
+ - `src/api.ts` — exported library surface
104
+ - `src/extension.ts` — minimal `/supi-settings` entrypoint
105
+ - `src/config.ts` — shared config loading and writing
106
+ - `src/config-settings.ts` — config-backed settings registration helper
107
+ - `src/settings-ui.ts` — shared settings overlay
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,30 +2,30 @@
2
2
  // Provides XML context tag wrapping, unified config system, context-message utilities,
3
3
  // and settings registry for supi-wide TUI settings.
4
4
 
5
- export type { SupiConfigLocation, SupiConfigOptions } from "./config.ts";
5
+ export type { SupiConfigLocation, SupiConfigOptions } from "./config/config.ts";
6
6
  export {
7
7
  loadSupiConfig,
8
8
  loadSupiConfigForScope,
9
9
  removeSupiConfigKey,
10
10
  writeSupiConfig,
11
- } from "./config.ts";
12
- export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config-settings.ts";
13
- export { registerConfigSettings } from "./config-settings.ts";
14
- export type { ContextMessageLike } from "./context-messages.ts";
11
+ } from "./config/config.ts";
12
+ export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config/config-settings.ts";
13
+ export { registerConfigSettings } from "./config/config-settings.ts";
14
+ export type { ContextMessageLike } from "./context/context-messages.ts";
15
15
  export {
16
16
  findLastUserMessageIndex,
17
17
  getContextToken,
18
18
  getPromptContent,
19
19
  pruneAndReorderContextMessages,
20
20
  restorePromptContent,
21
- } from "./context-messages.ts";
22
- export type { ContextProvider } from "./context-provider-registry.ts";
21
+ } from "./context/context-messages.ts";
22
+ export type { ContextProvider } from "./context/context-provider-registry.ts";
23
23
  export {
24
24
  clearRegisteredContextProviders,
25
25
  getRegisteredContextProviders,
26
26
  registerContextProvider,
27
- } from "./context-provider-registry.ts";
28
- export { wrapExtensionContext } from "./context-tag.ts";
27
+ } from "./context/context-provider-registry.ts";
28
+ export { wrapExtensionContext } from "./context/context-tag.ts";
29
29
  export type {
30
30
  DebugAgentAccess,
31
31
  DebugEvent,
@@ -64,14 +64,14 @@ export {
64
64
  walkProject,
65
65
  } from "./project-roots.ts";
66
66
  export { getActiveBranchEntries } from "./session-utils.ts";
67
- export { registerSettingsCommand } from "./settings-command.ts";
68
- export type { SettingsScope, SettingsSection } from "./settings-registry.ts";
67
+ export { registerSettingsCommand } from "./settings/settings-command.ts";
68
+ export type { SettingsScope, SettingsSection } from "./settings/settings-registry.ts";
69
69
  export {
70
70
  clearRegisteredSettings,
71
71
  getRegisteredSettings,
72
72
  registerSettings,
73
- } from "./settings-registry.ts";
74
- export { createInputSubmenu, openSettingsOverlay } from "./settings-ui.ts";
73
+ } from "./settings/settings-registry.ts";
74
+ export { createInputSubmenu, openSettingsOverlay } from "./settings/settings-ui.ts";
75
75
  export type { TitleTarget } from "./terminal.ts";
76
76
  export {
77
77
  DONE_SYMBOL,
@@ -2,9 +2,9 @@
2
2
  // Wraps registerSettings() and centralizes selected-scope loading + scoped persistence.
3
3
 
4
4
  import type { SettingItem } from "@earendil-works/pi-tui";
5
+ import type { SettingsScope } from "../settings/settings-registry.ts";
6
+ import { registerSettings } from "../settings/settings-registry.ts";
5
7
  import { loadSupiConfigForScope, removeSupiConfigKey, writeSupiConfig } from "./config.ts";
6
- import type { SettingsScope } from "./settings-registry.ts";
7
- import { registerSettings } from "./settings-registry.ts";
8
8
 
9
9
  export interface ConfigSettingsHelpers {
10
10
  /** Write a key to the selected scope's config section. */
@@ -3,7 +3,7 @@
3
3
  // Extensions declare context data providers via `registerContextProvider()` during their
4
4
  // factory function. The `/supi-context` command reads them via `getRegisteredContextProviders()`.
5
5
 
6
- import { createRegistry } from "./registry-utils.ts";
6
+ import { createRegistry } from "../registry-utils.ts";
7
7
 
8
8
  export interface ContextProvider {
9
9
  /** Unique identifier — e.g. "rtk" */
@@ -1 +1 @@
1
- export { registerSettingsCommand as default } from "./settings-command.ts";
1
+ export { registerSettingsCommand as default } from "./settings/settings-command.ts";
@@ -2,30 +2,30 @@
2
2
  // Provides XML context tag wrapping, unified config system, context-message utilities,
3
3
  // and settings registry for supi-wide TUI settings.
4
4
 
5
- export type { SupiConfigLocation, SupiConfigOptions } from "./config.ts";
5
+ export type { SupiConfigLocation, SupiConfigOptions } from "./config/config.ts";
6
6
  export {
7
7
  loadSupiConfig,
8
8
  loadSupiConfigForScope,
9
9
  removeSupiConfigKey,
10
10
  writeSupiConfig,
11
- } from "./config.ts";
12
- export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config-settings.ts";
13
- export { registerConfigSettings } from "./config-settings.ts";
14
- export type { ContextMessageLike } from "./context-messages.ts";
11
+ } from "./config/config.ts";
12
+ export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config/config-settings.ts";
13
+ export { registerConfigSettings } from "./config/config-settings.ts";
14
+ export type { ContextMessageLike } from "./context/context-messages.ts";
15
15
  export {
16
16
  findLastUserMessageIndex,
17
17
  getContextToken,
18
18
  getPromptContent,
19
19
  pruneAndReorderContextMessages,
20
20
  restorePromptContent,
21
- } from "./context-messages.ts";
22
- export type { ContextProvider } from "./context-provider-registry.ts";
21
+ } from "./context/context-messages.ts";
22
+ export type { ContextProvider } from "./context/context-provider-registry.ts";
23
23
  export {
24
24
  clearRegisteredContextProviders,
25
25
  getRegisteredContextProviders,
26
26
  registerContextProvider,
27
- } from "./context-provider-registry.ts";
28
- export { wrapExtensionContext } from "./context-tag.ts";
27
+ } from "./context/context-provider-registry.ts";
28
+ export { wrapExtensionContext } from "./context/context-tag.ts";
29
29
  export type {
30
30
  DebugAgentAccess,
31
31
  DebugEvent,
@@ -64,14 +64,14 @@ export {
64
64
  walkProject,
65
65
  } from "./project-roots.ts";
66
66
  export { getActiveBranchEntries } from "./session-utils.ts";
67
- export { registerSettingsCommand } from "./settings-command.ts";
68
- export type { SettingsScope, SettingsSection } from "./settings-registry.ts";
67
+ export { registerSettingsCommand } from "./settings/settings-command.ts";
68
+ export type { SettingsScope, SettingsSection } from "./settings/settings-registry.ts";
69
69
  export {
70
70
  clearRegisteredSettings,
71
71
  getRegisteredSettings,
72
72
  registerSettings,
73
- } from "./settings-registry.ts";
74
- export { createInputSubmenu, openSettingsOverlay } from "./settings-ui.ts";
73
+ } from "./settings/settings-registry.ts";
74
+ export { createInputSubmenu, openSettingsOverlay } from "./settings/settings-ui.ts";
75
75
  export type { TitleTarget } from "./terminal.ts";
76
76
  export {
77
77
  DONE_SYMBOL,
@@ -4,7 +4,7 @@
4
4
  // factory function. The generic settings UI reads them via `getRegisteredSettings()`.
5
5
 
6
6
  import type { SettingItem } from "@earendil-works/pi-tui";
7
- import { createRegistry } from "./registry-utils.ts";
7
+ import { createRegistry } from "../registry-utils.ts";
8
8
 
9
9
  export type SettingsScope = "project" | "global";
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-review",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "SuPi Review extension — structured code review via /supi-review command",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,7 +20,7 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@mrclrchtr/supi-core": "1.3.0"
23
+ "@mrclrchtr/supi-core": "1.4.0"
24
24
  },
25
25
  "bundledDependencies": [
26
26
  "@mrclrchtr/supi-core"
@@ -47,7 +47,8 @@
47
47
  },
48
48
  "pi": {
49
49
  "extensions": [
50
- "./src/extension.ts"
50
+ "./src/extension.ts",
51
+ "node_modules/@mrclrchtr/supi-core/src/extension.ts"
51
52
  ]
52
53
  },
53
54
  "main": "src/api.ts",
package/src/briefs.ts ADDED
@@ -0,0 +1,101 @@
1
+ import { getProfile } from "./profiles.ts";
2
+ import type { BuildPromptOptions } from "./prompts.ts";
3
+ import { buildReviewPrompt } from "./prompts.ts";
4
+ import type { ReviewBrief, ReviewTarget } from "./types.ts";
5
+
6
+ // ── Dynamic brief construction ───────────────────────────────────
7
+
8
+ export interface DynamicBriefInputs {
9
+ summary: string;
10
+ intent: string;
11
+ focus: string;
12
+ }
13
+
14
+ /**
15
+ * Build a dynamic review brief from structured user inputs.
16
+ * The finalPrompt field is empty — it must be set later by the caller
17
+ * via `assembleReviewerPrompt()` or explicitly assigned.
18
+ */
19
+ export function buildDynamicBrief(inputs: DynamicBriefInputs): ReviewBrief {
20
+ const title = `Review: ${inputs.summary}`;
21
+ return {
22
+ mode: "dynamic",
23
+ title,
24
+ summary: inputs.summary,
25
+ intent: inputs.intent,
26
+ focus: inputs.focus,
27
+ finalPrompt: "",
28
+ };
29
+ }
30
+
31
+ // ── Standard brief construction ──────────────────────────────────
32
+
33
+ /**
34
+ * Build a standard review brief from a named profile.
35
+ * The summary/intent/focus are derived from the profile definition.
36
+ * The finalPrompt field is empty — it must be set later by the caller
37
+ * via `assembleReviewerPrompt()` or explicitly assigned.
38
+ */
39
+ export function buildStandardBrief(profileId: string): ReviewBrief {
40
+ const profile = getProfile(profileId);
41
+ if (!profile) {
42
+ throw new Error(`Unknown profile: "${profileId}"`);
43
+ }
44
+ return {
45
+ mode: "standard",
46
+ title: `${profile.label} Review`,
47
+ summary: profile.description,
48
+ intent: profile.description,
49
+ focus: profile.label,
50
+ profileId: profile.id,
51
+ finalPrompt: "",
52
+ };
53
+ }
54
+
55
+ // ── Prompt assembly ──────────────────────────────────────────────
56
+
57
+ /**
58
+ * Assemble the full reviewer prompt from a review brief and target.
59
+ *
60
+ * The output has two sections:
61
+ * 1. **Review Request** — the brief's summary, intent, focus, and profile (if standard)
62
+ * 2. **Changes to review** — the existing target preamble + diff/custom instructions
63
+ */
64
+ export function assembleReviewerPrompt(
65
+ brief: ReviewBrief,
66
+ target: ReviewTarget,
67
+ diff: string = "",
68
+ options?: BuildPromptOptions,
69
+ ): string {
70
+ const parts: string[] = [];
71
+
72
+ // Review request summary from the brief
73
+ parts.push("## Review Request");
74
+ parts.push("");
75
+ parts.push(`**Summary:** ${brief.summary}`);
76
+ parts.push(`**Intended outcome:** ${brief.intent}`);
77
+ parts.push(`**Focus areas:** ${brief.focus}`);
78
+ if (brief.mode === "standard" && brief.profileId) {
79
+ parts.push(`**Review profile:** ${brief.profileId}`);
80
+ }
81
+ parts.push("");
82
+
83
+ // Existing target preamble + diff
84
+ parts.push(buildReviewPrompt(target, diff, options));
85
+
86
+ return parts.join("\n");
87
+ }
88
+
89
+ /**
90
+ * Build a fully resolved brief by assembling the prompt and setting
91
+ * the finalPrompt field. Used after the brief has been approved.
92
+ */
93
+ export function resolveBrief(
94
+ brief: ReviewBrief,
95
+ target: ReviewTarget,
96
+ diff: string = "",
97
+ options?: BuildPromptOptions,
98
+ ): ReviewBrief {
99
+ const finalPrompt = assembleReviewerPrompt(brief, target, diff, options);
100
+ return { ...brief, finalPrompt };
101
+ }
@@ -0,0 +1,52 @@
1
+ import type { ReviewProfile } from "./types.ts";
2
+
3
+ const STARTER_PROFILES: ReviewProfile[] = [
4
+ {
5
+ id: "general",
6
+ label: "General",
7
+ description: "Standard review covering correctness, security, performance, and maintainability",
8
+ systemPrompt: "",
9
+ },
10
+ {
11
+ id: "security",
12
+ label: "Security",
13
+ description:
14
+ "Focused security review — injection risks, auth bypasses, secrets exposure, data validation",
15
+ systemPrompt: [
16
+ "--- Security review guidance ---",
17
+ "Prioritize the following areas:",
18
+ "- Injection risks: SQL, command, template injection in user-facing inputs",
19
+ "- Authentication & authorization: missing checks, privilege escalation, session handling",
20
+ "- Secrets exposure: hardcoded keys/tokens, logging sensitive data, insecure storage",
21
+ "- Data validation: insufficient input sanitisation, unsafe deserialization",
22
+ "- Cryptographic misuse: weak algorithms, hardcoded IVs/seeds, signature validation gaps",
23
+ "",
24
+ "Flag anything that could lead to data loss, unauthorized access, or privilege escalation as critical priority.",
25
+ ].join("\n"),
26
+ },
27
+ {
28
+ id: "api-maintainability",
29
+ label: "API & Maintainability",
30
+ description:
31
+ "Focused on API design, breaking changes, consistency, and long-term code maintainability",
32
+ systemPrompt: [
33
+ "--- API & maintainability review guidance ---",
34
+ "Prioritize the following areas:",
35
+ "- API design: inconsistent signatures, breaking contract changes, poor ergonomics",
36
+ "- Breaking changes: modified exported types, removed public APIs, changed parameter shapes",
37
+ "- Code clarity: unclear naming, missing abstractions, overly nested control flow",
38
+ "- Duplication: repeated patterns that should be extracted, copy-pasted code blocks",
39
+ "- Documentation gaps: missing JSDoc, stale comments, undocumented exports",
40
+ ].join("\n"),
41
+ },
42
+ ];
43
+
44
+ /** Returns all available starter review profiles. */
45
+ export function getProfiles(): ReviewProfile[] {
46
+ return STARTER_PROFILES;
47
+ }
48
+
49
+ /** Returns the profile with the given id, or undefined if not found. */
50
+ export function getProfile(id: string): ReviewProfile | undefined {
51
+ return STARTER_PROFILES.find((p) => p.id === id);
52
+ }
package/src/review.ts CHANGED
@@ -1,6 +1,7 @@
1
+ // biome-ignore lint/nursery/noExcessiveLinesPerFile: pre-existing, needs refactoring
1
2
  import type { Model } from "@earendil-works/pi-ai";
2
3
  import type { ExtensionAPI, ModelRegistry } from "@earendil-works/pi-coding-agent";
3
- import { formatReviewContent } from "./format-content.ts";
4
+ import { buildDynamicBrief, buildStandardBrief } from "./briefs.ts";
4
5
  import {
5
6
  getCommitFileNames,
6
7
  getCommitShow,
@@ -10,11 +11,7 @@ import {
10
11
  getUncommittedDiff,
11
12
  getUncommittedFileNames,
12
13
  } from "./git.ts";
13
- import { ReviewProgressWidget } from "./progress-widget.ts";
14
14
  import { buildReviewPrompt } from "./prompts.ts";
15
- import { registerReviewRenderer } from "./renderer.ts";
16
- import { runReviewer } from "./runner.ts";
17
- import type { ReviewerInvocation } from "./runner-types.ts";
18
15
  import {
19
16
  filterByEnabledModels,
20
17
  loadReviewSettings,
@@ -22,14 +19,29 @@ import {
22
19
  registerReviewSettings,
23
20
  setReviewModelChoices,
24
21
  } from "./settings.ts";
25
- import { resolveGitTarget } from "./target-resolution.ts";
26
- import type { ReviewResult, ReviewTarget } from "./types.ts";
27
- import { selectAutoFix, selectBranch, selectCommit, selectPreset } from "./ui.ts";
22
+ import { runReviewer } from "./tool/runner.ts";
23
+ import type { ReviewerInvocation } from "./tool/runner-types.ts";
24
+ import { resolveGitTarget } from "./tool/target-resolution.ts";
25
+ import type { ReviewBrief, ReviewResult, ReviewTarget } from "./types.ts";
26
+ import { formatReviewContent } from "./ui/format-content.ts";
27
+ import { ReviewProgressWidget } from "./ui/progress-widget.ts";
28
+ import { registerReviewRenderer } from "./ui/renderer.ts";
29
+ import {
30
+ approveBriefViaEditor,
31
+ collectDynamicInputs,
32
+ selectAutoFix,
33
+ selectBranch,
34
+ selectCommit,
35
+ selectPreset,
36
+ selectProfile,
37
+ selectReviewMode,
38
+ } from "./ui/ui.ts";
28
39
 
29
40
  type CommandContext = Parameters<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>[1];
30
41
 
31
42
  interface ReviewExecutionOptions {
32
43
  target: ReviewTarget;
44
+ brief: ReviewBrief;
33
45
  maxDiffBytes: number;
34
46
  ctx: CommandContext;
35
47
  signal?: AbortSignal;
@@ -52,7 +64,7 @@ export default function reviewExtension(pi: ExtensionAPI) {
52
64
  return;
53
65
  }
54
66
 
55
- // Try to respect PIs scoped models (enabledModels).
67
+ // Try to respect PI's scoped models (enabledModels).
56
68
  // Workaround for pi-mono#3535 — swap to ctx.scopedModels when exposed by PI.
57
69
  const enabledPatterns = readPiEnabledModels();
58
70
  const models = enabledPatterns ? filterByEnabledModels(enabledPatterns, allModels) : allModels;
@@ -83,21 +95,88 @@ async function handleInteractive(
83
95
  ctx: CommandContext,
84
96
  pi: ExtensionAPI,
85
97
  ): Promise<void> {
98
+ // Step 1: Select review mode
99
+ const mode = await selectReviewMode(ctx);
100
+ if (!mode) return;
101
+
102
+ // Step 2: Select review target
86
103
  const preset = await selectPreset(ctx);
87
104
  if (!preset) return;
88
105
 
89
- const autoFix = await selectAutoFix(ctx, autoFixDefault);
90
- if (autoFix === undefined) return;
91
-
92
106
  const target = await resolvePresetTarget(preset, ctx);
93
107
  if (!target) return;
94
108
 
95
- const result = await runReviewWithLoader(target, maxDiffBytes, ctx, pi);
109
+ // Step 3: Build the review brief
110
+ let brief: ReviewBrief;
111
+ if (mode === "standard") {
112
+ const profileId = await selectProfile(ctx);
113
+ if (!profileId) return;
114
+ brief = buildStandardBrief(profileId);
115
+ } else {
116
+ const inputs = await collectDynamicInputs(ctx);
117
+ if (!inputs) return;
118
+ brief = buildDynamicBrief(inputs);
119
+ }
120
+
121
+ // Step 4: Assemble the full prompt and get user approval
122
+ const diffOrBody = getDiffText(target);
123
+ const truncated = maybeTruncateDiff(diffOrBody, maxDiffBytes);
124
+ const draftPrompt = buildReviewPrompt(
125
+ target,
126
+ truncated.text,
127
+ truncated.wasTruncated
128
+ ? { truncated: true, truncatedBytes: truncated.truncatedBytes }
129
+ : undefined,
130
+ );
131
+
132
+ // Show the brief context + draft prompt for approval
133
+ const approvalText = await approveBriefViaEditor(ctx, formatBriefWithPrompt(brief, draftPrompt));
134
+ if (!approvalText) return;
135
+
136
+ brief.finalPrompt = approvalText;
137
+
138
+ // Step 5: Auto-fix preference
139
+ const autoFix = await selectAutoFix(ctx, autoFixDefault);
140
+ if (autoFix === undefined) return;
141
+
142
+ // Step 6: Run the review
143
+ const result = await runReviewWithLoader(brief, target, maxDiffBytes, ctx, pi);
96
144
  injectReviewMessage(pi, result, autoFix);
97
145
  }
98
146
 
147
+ /** Extract the diff/show text from a target for display. */
148
+ function getDiffText(target: ReviewTarget): string {
149
+ if (target.type === "base-branch" || target.type === "uncommitted") {
150
+ return target.diff;
151
+ }
152
+ if (target.type === "commit") {
153
+ return target.show;
154
+ }
155
+ return "";
156
+ }
157
+
158
+ /** Format the brief summary + full prompt for editor approval. */
159
+ function formatBriefWithPrompt(brief: ReviewBrief, prompt: string): string {
160
+ return [
161
+ "# Review Brief",
162
+ "",
163
+ brief.mode === "standard"
164
+ ? `Profile: ${brief.profileId ?? "standard"}`
165
+ : "Review mode: dynamic",
166
+ `Summary: ${brief.summary}`,
167
+ `Intended outcome: ${brief.intent}`,
168
+ `Focus areas: ${brief.focus}`,
169
+ "",
170
+ "# Suggested review prompt",
171
+ "",
172
+ "Edit the prompt below if needed, then save and close to start the review.",
173
+ "",
174
+ prompt,
175
+ ].join("\n");
176
+ }
177
+
99
178
  async function resolvePresetTarget(
100
- preset: import("./ui.ts").Preset,
179
+ preset: import("./ui/ui.ts").Preset,
101
180
  ctx: CommandContext,
102
181
  ): Promise<ReviewTarget | undefined> {
103
182
  switch (preset) {
@@ -160,7 +239,9 @@ async function executeReview(options: ReviewExecutionOptions): Promise<ReviewRes
160
239
  return runReview({ ...options, target: resolved.target });
161
240
  }
162
241
 
242
+ // biome-ignore lint/complexity/useMaxParams: needs to pass brief, target, diffBytes, ctx, and pi for the full pipeline
163
243
  async function runReviewWithLoader(
244
+ brief: ReviewBrief,
164
245
  target: ReviewTarget,
165
246
  maxDiffBytes: number,
166
247
  ctx: CommandContext,
@@ -183,6 +264,7 @@ async function runReviewWithLoader(
183
264
 
184
265
  executeReview({
185
266
  target,
267
+ brief,
186
268
  maxDiffBytes,
187
269
  ctx,
188
270
  signal: widget.signal,
@@ -206,9 +288,8 @@ async function runReviewWithLoader(
206
288
  }
207
289
 
208
290
  function runReview(options: ReviewExecutionOptions): Promise<ReviewResult> {
209
- const { target, maxDiffBytes, ctx, signal, onToolActivity, onProgress } = options;
291
+ const { target, brief, ctx, signal, onToolActivity, onProgress } = options;
210
292
  const settings = loadReviewSettings(ctx.cwd);
211
- // ctx.modelRegistry is available because CommandContext extends ExtensionContext
212
293
  const model = resolveReviewerModel(settings, ctx.modelRegistry, ctx.model);
213
294
  if (!model) {
214
295
  return Promise.resolve({
@@ -219,24 +300,8 @@ function runReview(options: ReviewExecutionOptions): Promise<ReviewResult> {
219
300
  });
220
301
  }
221
302
 
222
- let diffOrBody = "";
223
- if (target.type === "base-branch" || target.type === "uncommitted") {
224
- diffOrBody = target.diff;
225
- } else if (target.type === "commit") {
226
- diffOrBody = target.show;
227
- }
228
-
229
- const truncated =
230
- target.type === "custom"
231
- ? { text: "", wasTruncated: false, truncatedBytes: 0 }
232
- : maybeTruncateDiff(diffOrBody, maxDiffBytes);
233
- const prompt = buildReviewPrompt(
234
- target,
235
- truncated.text,
236
- truncated.wasTruncated
237
- ? { truncated: true, truncatedBytes: truncated.truncatedBytes }
238
- : undefined,
239
- );
303
+ // Use the approved brief's final prompt directly
304
+ const prompt = brief.finalPrompt;
240
305
 
241
306
  const invocation: ReviewerInvocation = {
242
307
  prompt,
@@ -244,6 +309,7 @@ function runReview(options: ReviewExecutionOptions): Promise<ReviewResult> {
244
309
  modelRegistry: ctx.modelRegistry,
245
310
  cwd: ctx.cwd,
246
311
  target,
312
+ brief,
247
313
  signal,
248
314
  onToolActivity,
249
315
  onProgress,
@@ -1,6 +1,6 @@
1
1
  import type { Model } from "@earendil-works/pi-ai";
2
2
  import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
3
- import type { ReviewTarget } from "./types.ts";
3
+ import type { ReviewBrief, ReviewTarget } from "../types.ts";
4
4
 
5
5
  /** Progress state exposed by the runner for widget integration. */
6
6
  export interface ReviewProgress {
@@ -24,6 +24,8 @@ export interface ReviewerInvocation {
24
24
  cwd: string;
25
25
  signal?: AbortSignal;
26
26
  target: ReviewTarget;
27
+ /** The approved review brief used to generate the prompt. */
28
+ brief?: ReviewBrief;
27
29
  timeoutMs?: number;
28
30
  /** Callback for tool activity events (starts/ends) for widget integration. */
29
31
  onToolActivity?: (event: { toolName: string; phase: "start" | "end" }) => void;
@@ -10,8 +10,8 @@ import {
10
10
  SessionManager,
11
11
  } from "@earendil-works/pi-coding-agent";
12
12
  import { Type } from "typebox";
13
+ import type { ReviewOutputEvent, ReviewResult, ReviewTarget } from "../types.ts";
13
14
  import type { ReviewerInvocation, ReviewProgress } from "./runner-types.ts";
14
- import type { ReviewOutputEvent, ReviewResult, ReviewTarget } from "./types.ts";
15
15
 
16
16
  export type { ReviewerInvocation } from "./runner-types.ts";
17
17
 
@@ -249,8 +249,15 @@ function handleToolEnd(
249
249
  ctx.onToolActivity?.({ toolName: event.toolName, phase: "end" });
250
250
  ctx.onProgress?.({ ...ctx.progress, activities: [...ctx.progress.activities] });
251
251
  }
252
- function handleAgentEnd(ctx: RunnerContext): void {
252
+ function handleAgentEnd(
253
+ event: Extract<AgentSessionEvent, { type: "agent_end" }>,
254
+ ctx: RunnerContext,
255
+ ): void {
253
256
  if (ctx.state.settled || ctx.signal?.aborted || ctx.timeout.aborting) return;
257
+ const retryAwareEvent = event as Extract<AgentSessionEvent, { type: "agent_end" }> & {
258
+ willRetry?: boolean;
259
+ };
260
+ if (retryAwareEvent.willRetry) return;
254
261
  if (ctx.resultHolder.value) {
255
262
  ctx.resolve(
256
263
  ctx.cleanup({ kind: "success", output: ctx.resultHolder.value, target: ctx.target }),
@@ -284,7 +291,7 @@ function handleSessionEvent(event: AgentSessionEvent, ctx: RunnerContext): void
284
291
  handleToolEnd(event, ctx);
285
292
  break;
286
293
  case "agent_end":
287
- handleAgentEnd(ctx);
294
+ handleAgentEnd(event, ctx);
288
295
  break;
289
296
  // Ignore other events (queue_update, compaction, auto_retry, etc.)
290
297
  default:
@@ -299,12 +306,13 @@ export async function runReviewer(inv: ReviewerInvocation): Promise<ReviewResult
299
306
  cwd,
300
307
  signal,
301
308
  target,
309
+ brief,
302
310
  onToolActivity,
303
311
  onProgress,
304
312
  timeoutMs = DEFAULT_TIMEOUT_MS,
305
313
  } = inv;
306
314
  if (signal?.aborted) {
307
- return { kind: "canceled", target };
315
+ return { kind: "canceled", target, brief };
308
316
  }
309
317
  // Holder for the submit_review tool result
310
318
  const resultHolder: { value: ReviewOutputEvent | undefined } = { value: undefined };
@@ -314,7 +322,7 @@ export async function runReviewer(inv: ReviewerInvocation): Promise<ReviewResult
314
322
  session = await createReviewerSession(model, cwd, submitReviewTool, modelRegistry);
315
323
  } catch (err) {
316
324
  const reason = `Failed to create reviewer session: ${err instanceof Error ? err.message : String(err)}`;
317
- return { kind: "failed" as const, reason, target };
325
+ return { kind: "failed" as const, reason, target, brief };
318
326
  }
319
327
  const progress: ReviewProgress = { turns: 0, toolUses: 0, activities: [], tokens: undefined };
320
328
  const state = { settled: false };
@@ -324,7 +332,7 @@ export async function runReviewer(inv: ReviewerInvocation): Promise<ReviewResult
324
332
  state.settled = true;
325
333
  cancelTeardown?.();
326
334
  session.dispose();
327
- return result;
335
+ return brief ? { ...result, brief } : result;
328
336
  };
329
337
  return new Promise<ReviewResult>((resolve) => {
330
338
  const timeoutRef = {
@@ -6,8 +6,8 @@ import {
6
6
  getMergeBase,
7
7
  getUncommittedDiff,
8
8
  getUncommittedFileNames,
9
- } from "./git.ts";
10
- import type { ReviewResult, ReviewTarget } from "./types.ts";
9
+ } from "../git.ts";
10
+ import type { ReviewResult, ReviewTarget } from "../types.ts";
11
11
 
12
12
  interface TargetResolutionContext {
13
13
  cwd: string;
package/src/types.ts CHANGED
@@ -31,6 +31,48 @@ export type ReviewTarget =
31
31
  | { type: "commit"; sha: string; show: string; changedFiles?: string[] }
32
32
  | { type: "custom"; instructions: string; changedFiles?: string[] };
33
33
 
34
+ // ── Review modes and profiles ───────────────────────────────
35
+
36
+ /** Standard vs dynamic review mode. */
37
+ export type ReviewMode = "standard" | "dynamic";
38
+
39
+ /**
40
+ * A review profile definition for standard reviews.
41
+ * Each profile provides a named set of review focus areas
42
+ * and optional system prompt guidance for the reviewer session.
43
+ */
44
+ export interface ReviewProfile {
45
+ id: string;
46
+ label: string;
47
+ description: string;
48
+ /**
49
+ * Additional system-prompt guidance injected into the
50
+ * reviewer child session for this type of review.
51
+ */
52
+ systemPrompt: string;
53
+ }
54
+
55
+ /**
56
+ * A review brief captures what the user wants the reviewer to examine.
57
+ * It is assembled before the child reviewer session starts and
58
+ * influences both the prompt and the final result rendering.
59
+ */
60
+ export interface ReviewBrief {
61
+ mode: ReviewMode;
62
+ /** Human-readable title for the review (e.g. "Review: auth middleware"). */
63
+ title: string;
64
+ /** Summary of what changed (dynamic) or profile description (standard). */
65
+ summary: string;
66
+ /** Intended outcome of the change being reviewed. */
67
+ intent: string;
68
+ /** Focus areas or risk areas for the reviewer to examine. */
69
+ focus: string;
70
+ /** Profile id when mode === "standard", undefined otherwise. */
71
+ profileId?: string;
72
+ /** The assembled final prompt text sent to the reviewer. */
73
+ finalPrompt: string;
74
+ }
75
+
34
76
  export interface ReviewSettings {
35
77
  reviewModel: string;
36
78
  maxDiffBytes: number;
@@ -38,12 +80,13 @@ export interface ReviewSettings {
38
80
  }
39
81
 
40
82
  export type ReviewResult =
41
- | { kind: "success"; output: ReviewOutputEvent; target: ReviewTarget }
42
- | { kind: "failed"; reason: string; target: ReviewTarget }
43
- | { kind: "canceled"; target: ReviewTarget }
83
+ | { kind: "success"; output: ReviewOutputEvent; target: ReviewTarget; brief?: ReviewBrief }
84
+ | { kind: "failed"; reason: string; target: ReviewTarget; brief?: ReviewBrief }
85
+ | { kind: "canceled"; target: ReviewTarget; brief?: ReviewBrief }
44
86
  | {
45
87
  kind: "timeout";
46
88
  target: ReviewTarget;
47
89
  timeoutMs: number;
48
90
  partialOutput?: string;
91
+ brief?: ReviewBrief;
49
92
  };
@@ -1,4 +1,4 @@
1
- import type { ReviewResult } from "./types.ts";
1
+ import type { ReviewResult } from "../types.ts";
2
2
 
3
3
  function priorityLabel(priority: number): string {
4
4
  switch (priority) {
@@ -39,11 +39,22 @@ function formatTimeoutContent(result: Extract<ReviewResult, { kind: "timeout" }>
39
39
  function formatSuccessContent(result: Extract<ReviewResult, { kind: "success" }>): string {
40
40
  const output = result.output;
41
41
  const confidencePercent = Math.round(output.overall_confidence_score * 100);
42
- const lines = [
43
- "## Code Review Result",
44
- "",
45
- `Verdict: ${output.overall_correctness} (confidence: ${confidencePercent}%)`,
46
- ];
42
+ const lines: string[] = ["## Code Review Result"];
43
+
44
+ // Show the review request context (if brief is available)
45
+ if (result.brief) {
46
+ lines.push("", "### Review Requested", "");
47
+ if (result.brief.mode === "standard" && result.brief.profileId) {
48
+ lines.push(`**Mode:** Standard (${result.brief.profileId})`);
49
+ } else {
50
+ lines.push("**Mode:** Dynamic");
51
+ }
52
+ lines.push(`**Summary:** ${result.brief.summary}`);
53
+ lines.push(`**Intended outcome:** ${result.brief.intent}`);
54
+ lines.push(`**Focus areas:** ${result.brief.focus}`);
55
+ }
56
+
57
+ lines.push("", `Verdict: ${output.overall_correctness} (confidence: ${confidencePercent}%)`);
47
58
 
48
59
  if (output.findings.length > 0) {
49
60
  lines.push("", "### Findings", "", ...formatFindings(output.findings));
@@ -1,7 +1,11 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { CancellableLoader, Container, Text, type TUI } from "@earendil-works/pi-tui";
3
- import { formatTokens } from "./runner.ts";
4
- import type { ReviewProgress } from "./runner-types.ts";
2
+ import { CancellableLoader, Container, Text } from "@earendil-works/pi-tui";
3
+ import { formatTokens } from "../tool/runner.ts";
4
+ import type { ReviewProgress } from "../tool/runner-types.ts";
5
+
6
+ interface ReviewProgressTui {
7
+ requestRender(): void;
8
+ }
5
9
 
6
10
  /**
7
11
  * Live progress widget for code review.
@@ -13,16 +17,16 @@ export class ReviewProgressWidget extends Container {
13
17
  private _message: string;
14
18
  private _progress: ReviewProgress = { turns: 0, toolUses: 0, activities: [] };
15
19
  private _loader: CancellableLoader;
16
- private _tui: TUI;
20
+ private _tui: ReviewProgressTui;
17
21
  private _theme: Theme;
18
22
 
19
- constructor(tui: TUI, theme: Theme, message: string) {
23
+ constructor(tui: ReviewProgressTui, theme: Theme, message: string) {
20
24
  super();
21
25
  this._tui = tui;
22
26
  this._theme = theme;
23
27
  this._message = message;
24
28
  this._loader = new CancellableLoader(
25
- tui,
29
+ tui as ConstructorParameters<typeof CancellableLoader>[0],
26
30
  (s: string) => theme.fg("accent", s),
27
31
  (s: string) => theme.fg("muted", s),
28
32
  message,
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
3
- import type { ReviewFinding, ReviewResult } from "./types.ts";
3
+ import type { ReviewFinding, ReviewResult } from "../types.ts";
4
4
 
5
5
  export function registerReviewRenderer(pi: ExtensionAPI): void {
6
6
  pi.registerMessageRenderer("supi-review", (message, { expanded }, theme) => {
@@ -35,6 +35,18 @@ function renderSuccess(
35
35
  container.addChild(new Text(theme.fg("accent", "◆ Code Review Results"), 1, 0));
36
36
  container.addChild(new Spacer(1));
37
37
 
38
+ // Brief context
39
+ if (result.brief) {
40
+ const modeLabel =
41
+ result.brief.mode === "standard" && result.brief.profileId
42
+ ? `Standard (${result.brief.profileId})`
43
+ : "Dynamic";
44
+ container.addChild(new Text(theme.fg("muted", `Review mode: ${modeLabel}`), 1, 0));
45
+ container.addChild(new Text(theme.fg("muted", `Summary: ${result.brief.summary}`), 1, 0));
46
+ container.addChild(new Text(theme.fg("muted", `Focus: ${result.brief.focus}`), 1, 0));
47
+ container.addChild(new Spacer(1));
48
+ }
49
+
38
50
  const normalizedVerdict = output.overall_correctness.toLowerCase();
39
51
  const verdictColor = normalizedVerdict.includes("incorrect")
40
52
  ? "warning"
@@ -1,9 +1,17 @@
1
1
  import { DynamicBorder, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
3
- import { getLocalBranches, getRecentCommits } from "./git.ts";
3
+ import { getLocalBranches, getRecentCommits } from "../git.ts";
4
+ import { getProfiles } from "../profiles.ts";
5
+ import type { ReviewMode } from "../types.ts";
4
6
 
5
7
  export type Preset = "base-branch" | "uncommitted" | "commit" | "custom";
6
8
 
9
+ export interface DynamicInputs {
10
+ summary: string;
11
+ intent: string;
12
+ focus: string;
13
+ }
14
+
7
15
  interface SelectFromListOptions<T> {
8
16
  items: SelectItem[];
9
17
  title: string;
@@ -83,6 +91,90 @@ export async function selectAutoFix(
83
91
  });
84
92
  }
85
93
 
94
+ /** Select review mode: standard or dynamic. */
95
+ export async function selectReviewMode(ctx: ExtensionContext): Promise<ReviewMode | undefined> {
96
+ return selectFromList(ctx, {
97
+ items: [
98
+ {
99
+ value: "dynamic",
100
+ label: "Dynamic review",
101
+ description:
102
+ "Describe what changed and what to focus on — the agent drafts the review brief",
103
+ },
104
+ {
105
+ value: "standard",
106
+ label: "Standard review",
107
+ description: "Use a predefined review profile (general, security, API & maintainability)",
108
+ },
109
+ ],
110
+ title: "Select review mode",
111
+ maxHeight: 4,
112
+ onSelect: (item) => item.value as ReviewMode,
113
+ });
114
+ }
115
+
116
+ /** Select a standard review profile. */
117
+ export async function selectProfile(ctx: ExtensionContext): Promise<string | undefined> {
118
+ const profiles = getProfiles();
119
+ return selectFromList(ctx, {
120
+ items: profiles.map((p) => ({
121
+ value: p.id,
122
+ label: p.label,
123
+ description: p.description,
124
+ })),
125
+ title: "Select review profile",
126
+ maxHeight: Math.min(profiles.length + 1, 6),
127
+ onSelect: (item) => item.value as string,
128
+ });
129
+ }
130
+
131
+ /** Collect structured inputs for a dynamic review brief via a single editor prompt. */
132
+ export async function collectDynamicInputs(
133
+ ctx: ExtensionContext,
134
+ ): Promise<DynamicInputs | undefined> {
135
+ const template = [
136
+ "# Dynamic Review Brief",
137
+ "",
138
+ "Fill in the fields below. Lines starting with `#` are comments and will be stripped.",
139
+ "",
140
+ "# What changed? (summary of the changes being reviewed)",
141
+ "",
142
+ "# What is the intended outcome of this change?",
143
+ "",
144
+ "# What should the reviewer focus on? (risk areas, specific concerns)",
145
+ "",
146
+ ].join("\n");
147
+
148
+ const text = await ctx.ui.editor("Dynamic Review Brief", template);
149
+ if (!text?.trim()) return undefined;
150
+
151
+ // Parse non-comment lines into sections
152
+ const lines = text.split("\n").filter((l) => !l.trim().startsWith("#") && l.trim().length > 0);
153
+ if (lines.length < 3) {
154
+ ctx.ui.notify("Please provide all three fields: summary, intent, and focus", "warning");
155
+ return undefined;
156
+ }
157
+
158
+ return {
159
+ summary: lines[0]?.trim() ?? "",
160
+ intent: lines[1]?.trim() ?? "",
161
+ focus: lines.slice(2).join(" ").trim(),
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Show the full draft prompt in an editor for review and editing.
167
+ * Returns the edited prompt text, or undefined if the user cancels.
168
+ */
169
+ export async function approveBriefViaEditor(
170
+ ctx: ExtensionContext,
171
+ draftPrompt: string,
172
+ ): Promise<string | undefined> {
173
+ const result = await ctx.ui.editor("Review the draft and edit if needed", draftPrompt);
174
+ if (!result?.trim()) return undefined;
175
+ return result;
176
+ }
177
+
86
178
  export async function selectBranch(ctx: ExtensionContext): Promise<string | undefined> {
87
179
  const branches = await getLocalBranches(ctx.cwd);
88
180
  if (branches.length === 0) {