Skip to content
Merged
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
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# Plannotator

Interactive Plan & Code Review for AI Coding Agents. Mark up and refine your plans or code diffs using a visual UI, share for team collaboration, and seamlessly integrate with **Claude Code**, **OpenCode**, **Pi**, and **Codex**.
Interactive Plan & Code Review for AI Coding Agents. Mark up and refine your plans or code diffs using a visual UI, share for team collaboration, and seamlessly integrate with **Claude Code**, **Copilot CLI**, **Gemini CLI**, **OpenCode**, **Pi**, and **Codex**.

**Plan Mode Demos:**
<table>
Expand Down Expand Up @@ -54,6 +54,7 @@ Plannotator lets you privately share plans, annotations, and feedback with colle

- [Claude Code](#install-for-claude-code)
- [Copilot CLI](#install-for-copilot-cli)
- [Gemini CLI](#install-for-gemini-cli)
- [OpenCode](#install-for-opencode)
- [Pi](#install-for-pi)
- [Codex](#install-for-codex)
Expand Down Expand Up @@ -116,6 +117,39 @@ See [apps/copilot/README.md](apps/copilot/README.md) for details.

---

## Install for Gemini CLI

**Install the `plannotator` command:**

**macOS / Linux / WSL:**

```bash
curl -fsSL https://plannotator.ai/install.sh | bash
```

**Windows PowerShell:**

```powershell
irm https://plannotator.ai/install.ps1 | iex
```

The installer auto-detects Gemini CLI (checks for `~/.gemini`) and configures the plan review hook and policy. It also installs `/plannotator-review` and `/plannotator-annotate` slash commands.

**Then in Gemini CLI:**

```
/plan # Enter plan mode — plans open in your browser
/plannotator-review # Code review for current changes
/plannotator-review <pr-url> # Review a GitHub pull request
/plannotator-annotate <file.md> # Annotate a markdown file
```

Requires Gemini CLI 0.36.0 or later.

See [apps/gemini/README.md](apps/gemini/README.md) for details.

---

## Install for OpenCode

Add to your `opencode.json`:
Expand Down
89 changes: 89 additions & 0 deletions apps/gemini/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Plannotator for Gemini CLI

Interactive plan review, code review, and markdown annotation for Google Gemini CLI.

## Install

**Install the `plannotator` command:**

**macOS / Linux / WSL:**

```bash
curl -fsSL https://plannotator.ai/install.sh | bash
```

**Windows PowerShell:**

```powershell
irm https://plannotator.ai/install.ps1 | iex
```

The installer auto-detects Gemini CLI (checks for `~/.gemini`) and configures:

- **Policy file** at `~/.gemini/policies/plannotator.toml` — allows `exit_plan_mode` without the TUI confirmation dialog
- **Hook** in `~/.gemini/settings.json` — intercepts `exit_plan_mode` and opens the browser review UI
- **Slash commands** at `~/.gemini/commands/` — `/plannotator-review` and `/plannotator-annotate`

## How It Works

### Plan Mode Integration

When you use `/plan` in Gemini CLI:

1. The agent creates a plan and calls `exit_plan_mode`
2. The user policy auto-allows `exit_plan_mode` (skipping the TUI dialog)
3. The `BeforeTool` hook intercepts the call, reads the plan from disk, and opens the Plannotator review UI in your browser
4. You review the plan, optionally add annotations
5. **Approve** → the plan is accepted and the agent proceeds
6. **Deny** → the agent receives your feedback and revises the plan

### Available Commands

| Command | Description |
|---------|-------------|
| `/plannotator-review` | Open interactive code review for current changes or a PR URL |
| `/plannotator-review <pr-url>` | Review a GitHub pull request |
| `/plannotator-annotate <file>` | Open interactive annotation UI for a markdown file |

## Manual Setup

If the installer didn't auto-configure your settings (e.g. `~/.gemini/settings.json` already existed), add the hook manually:

```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "exit_plan_mode",
"hooks": [
{
"type": "command",
"command": "plannotator",
"timeout": 345600
}
]
}
]
}
}
```

## Environment Variables

| Variable | Description |
|----------|-------------|
| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
| `PLANNOTATOR_BROWSER` | Custom browser to open. macOS: app name or path. Linux/Windows: executable path. |
| `PLANNOTATOR_SHARE` | Set to `disabled` to turn off URL sharing. |

## Requirements

- Gemini CLI 0.36.0 or later
- `plannotator` binary on PATH

## Links

- [Website](https://plannotator.ai)
- [GitHub](https://github.com/backnotprop/plannotator)
- [Docs](https://plannotator.ai/docs/getting-started/installation/)
10 changes: 10 additions & 0 deletions apps/gemini/commands/plannotator-annotate.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description = "Open interactive annotation UI for a markdown file or folder"
prompt = """
## Markdown Annotations

!{plannotator annotate {{args}}}

## Your task

Address the annotation feedback above. The user has reviewed the markdown file and provided specific annotations and comments.
"""
10 changes: 10 additions & 0 deletions apps/gemini/commands/plannotator-review.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description = "Open interactive code review for current changes or a PR URL"
prompt = """
## Code Review Feedback
!{plannotator review {{args}}}
## Your task
If the review above contains feedback or annotations, address them. If no changes were requested, acknowledge and continue.
"""
8 changes: 8 additions & 0 deletions apps/gemini/hooks/plannotator.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Plannotator policy for Gemini CLI
# Allows exit_plan_mode without TUI confirmation so the browser UI is the sole gate.
# Installed to: ~/.gemini/policies/plannotator.toml

[[rule]]
toolName = "exit_plan_mode"
decision = "allow"
priority = 100
16 changes: 16 additions & 0 deletions apps/gemini/hooks/settings-snippet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"hooks": {
"BeforeTool": [
{
"matcher": "exit_plan_mode",
"hooks": [
{
"type": "command",
"command": "plannotator",
"timeout": 345600
}
]
}
]
}
}
105 changes: 69 additions & 36 deletions apps/hook/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,12 +749,30 @@ if (args[0] === "sessions") {

let planContent = "";
let permissionMode = "default";
let isGemini = false;
let planFilename = "";
let event: Record<string, any>;
try {
const event = JSON.parse(eventJson);
planContent = event.tool_input?.plan || "";
event = JSON.parse(eventJson);

// Detect harness: Gemini sends plan_filename (file on disk), Claude Code sends plan (inline)
planFilename = event.tool_input?.plan_filename || event.tool_input?.plan_path || "";
isGemini = !!planFilename;

if (isGemini) {
// Reconstruct full plan path from transcript_path and session_id:
// transcript_path = <projectTempDir>/chats/session-...json
// plan lives at = <projectTempDir>/<session_id>/plans/<plan_filename>
const projectTempDir = path.dirname(path.dirname(event.transcript_path));
const planFilePath = path.join(projectTempDir, event.session_id, "plans", planFilename);
planContent = await Bun.file(planFilePath).text();
} else {
planContent = event.tool_input?.plan || "";
}

permissionMode = event.permission_mode || "default";
} catch {
console.error("Failed to parse hook event from stdin");
} catch (e: any) {
console.error(`Failed to parse hook event from stdin: ${e?.message || e}`);
process.exit(1);
}

Expand All @@ -768,7 +786,7 @@ if (args[0] === "sessions") {
// Start the plan review server
const server = await startPlannotatorServer({
plan: planContent,
origin: detectedOrigin,
origin: isGemini ? "gemini-cli" : detectedOrigin,
permissionMode,
sharingEnabled,
shareBaseUrl,
Expand Down Expand Up @@ -802,41 +820,56 @@ if (args[0] === "sessions") {
// Cleanup
server.stop();

// Output JSON for PermissionRequest hook decision control
if (result.approved) {
// Build updatedPermissions to preserve the current permission mode
const updatedPermissions = [];
if (result.permissionMode) {
updatedPermissions.push({
type: "setMode",
mode: result.permissionMode,
destination: "session",
});
// Output decision in the appropriate format for the harness
if (isGemini) {
if (result.approved) {
console.log(result.feedback ? JSON.stringify({ systemMessage: result.feedback }) : "{}");
} else {
console.log(
JSON.stringify({
decision: "deny",
reason: planDenyFeedback(result.feedback || "", "exit_plan_mode", {
planFilePath: planFilename,
}),
})
);
}
} else {
// Claude Code: PermissionRequest hook decision
if (result.approved) {
const updatedPermissions = [];
if (result.permissionMode) {
updatedPermissions.push({
type: "setMode",
mode: result.permissionMode,
destination: "session",
});
}

console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "PermissionRequest",
decision: {
behavior: "allow",
...(updatedPermissions.length > 0 && { updatedPermissions }),
console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "PermissionRequest",
decision: {
behavior: "allow",
...(updatedPermissions.length > 0 && { updatedPermissions }),
},
},
},
})
);
} else {
console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "PermissionRequest",
decision: {
behavior: "deny",
message: planDenyFeedback(result.feedback || "", "ExitPlanMode"),
})
);
} else {
console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "PermissionRequest",
decision: {
behavior: "deny",
message: planDenyFeedback(result.feedback || "", "ExitPlanMode"),
},
},
},
})
);
})
);
}
}

process.exit(0);
Expand Down
Binary file added apps/marketing/public/assets/icon-gemini.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions apps/marketing/src/components/landing/HeroSection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ import AnnoReplace from './AnnoReplace.astro';
<img src="/assets/icon-copilot.svg" alt="" class="w-4 h-4 icon-copilot" />
<span>Copilot</span>
</button>
<button
class="agent-btn"
data-agent="gemini"
data-command="curl -fsSL https://plannotator.ai/install.sh | bash"
data-video=""
>
<img src="/assets/icon-gemini.png" alt="" class="w-4 h-4" />
<span>Gemini</span>
</button>
<button
class="agent-btn"
data-agent="opencode"
Expand Down Expand Up @@ -214,6 +223,14 @@ import AnnoReplace from './AnnoReplace.astro';
],
detail: 'Then run in Copilot CLI:'
},
gemini: {
steps: [
'/plannotator-review',
'/plannotator-annotate <file.md>',
'Use /plan for plan review with browser approval'
],
detail: 'Then use in Gemini CLI:'
},
pi: {
detail: 'Or try without installing: pi -e npm:@plannotator/pi-extension'
},
Expand Down
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1307,9 +1307,9 @@ const App: React.FC = () => {
}}
disabled={isSubmitting}
isLoading={isSubmitting}
dimmed={origin === 'claude-code' && allAnnotations.length > 0}
dimmed={(origin === 'claude-code' || origin === 'gemini-cli') && allAnnotations.length > 0}
/>
{origin === 'claude-code' && allAnnotations.length > 0 && (
{(origin === 'claude-code' || origin === 'gemini-cli') && allAnnotations.length > 0 && (
<div className="absolute top-full right-0 mt-2 px-3 py-2 bg-popover border border-border rounded-lg shadow-xl text-xs text-foreground w-56 text-center opacity-0 invisible group-hover/approve:opacity-100 group-hover/approve:visible transition-all pointer-events-none z-50">
<div className="absolute bottom-full right-4 border-4 border-transparent border-b-border" />
<div className="absolute bottom-full right-4 mt-px border-4 border-transparent border-b-popover" />
Expand Down
Loading