@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.
- package/README.md +91 -51
- package/node_modules/@mrclrchtr/supi-core/README.md +52 -41
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/{context-provider-registry.ts → context/context-provider-registry.ts} +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{settings-registry.ts → settings/settings-registry.ts} +1 -1
- package/package.json +4 -3
- package/src/briefs.ts +101 -0
- package/src/profiles.ts +52 -0
- package/src/review.ts +100 -34
- package/src/{runner-types.ts → tool/runner-types.ts} +3 -1
- package/src/{runner.ts → tool/runner.ts} +14 -6
- package/src/{target-resolution.ts → tool/target-resolution.ts} +2 -2
- package/src/types.ts +46 -3
- package/src/{format-content.ts → ui/format-content.ts} +17 -6
- package/src/{progress-widget.ts → ui/progress-widget.ts} +10 -6
- package/src/{renderer.ts → ui/renderer.ts} +13 -1
- package/src/{ui.ts → ui/ui.ts} +93 -1
- /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
For local development:
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
```bash
|
|
16
|
+
pi install ./packages/supi-review
|
|
17
|
+
```
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
## Review modes
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
### Dynamic review
|
|
46
50
|
|
|
47
|
-
|
|
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
|
-
|
|
53
|
+
- what changed
|
|
54
|
+
- the intended outcome
|
|
55
|
+
- what the reviewer should focus on
|
|
61
56
|
|
|
62
|
-
|
|
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
|
-
|
|
59
|
+
### Standard review
|
|
69
60
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
##
|
|
111
|
+
## Source
|
|
77
112
|
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
9
|
+
### As a dependency for another extension
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
pnpm add @mrclrchtr/supi-core
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`@mrclrchtr/supi-core` now has two explicit surfaces:
|
|
15
|
+
### As a pi package
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
+
Installing it as a pi package adds the minimal `/supi-settings` extension surface.
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
## Package surfaces
|
|
25
24
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
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
|
-
##
|
|
28
|
+
## What you get from the API
|
|
34
29
|
|
|
35
|
-
Config
|
|
30
|
+
### Config helpers
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
- `removeSupiConfigKey()`
|
|
52
|
-
- `registerConfigSettings()`
|
|
52
|
+
- project/global scope toggle
|
|
53
|
+
- grouped extension sections
|
|
54
|
+
- searchable setting lists
|
|
53
55
|
|
|
54
|
-
|
|
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
|
-
- `
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
-
|
|
96
|
-
-
|
|
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
|
|
@@ -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,
|
package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts}
RENAMED
|
@@ -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 "
|
|
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,
|
package/node_modules/@mrclrchtr/supi-core/src/{settings-registry.ts → settings/settings-registry.ts}
RENAMED
|
@@ -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 "
|
|
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
|
+
"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.
|
|
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
|
+
}
|
package/src/profiles.ts
ADDED
|
@@ -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 {
|
|
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 {
|
|
26
|
-
import type {
|
|
27
|
-
import {
|
|
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 PI
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
223
|
-
|
|
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 "
|
|
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(
|
|
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 "
|
|
10
|
-
import type { ReviewResult, ReviewTarget } from "
|
|
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 "
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
3
|
-
import { formatTokens } from "
|
|
4
|
-
import type { ReviewProgress } from "
|
|
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:
|
|
20
|
+
private _tui: ReviewProgressTui;
|
|
17
21
|
private _theme: Theme;
|
|
18
22
|
|
|
19
|
-
constructor(tui:
|
|
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 "
|
|
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"
|
package/src/{ui.ts → ui/ui.ts}
RENAMED
|
@@ -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 "
|
|
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) {
|
|
File without changes
|
/package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts}
RENAMED
|
File without changes
|
|
File without changes
|
/package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts}
RENAMED
|
File without changes
|
|
File without changes
|