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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Install dependencies
run: bun install

- name: Generate Pi extension shared copies
- name: Generate Pi extension shared helpers
run: bash apps/pi-extension/vendor.sh

- name: Type check
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Install dependencies
run: bun install

- name: Generate Pi extension shared copies
- name: Generate Pi extension shared helpers
run: bash apps/pi-extension/vendor.sh

- name: Type check
Expand Down
14 changes: 9 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ plannotator/
│ ├── shared/ # Shared types, utilities, and cross-runtime logic
│ │ ├── storage.ts # Plan saving, version history, archive listing (node:fs only)
│ │ ├── draft.ts # Annotation draft persistence (node:fs only)
│ │ └── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ │ ├── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ │ ├── plugin-protocol.ts # JSON protocol for binary-owned plugin commands
│ │ ├── plugin-client.ts # Shared OpenCode/Pi subprocess client for plannotator plugin commands
│ │ └── plugin-binary.ts # Binary discovery, compatibility checks, and installer bridge
│ ├── editor/ # Plan review app
│ │ ├── App.tsx # Main plan review app
│ │ └── shortcuts.ts # planReviewSurface + annotateSurface — composes plan-review scopes into per-surface registries
Expand All @@ -90,12 +93,11 @@ plannotator/

## Server Runtimes

There are two separate server implementations with the same API surface:
Plannotator has one server implementation:

- **Bun server** (`packages/server/`) — used by both Claude Code (`apps/hook/`) and OpenCode (`apps/opencode-plugin/`). These plugins import directly from `@plannotator/server`.
- **Pi server** (`apps/pi-extension/server/`) — a standalone Node.js server for the Pi extension. It mirrors the Bun server's API but uses `node:http` primitives instead of Bun's `Request`/`Response` APIs.
- **Bun server** (`packages/server/`) — owns plan review, code review, annotate, archive, and shared browser APIs.

When adding or modifying server endpoints, both implementations must be updated. Runtime-agnostic logic (store, validation, types) lives in `packages/shared/` and is imported by both.
Claude Code runs this server through the released `plannotator` binary entrypoint. OpenCode and Pi do not package their own server implementations; they call the same binary through the plugin protocol in `packages/shared/plugin-protocol.ts`. Runtime-agnostic logic (store, validation, types) lives in `packages/shared/`.

## Installation

Expand All @@ -118,6 +120,8 @@ claude --plugin-dir ./apps/hook
| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
| `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
| `PLANNOTATOR_BIN` | Explicit `plannotator` binary path for OpenCode/Pi plugin clients. Overrides PATH and standard install locations. |
| `PLANNOTATOR_DISABLE_AUTO_INSTALL` | Set to `1` / `true` to make OpenCode/Pi fail clearly instead of running the official installer when no compatible binary is found. |
| `PLANNOTATOR_SHARE` | Set to `disabled` to turn off URL sharing entirely. Default: enabled. |
| `PLANNOTATOR_SHARE_URL` | Custom base URL for share links (self-hosted portal). Default: `https://share.plannotator.ai`. |
| `PLANNOTATOR_PASTE_URL` | Base URL of the paste service API for short URL sharing. Default: `https://plannotator-paste.plannotator.workers.dev`. |
Expand Down
2 changes: 1 addition & 1 deletion apps/copilot/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "plannotator-copilot",
"description": "Interactive Plan & Code Review for GitHub Copilot CLI. Visual annotations, team sharing, structured feedback.",
"version": "0.19.18",
"version": "0.19.17",
"author": { "name": "backnotprop" },
"repository": "https://github.com/backnotprop/plannotator",
"license": "MIT OR Apache-2.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/hook/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "plannotator",
"description": "Interactive Plan Review: Mark up and refine your plans using a UI, easily share for team collaboration, automatically integrates with plan mode hooks.",
"version": "0.19.18",
"version": "0.19.17",
"author": {
"name": "backnotprop"
},
Expand Down
2 changes: 2 additions & 0 deletions apps/hook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ curl -fsSL https://plannotator.ai/install.cmd -o install.cmd && install.cmd && d

