Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/hook/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import { resolveMarkdownFile, hasMarkdownFiles } from "@plannotator/shared/resol
import { FILE_BROWSER_EXCLUDED } from "@plannotator/shared/reference-common";
import { statSync, rmSync, realpathSync, existsSync } from "fs";
import { parseRemoteUrl } from "@plannotator/shared/repo";
import { getReviewApprovedPrompt } from "@plannotator/shared/prompts";
import { registerSession, unregisterSession, listSessions } from "@plannotator/server/sessions";
import { openBrowser } from "@plannotator/server/browser";
import { detectProjectName } from "@plannotator/server/project";
Expand Down Expand Up @@ -443,7 +444,7 @@ if (args[0] === "sessions") {
if (result.exit) {
console.log("Review session closed without feedback.");
} else if (result.approved) {
console.log("Code review completed — no changes requested.");
console.log(getReviewApprovedPrompt(detectedOrigin));
} else {
console.log(result.feedback);
if (!isPRMode) {
Expand Down
33 changes: 30 additions & 3 deletions apps/marketing/src/content/docs/commands/code-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Review server starts, opens browser with diff viewer
User annotates code, provides feedback
Send Feedback → feedback sent to agent
Approve → "LGTM" sent to agent
Approve → configured approval prompt sent to agent
```

**PR review:**
Expand All @@ -57,7 +57,7 @@ Review server starts, opens browser with diff viewer
User annotates code, provides feedback
Send Feedback → PR context included in feedback
Approve → "LGTM" sent to agent
Approve → configured approval prompt sent to agent
```

## Switching diff types
Expand Down Expand Up @@ -109,10 +109,37 @@ If only one provider is installed, it's used automatically with no configuration
## Submitting feedback

- **Send Feedback** formats your annotations and sends them to the agent
- **Approve** sends "LGTM" to the agent, indicating the changes look good
- **Approve** sends a review-approval prompt to the agent. By default this says no changes were requested, and you can override it in `~/.plannotator/config.json`.

After submission, the agent receives your feedback and can act on it, whether that's fixing issues, explaining decisions, or making the requested changes.

### Customizing the approval prompt

You can override the approval prompt in `~/.plannotator/config.json`.

```json
{
"prompts": {
"review": {
"approved": "# Code Review\n\nCommit these changes now.",
"runtimes": {
"opencode": {
"approved": "# Code Review\n\nNo further changes requested. Commit your work."
}
}
}
}
}
```

Resolution order:

1. `prompts.review.runtimes.<runtime>.approved`
2. `prompts.review.approved`
3. Plannotator's built-in default

Runtime keys use Plannotator's runtime identifiers. For code review, the current values are `claude-code`, `opencode`, `copilot-cli`, `pi`, and `codex`.

## Server API

| Endpoint | Method | Purpose |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ sidebar:
section: "Getting Started"
---

Plannotator is configured through environment variables and hook/plugin configuration files. No config file of its own is required.
Plannotator is configured through environment variables, hook/plugin configuration files, and an optional `~/.plannotator/config.json` file for persistent settings and feature-specific overrides.

## Environment variables

Expand Down Expand Up @@ -63,6 +63,12 @@ This registers the `submit_plan` tool. Slash commands (`/plannotator-review`, `/

Approved and denied plans are saved to `~/.plannotator/plans/` by default. You can change the save directory or disable saving in the Plannotator UI settings (gear icon).

## Config file

Plannotator also reads `~/.plannotator/config.json` for persistent settings and feature-specific overrides.

For example, code review approval prompts can be customized there. See the code review docs for the prompt shape and supported runtime keys.

## Remote mode

When working over SSH, in a devcontainer, or in Docker, set `PLANNOTATOR_REMOTE=1` (or `true`) and `PLANNOTATOR_PORT` to a port you'll forward. Set `PLANNOTATOR_REMOTE=0` / `false` if you need to force local behavior even when SSH env vars are present. See the [remote & devcontainers guide](/docs/guides/remote-and-devcontainers/) for setup instructions.
Expand Down
3 changes: 2 additions & 1 deletion apps/opencode-plugin/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { getGitContext, runGitDiffWithContext } from "@plannotator/server/git";
import { parsePRUrl, checkPRAuth, fetchPR, getCliName, getMRLabel, getMRNumberLabel, getDisplayRepo } from "@plannotator/server/pr";
import { loadConfig, resolveDefaultDiffType, resolveUseJina } from "@plannotator/shared/config";
import { getReviewApprovedPrompt } from "@plannotator/shared/prompts";
import { resolveMarkdownFile } from "@plannotator/shared/resolve-file";
import { htmlToMarkdown } from "@plannotator/shared/html-to-markdown";
import { urlToMarkdown } from "@plannotator/shared/url-to-markdown";
Expand Down Expand Up @@ -123,7 +124,7 @@ export async function handleReviewCommand(
const targetAgent = result.agentSwitch || "build";

const message = result.approved
? "# Code Review\n\nCode review completed — no changes requested."
? getReviewApprovedPrompt("opencode")
: isPRMode
? result.feedback
: `${result.feedback}\n\nPlease address this feedback.`;
Expand Down
3 changes: 2 additions & 1 deletion apps/pi-extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { FILE_BROWSER_EXCLUDED } from "./generated/reference-common.js";
import { htmlToMarkdown } from "./generated/html-to-markdown.js";
import { urlToMarkdown } from "./generated/url-to-markdown.js";
import { loadConfig, resolveUseJina } from "./generated/config.js";
import { getReviewApprovedPrompt } from "./generated/prompts.js";
import {
getLastAssistantMessageText,
hasPlanBrowserHtml,
Expand Down Expand Up @@ -355,7 +356,7 @@ export default function plannotator(pi: ExtensionAPI): void {
} else if (result.feedback) {
if (result.approved) {
pi.sendUserMessage(
`# Code Review\n\nCode review completed — no changes requested.`,
getReviewApprovedPrompt("pi", loadConfig()),
);
} else if (isPRReview) {
// Platform PR actions (approve/comment) return approved:false with a
Expand Down
2 changes: 1 addition & 1 deletion apps/pi-extension/vendor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ cd "$(dirname "$0")"

mkdir -p generated generated/ai/providers

for f in feedback-templates review-core storage draft project pr-provider pr-github pr-gitlab checklist integrations-common repo reference-common favicon resolve-file config external-annotation agent-jobs worktree html-to-markdown url-to-markdown; do
for f in feedback-templates prompts review-core storage draft project pr-provider pr-github pr-gitlab checklist integrations-common repo reference-common favicon resolve-file config external-annotation agent-jobs worktree html-to-markdown url-to-markdown; do
src="../../packages/shared/$f.ts"
printf '// @generated — DO NOT EDIT. Source: packages/shared/%s.ts\n' "$f" | cat - "$src" > "generated/$f.ts"
done
Expand Down
54 changes: 53 additions & 1 deletion packages/shared/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,55 @@ export interface CCLabelConfig {
blocking: boolean;
}

export interface ReviewPromptOverrides {
approved?: string;
}

export type PromptRuntime =
| "claude-code"
| "opencode"
| "copilot-cli"
| "pi"
| "codex"
| "gemini-cli";

export interface PromptConfig {
review?: {
approved?: string;
runtimes?: Partial<Record<PromptRuntime, ReviewPromptOverrides>>;
};
}

export function mergePromptConfig(
current?: PromptConfig,
partial?: PromptConfig,
): PromptConfig | undefined {
if (!current && !partial) return undefined;

const currentReview = current?.review;
const partialReview = partial?.review;

const mergedReview = (currentReview || partialReview)
? {
...currentReview,
...partialReview,
runtimes: (currentReview?.runtimes || partialReview?.runtimes)
? { ...currentReview?.runtimes, ...partialReview?.runtimes }
: undefined,
}
: undefined;

return {
...current,
...partial,
review: mergedReview,
};
}

export interface PlannotatorConfig {
displayName?: string;
diffOptions?: DiffOptions;
prompts?: PromptConfig;
conventionalComments?: boolean;
/** null = explicitly cleared (use defaults), undefined = not set */
conventionalLabels?: CCLabelConfig[] | null;
Expand Down Expand Up @@ -83,7 +129,13 @@ export function saveConfig(partial: Partial<PlannotatorConfig>): void {
const mergedDiffOptions = (current.diffOptions || partial.diffOptions)
? { ...current.diffOptions, ...partial.diffOptions }
: undefined;
const merged = { ...current, ...partial, diffOptions: mergedDiffOptions };
const mergedPrompts = mergePromptConfig(current.prompts, partial.prompts);
const merged = {
...current,
...partial,
diffOptions: mergedDiffOptions,
prompts: mergedPrompts,
};
mkdirSync(CONFIG_DIR, { recursive: true });
writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
} catch (e) {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"./external-annotation": "./external-annotation.ts",
"./agent-jobs": "./agent-jobs.ts",
"./config": "./config.ts",
"./prompts": "./prompts.ts",
"./improvement-hooks": "./improvement-hooks.ts",
"./worktree": "./worktree.ts",
"./html-to-markdown": "./html-to-markdown.ts",
Expand Down
91 changes: 91 additions & 0 deletions packages/shared/prompts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, test } from "bun:test";
import { mergePromptConfig } from "./config";
import { DEFAULT_REVIEW_APPROVED_PROMPT, getConfiguredPrompt, getReviewApprovedPrompt } from "./prompts";

describe("prompts", () => {
test("falls back to built-in default when no config is present", () => {
expect(getReviewApprovedPrompt("opencode", {})).toBe(DEFAULT_REVIEW_APPROVED_PROMPT);
});

test("uses generic configured review approval prompt", () => {
expect(
getReviewApprovedPrompt("opencode", {
prompts: { review: { approved: "Commit these changes now." } },
}),
).toBe("Commit these changes now.");
});

test("runtime-specific review approval prompt wins over generic prompt", () => {
expect(
getReviewApprovedPrompt("opencode", {
prompts: {
review: {
approved: "Generic approval.",
runtimes: {
opencode: { approved: "OpenCode-specific approval." },
},
},
},
}),
).toBe("OpenCode-specific approval.");
});

test("blank prompt values fall back to the next available default", () => {
expect(
getReviewApprovedPrompt("opencode", {
prompts: {
review: {
approved: " ",
runtimes: {
opencode: { approved: "" },
},
},
},
}),
).toBe(DEFAULT_REVIEW_APPROVED_PROMPT);
});

test("generic loader resolves prompt paths with fallback", () => {
expect(
getConfiguredPrompt({
section: "review",
key: "approved",
runtime: "pi",
fallback: "Fallback",
config: {
prompts: {
review: {
runtimes: {
pi: { approved: "Pi prompt" },
},
},
},
},
}),
).toBe("Pi prompt");
});

test("mergePromptConfig keeps generic and sibling runtime prompts", () => {
const merged = mergePromptConfig(
{
review: {
approved: "Generic approval.",
runtimes: {
opencode: { approved: "OpenCode approval." },
},
},
},
{
review: {
runtimes: {
"claude-code": { approved: "Claude approval." },
},
},
},
);

expect(merged?.review?.approved).toBe("Generic approval.");
expect(merged?.review?.runtimes?.opencode?.approved).toBe("OpenCode approval.");
expect(merged?.review?.runtimes?.["claude-code"]?.approved).toBe("Claude approval.");
});
});
43 changes: 43 additions & 0 deletions packages/shared/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { loadConfig, type PlannotatorConfig, type PromptRuntime } from "./config";

export const DEFAULT_REVIEW_APPROVED_PROMPT = "# Code Review\n\nCode review completed — no changes requested.";

type PromptSection = "review";
type PromptKey = "approved";

interface PromptLookupOptions {
section: PromptSection;
key: PromptKey;
runtime?: PromptRuntime | null;
config?: PlannotatorConfig;
fallback: string;
}

function normalizePrompt(prompt: string | undefined): string | undefined {
const trimmed = prompt?.trim();
return trimmed ? prompt : undefined;
}

export function getConfiguredPrompt(options: PromptLookupOptions): string {
const resolvedConfig = options.config ?? loadConfig();
const section = resolvedConfig.prompts?.[options.section];
const runtimePrompt = options.runtime
? normalizePrompt(section?.runtimes?.[options.runtime]?.[options.key])
: undefined;
const genericPrompt = normalizePrompt(section?.[options.key]);

return runtimePrompt ?? genericPrompt ?? options.fallback;
}

export function getReviewApprovedPrompt(
runtime?: PromptRuntime | null,
config?: PlannotatorConfig,
): string {
return getConfiguredPrompt({
section: "review",
key: "approved",
runtime,
config,
fallback: DEFAULT_REVIEW_APPROVED_PROMPT,
});
}