Skip to content

Commit 1800c24

Browse files
authored
feat(gemini): add Gemini CLI plan review integration
* feat(gemini): add Gemini CLI plan review integration Adds a new `apps/gemini-hook/` adapter that enables Plannotator plan review for Gemini CLI users via the BeforeTool hook system. The adapter reads the plan file from disk (Gemini provides a path, not inline content), delegates to the shared @plannotator/server for the browser-based review UI, and translates the decision back into Gemini's hook output format. Requires an upstream fix (google-gemini/gemini-cli#21802) that makes `decision = "allow"` user policies work for exit_plan_mode, allowing hooks to replace the built-in TUI approval dialog. Includes: - apps/gemini-hook/server/index.ts — stdin/stdout adapter - apps/gemini-hook/hooks/ — policy TOML + settings snippet - scripts/install.sh — Gemini binary download, policy install, settings config For provenance purposes, this commit was AI assisted. * refactor(gemini): use single binary with auto-detection instead of separate app Removes apps/gemini-hook/ — the plannotator binary now auto-detects Gemini CLI from stdin (plan_path = file on disk) vs Claude Code (plan = inline content) and branches input parsing + output formatting. Config fixtures live in apps/gemini/ (policy TOML + settings snippet). Install script gates on ~/.gemini existing so Claude-only users are unaffected. For provenance purposes, this commit was AI assisted. * test(gemini): add manual sandbox script for Gemini CLI integration Three modes: - --simulate: pipes BeforeTool JSON to hook, tests approve/deny output - (default): runs local patched Gemini build - --nightly: installs Gemini nightly and runs it Backs up and restores ~/.gemini config on exit. For provenance purposes, this commit was AI assisted. * feat(gemini): add slash commands, marketing tab, and docs for Gemini CLI - Add /plannotator-review and /plannotator-annotate slash commands (.toml) - Install Gemini slash commands in all three install scripts (sh, ps1, cmd) - Add Gemini tab to marketing landing page with icon - Add Gemini CLI to top-level README install section - Create apps/gemini/README.md with full setup and usage docs - Remove stale dev:gemini script and regenerate bun.lock For provenance purposes, this commit was AI assisted. * fix(gemini): merge hook into existing settings.json instead of printing instructions When ~/.gemini/settings.json already exists, use node to JSON-merge the BeforeTool hook config rather than asking the user to do it manually. Falls back to instructions only if node is unavailable. For provenance purposes, this commit was AI assisted. * fix(gemini): handle plan_filename rename and fix scoping bug Gemini CLI nightly renamed plan_path to plan_filename in exit_plan_mode. Accept both field names for forward/backward compatibility. Reconstruct full plan path from transcript_path + session_id + plans/ + filename. Also hoist planFilename variable out of try block so it's accessible in the deny output path (was causing ReferenceError). For provenance purposes, this commit was AI assisted. * fix(gemini): dim approve button when annotations exist for Gemini CLI Gemini's hook runner ignores systemMessage on the allow path, so approve-with-feedback is silently dropped — same limitation as Claude Code. Extend the existing UI gate to also apply for gemini-cli origin. For provenance purposes, this commit was AI assisted. * fix(gemini): add AGENT_CONFIG entry and fix sandbox simulate mode Register "gemini-cli" in AGENT_CONFIG so the UI shows "Gemini CLI" with proper badge styling instead of generic "Coding Agent" fallback. Update sandbox simulate mode to match production input format: use plan_filename instead of plan_path, include transcript_path, and simulate the Gemini directory structure for path reconstruction. For provenance purposes, this commit was AI assisted.
1 parent 2f3b235 commit 1800c24

File tree

16 files changed

+759
-42
lines changed

16 files changed

+759
-42
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# Plannotator
66

7-
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**.
7+
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**.
88

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