Released binaries ship with SHA256 sidecars and [SLSA build provenance](https://slsa.dev/) attestations from v0.17.2 onwards. See the [installation docs](https://plannotator.ai/docs/getting-started/installation/#verifying-your-install) for version pinning and verification commands.

The released binary owns Plannotator's browser server runtime for Claude Code, OpenCode, and Pi. See [Single Binary Runtime](../../docs/single-binary-runtime.md) for the plugin client boundary and daemon-next design.

---

[Plugin Installation](#plugin-installation) · [Manual Installation (Hooks)](#manual-installation-hooks) · [Obsidian Integration](#obsidian-integration)
Expand Down
2 changes: 2 additions & 0 deletions apps/hook/server/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function formatTopLevelHelp(): string {
" plannotator archive",
" plannotator sessions",
" plannotator improve-context",
" plannotator plugin capabilities",
"",
"Note:",
" running 'plannotator' without arguments is for hook integration and expects JSON on stdin",
Expand All @@ -50,6 +51,7 @@ export function formatInteractiveNoArgClarification(): string {
" plannotator last",
" plannotator archive",
" plannotator sessions",
" plannotator plugin capabilities",
"",
"Run 'plannotator --help' for top-level usage.",
].join("\n");
Expand Down
37 changes: 0 additions & 37 deletions apps/hook/server/codex-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,43 +310,6 @@ describe("getLastCodexMessage", () => {
expect(result).not.toBeNull();
expect(result!.text).toBe("Valid message");
});

test("can ignore assistant messages from the active Codex turn", () => {
const previousTurnId = "turn-previous";
const activeTurnId = "turn-active";
const path = writeTempRollout(
buildRollout(
sessionMeta(),
turnStarted(previousTurnId),
userMessage("Explain the thing"),
assistantMessage("Substantive final answer"),
turnCompleted(previousTurnId),
turnStarted(activeTurnId),
userMessage("[$plannotator-last]"),
assistantMessage("I’ll open Plannotator on my last response.")
)
);

const result = getLastCodexMessage(path, { beforeActiveTurn: true });
expect(result).not.toBeNull();
expect(result!.text).toBe("Substantive final answer");
});

test("keeps default latest-message behavior inside an active turn", () => {
const turnId = "turn-active";
const path = writeTempRollout(
buildRollout(
sessionMeta(),
assistantMessage("Previous answer"),
turnStarted(turnId),
assistantMessage("Current status update")
)
);

const result = getLastCodexMessage(path);
expect(result).not.toBeNull();
expect(result!.text).toBe("Current status update");
});
});

describe("getLatestCodexPlan", () => {
Expand Down
32 changes: 2 additions & 30 deletions apps/hook/server/codex-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,12 @@ export interface CodexPlanResult {
source: CodexPlanSource;
}

export interface GetLastCodexMessageOptions {
beforeActiveTurn?: boolean;
}

export interface GetLatestCodexPlanOptions {
turnId?: string;
stopHookActive?: boolean;
}

const TURN_START_TYPES = new Set(["task_started", "turn_started"]);
const TURN_COMPLETE_TYPES = new Set(["task_complete", "turn_completed"]);
const PROPOSED_PLAN_RE = /<proposed_plan>([\s\S]*?)<\/proposed_plan>/gi;

// --- Rollout File Discovery ---
Expand Down Expand Up @@ -205,24 +200,6 @@ function findTurnStartIndex(entries: RolloutEntry[], turnId?: string): number {
return lastTurnContext === -1 ? 0 : lastTurnContext;
}

function findActiveTurnStartIndex(entries: RolloutEntry[]): number {
const latestTurnStart = findLastIndex(
entries,
(entry) =>
entry.type === "event_msg" &&
TURN_START_TYPES.has(entry.payload?.type || "")
);
if (latestTurnStart === -1) return -1;

const latestTurnComplete = findLastIndex(
entries,
(entry) =>
entry.type === "event_msg" &&
TURN_COMPLETE_TYPES.has(entry.payload?.type || "")
);
return latestTurnStart > latestTurnComplete ? latestTurnStart : -1;
}

function isHookPromptMessage(entry: RolloutEntry): boolean {
if (entry.type !== "response_item") return false;
if (entry.payload?.type !== "message") return false;
Expand Down Expand Up @@ -318,17 +295,12 @@ function pickLatestPreferredPlan(
* Extracts output_text blocks from payload.content.
*/
export function getLastCodexMessage(
rolloutPath: string,
options: GetLastCodexMessageOptions = {}
rolloutPath: string
): { text: string } | null {
const entries = parseRolloutEntries(rolloutPath);
const activeTurnStart = options.beforeActiveTurn
? findActiveTurnStartIndex(entries)
: -1;
const endIndex = activeTurnStart === -1 ? entries.length - 1 : activeTurnStart - 1;

// Walk backward
for (let i = endIndex; i >= 0; i--) {
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
if (entry.type !== "response_item") continue;
if (entry.payload?.type !== "message") continue;
Expand Down
10 changes: 10 additions & 0 deletions apps/hook/server/html-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Keep text imports isolated so protocol-only commands can run from source
// before apps/hook/dist has been built.
// @ts-ignore - Bun import attribute for text
import planHtml from "../dist/index.html" with { type: "text" };

// @ts-ignore - Bun import attribute for text
import reviewHtml from "../dist/review.html" with { type: "text" };

export const planHtmlContent = planHtml as unknown as string;
export const reviewHtmlContent = reviewHtml as unknown as string;
Loading
Loading