5555
- [Claude Code](#install-for-claude-code)
5656
- [Copilot CLI](#install-for-copilot-cli)
57+
- [Gemini CLI](#install-for-gemini-cli)
5758
- [OpenCode](#install-for-opencode)
5859
- [Pi](#install-for-pi)
5960
- [Codex](#install-for-codex)
@@ -116,6 +117,39 @@ See [apps/copilot/README.md](apps/copilot/README.md) for details.
116117

117118
---
118119

120+
## Install for Gemini CLI
121+
122+
**Install the `plannotator` command:**
123+
124+
**macOS / Linux / WSL:**
125+
126+
```bash
127+
curl -fsSL https://plannotator.ai/install.sh | bash
128+
```
129+
130+
**Windows PowerShell:**
131+
132+
```powershell
133+
irm https://plannotator.ai/install.ps1 | iex
134+
```
135+
136+
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.
137+
138+
**Then in Gemini CLI:**
139+
140+
```
141+
/plan # Enter plan mode — plans open in your browser
142+
/plannotator-review # Code review for current changes
143+
/plannotator-review <pr-url> # Review a GitHub pull request
144+
/plannotator-annotate <file.md> # Annotate a markdown file
145+
```
146+
147+
Requires Gemini CLI 0.36.0 or later.
148+
149+
See [apps/gemini/README.md](apps/gemini/README.md) for details.
150+
151+
---
152+
119153
## Install for OpenCode
120154

121155
Add to your `opencode.json`:

apps/gemini/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Plannotator for Gemini CLI
2+
3+
Interactive plan review, code review, and markdown annotation for Google Gemini CLI.
4+
5+
## Install
6+
7+
**Install the `plannotator` command:**
8+
9+
**macOS / Linux / WSL:**
10+
11+
```bash
12+
curl -fsSL https://plannotator.ai/install.sh | bash
13+
```
14+
15+
**Windows PowerShell:**
16+
17+
```powershell
18+
irm https://plannotator.ai/install.ps1 | iex
19+
```
20+
21+
The installer auto-detects Gemini CLI (checks for `~/.gemini`) and configures:
22+
23+
- **Policy file** at `~/.gemini/policies/plannotator.toml` — allows `exit_plan_mode` without the TUI confirmation dialog
24+
- **Hook** in `~/.gemini/settings.json` — intercepts `exit_plan_mode` and opens the browser review UI
25+
- **Slash commands** at `~/.gemini/commands/``/plannotator-review` and `/plannotator-annotate`
26+
27+
## How It Works
28+
29+
### Plan Mode Integration
30+
31+
When you use `/plan` in Gemini CLI:
32+
33+
1. The agent creates a plan and calls `exit_plan_mode`
34+
2. The user policy auto-allows `exit_plan_mode` (skipping the TUI dialog)
35+
3. The `BeforeTool` hook intercepts the call, reads the plan from disk, and opens the Plannotator review UI in your browser
36+
4. You review the plan, optionally add annotations
37+
5. **Approve** → the plan is accepted and the agent proceeds
38+
6. **Deny** → the agent receives your feedback and revises the plan
39+
40+
### Available Commands
41+
42+
| Command | Description |
43+
|---------|-------------|
44+
| `/plannotator-review` | Open interactive code review for current changes or a PR URL |
45+
| `/plannotator-review <pr-url>` | Review a GitHub pull request |
46+
| `/plannotator-annotate <file>` | Open interactive annotation UI for a markdown file |
47+
48+
## Manual Setup
49+
50+
If the installer didn't auto-configure your settings (e.g. `~/.gemini/settings.json` already existed), add the hook manually:
51+
52+
```json
53+
{
54+
"hooks": {
55+
"BeforeTool": [
56+
{
57+
"matcher": "exit_plan_mode",
58+
"hooks": [
59+
{
60+
"type": "command",
61+
"command": "plannotator",
62+
"timeout": 345600
63+
}
64+
]
65+
}
66+
]
67+
}
68+
}
69+
```
70+
71+
## Environment Variables
72+
73+
| Variable | Description |
74+
|----------|-------------|
75+
| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
76+
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
77+
| `PLANNOTATOR_BROWSER` | Custom browser to open. macOS: app name or path. Linux/Windows: executable path. |
78+
| `PLANNOTATOR_SHARE` | Set to `disabled` to turn off URL sharing. |
79+
80+
## Requirements
81+
82+
- Gemini CLI 0.36.0 or later
83+
- `plannotator` binary on PATH
84+
85+
## Links
86+
87+
- [Website](https://plannotator.ai)
88+
- [GitHub](https://github.com/backnotprop/plannotator)
89+
- [Docs](https://plannotator.ai/docs/getting-started/installation/)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
description = "Open interactive annotation UI for a markdown file or folder"
2+
prompt = """
3+
## Markdown Annotations
4+
5+
!{plannotator annotate {{args}}}
6+
7+
## Your task
8+
9+
Address the annotation feedback above. The user has reviewed the markdown file and provided specific annotations and comments.
10+
"""
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
description = "Open interactive code review for current changes or a PR URL"
2+
prompt = """
3+
## Code Review Feedback
4+
5+
!{plannotator review {{args}}}
6+
7+
## Your task
8+
9+
If the review above contains feedback or annotations, address them. If no changes were requested, acknowledge and continue.
10+
"""

apps/gemini/hooks/plannotator.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Plannotator policy for Gemini CLI
2+
# Allows exit_plan_mode without TUI confirmation so the browser UI is the sole gate.
3+
# Installed to: ~/.gemini/policies/plannotator.toml
4+
5+
[[rule]]
6+
toolName = "exit_plan_mode"
7+
decision = "allow"
8+
priority = 100
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"hooks": {
3+
"BeforeTool": [
4+
{
5+
"matcher": "exit_plan_mode",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "plannotator",
10+
"timeout": 345600
11+
}
12+
]
13+
}
14+
]
15+
}
16+
}

apps/hook/server/index.ts

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -749,12 +749,30 @@ if (args[0] === "sessions") {
749749

750750
let planContent = "";
751751
let permissionMode = "default";
752+
let isGemini = false;
753+
let planFilename = "";
754+
let event: Record<string, any>;
752755
try {
753-
const event = JSON.parse(eventJson);
754-
planContent = event.tool_input?.plan || "";
756+
event = JSON.parse(eventJson);
757+
758+
// Detect harness: Gemini sends plan_filename (file on disk), Claude Code sends plan (inline)
759+
planFilename = event.tool_input?.plan_filename || event.tool_input?.plan_path || "";
760+
isGemini = !!planFilename;
761+
762+
if (isGemini) {
763+
// Reconstruct full plan path from transcript_path and session_id:
764+
// transcript_path = <projectTempDir>/chats/session-...json
765+
// plan lives at = <projectTempDir>/<session_id>/plans/<plan_filename>
766+
const projectTempDir = path.dirname(path.dirname(event.transcript_path));
767+
const planFilePath = path.join(projectTempDir, event.session_id, "plans", planFilename);
768+
planContent = await Bun.file(planFilePath).text();
769+
} else {
770+
planContent = event.tool_input?.plan || "";
771+
}
772+
755773
permissionMode = event.permission_mode || "default";
756-
} catch {
757-
console.error("Failed to parse hook event from stdin");
774+
} catch (e: any) {
775+
console.error(`Failed to parse hook event from stdin: ${e?.message || e}`);
758776
process.exit(1);
759777
}
760778

@@ -768,7 +786,7 @@ if (args[0] === "sessions") {
768786
// Start the plan review server
769787
const server = await startPlannotatorServer({
770788
plan: planContent,
771-
origin: detectedOrigin,
789+
origin: isGemini ? "gemini-cli" : detectedOrigin,
772790
permissionMode,
773791
sharingEnabled,
774792
shareBaseUrl,
@@ -802,41 +820,56 @@ if (args[0] === "sessions") {
802820
// Cleanup
803821
server.stop();
804822

805-
// Output JSON for PermissionRequest hook decision control
806-
if (result.approved) {
807-
// Build updatedPermissions to preserve the current permission mode
808-
const updatedPermissions = [];
809-
if (result.permissionMode) {
810-
updatedPermissions.push({
811-
type: "setMode",
812-
mode: result.permissionMode,
813-
destination: "session",
814-
});
823+
// Output decision in the appropriate format for the harness
824+
if (isGemini) {
825+
if (result.approved) {
826+
console.log(result.feedback ? JSON.stringify({ systemMessage: result.feedback }) : "{}");
827+
} else {
828+
console.log(
829+
JSON.stringify({
830+
decision: "deny",
831+
reason: planDenyFeedback(result.feedback || "", "exit_plan_mode", {
832+
planFilePath: planFilename,
833+
}),
834+
})
835+
);
815836
}
837+
} else {
838+
// Claude Code: PermissionRequest hook decision
839+
if (result.approved) {
840+
const updatedPermissions = [];
841+
if (result.permissionMode) {
842+
updatedPermissions.push({
843+
type: "setMode",
844+
mode: result.permissionMode,
845+
destination: "session",
846+
});
847+
}
816848

817-
console.log(
818-
JSON.stringify({
819-
hookSpecificOutput: {
820-
hookEventName: "PermissionRequest",
821-
decision: {
822-
behavior: "allow",
823-
...(updatedPermissions.length > 0 && { updatedPermissions }),
849+
console.log(
850+
JSON.stringify({
851+
hookSpecificOutput: {
852+
hookEventName: "PermissionRequest",
853+
decision: {
854+
behavior: "allow",
855+
...(updatedPermissions.length > 0 && { updatedPermissions }),
856+
},
824857
},
825-
},
826-
})
827-
);
828-
} else {
829-
console.log(
830-
JSON.stringify({
831-
hookSpecificOutput: {
832-
hookEventName: "PermissionRequest",
833-
decision: {
834-
behavior: "deny",
835-
message: planDenyFeedback(result.feedback || "", "ExitPlanMode"),
858+
})
859+
);
860+
} else {
861+
console.log(
862+
JSON.stringify({
863+
hookSpecificOutput: {
864+
hookEventName: "PermissionRequest",
865+
decision: {
866+
behavior: "deny",
867+
message: planDenyFeedback(result.feedback || "", "ExitPlanMode"),
868+
},
836869
},
837-
},
838-
})
839-
);
870+
})
871+
);
872+
}
840873
}
841874

842875
process.exit(0);
45.6 KB
Loading

apps/marketing/src/components/landing/HeroSection.astro

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ import AnnoReplace from './AnnoReplace.astro';
6262
<img src="/assets/icon-copilot.svg" alt="" class="w-4 h-4 icon-copilot" />
6363
<span>Copilot</span>
6464
</button>
65+
<button
66+
class="agent-btn"
67+
data-agent="gemini"
68+
data-command="curl -fsSL https://plannotator.ai/install.sh | bash"
69+
data-video=""
70+
>
71+
<img src="/assets/icon-gemini.png" alt="" class="w-4 h-4" />
72+
<span>Gemini</span>
73+
</button>
6574
<button
6675
class="agent-btn"
6776
data-agent="opencode"
@@ -214,6 +223,14 @@ import AnnoReplace from './AnnoReplace.astro';
214223
],
215224
detail: 'Then run in Copilot CLI:'
216225
},
226+
gemini: {
227+
steps: [
228+
'/plannotator-review',
229+
'/plannotator-annotate <file.md>',
230+
'Use /plan for plan review with browser approval'
231+
],
232+
detail: 'Then use in Gemini CLI:'
233+
},
217234
pi: {
218235
detail: 'Or try without installing: pi -e npm:@plannotator/pi-extension'
219236
},

bun.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)