Skip to content

Commit c2037b7

Browse files
backnotpropclaude
andauthored
feat: --browser CLI flag & session discovery (#242)
* feat: add --browser CLI flag and session discovery (#135) - Add `--browser <name>` global flag to override which browser opens (sets PLANNOTATOR_BROWSER, works with existing openBrowser() logic) - Add `plannotator sessions` subcommand to list active server sessions with `--open [N]` to reopen in browser and `--clean` to prune stale entries - Track active sessions in ~/.plannotator/sessions/<pid>.json with automatic stale cleanup via PID liveness checks - Register/unregister sessions in all three server modes (plan, review, annotate) - Export ./sessions and ./project from @plannotator/server package Closes #135 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add integration test for session discovery Non-interactive test script that validates: - Session file creation/cleanup lifecycle - Session file content (all fields) - `plannotator sessions` listing, --open, --clean - Stale PID auto-cleanup Also syncs bun.lock versions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add --browser flag and session discovery to configuration page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add troubleshooting page Covers lost tabs (sessions --open), data storage locations, browser issues, and hook not firing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7596854 commit c2037b7

File tree

8 files changed

+671
-7
lines changed

8 files changed

+671
-7
lines changed

apps/hook/server/index.ts

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Plannotator CLI for Claude Code
33
*
4-
* Supports three modes:
4+
* Supports four modes:
55
*
66
* 1. Plan Review (default, no args):
77
* - Spawned by ExitPlanMode hook
@@ -18,6 +18,14 @@
1818
* - Opens any markdown file in the annotation UI
1919
* - Outputs structured feedback to stdout
2020
*
21+
* 4. Sessions (`plannotator sessions`):
22+
* - Lists active Plannotator server sessions
23+
* - `--open [N]` reopens a session in the browser
24+
* - `--clean` removes stale session files
25+
*
26+
* Global flags:
27+
* --browser <name> - Override which browser to open (e.g. "Google Chrome")
28+
*
2129
* Environment variables:
2230
* PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode (preferred)
2331
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
@@ -38,6 +46,10 @@ import {
3846
import { getGitContext, runGitDiff } from "@plannotator/server/git";
3947
import { writeRemoteShareLink } from "@plannotator/server/share-url";
4048
import { resolveMarkdownFile } from "@plannotator/server/resolve-file";
49+
import { registerSession, unregisterSession, listSessions } from "@plannotator/server/sessions";
50+
import { openBrowser } from "@plannotator/server/browser";
51+
import { detectProjectName } from "@plannotator/server/project";
52+
import path from "path";
4153

4254
// Embed the built HTML at compile time
4355
// @ts-ignore - Bun import attribute for text
@@ -51,6 +63,16 @@ const reviewHtmlContent = reviewHtml as unknown as string;
5163
// Check for subcommand
5264
const args = process.argv.slice(2);
5365

66+
// Global flag: --browser <name>
67+
const browserIdx = args.indexOf("--browser");
68+
if (browserIdx !== -1 && args[browserIdx + 1]) {
69+
process.env.PLANNOTATOR_BROWSER = args[browserIdx + 1];
70+
args.splice(browserIdx, 2);
71+
}
72+
73+
// Ensure session cleanup on exit
74+
process.on("exit", () => unregisterSession());
75+
5476
// Check if URL sharing is enabled (default: true)
5577
const sharingEnabled = process.env.PLANNOTATOR_SHARE !== "disabled";
5678

@@ -60,7 +82,52 @@ const shareBaseUrl = process.env.PLANNOTATOR_SHARE_URL || undefined;
6082
// Paste service URL for short URL sharing
6183
const pasteApiUrl = process.env.PLANNOTATOR_PASTE_URL || undefined;
6284

63-
if (args[0] === "review") {
85+
if (args[0] === "sessions") {
86+
// ============================================
87+
// SESSION DISCOVERY MODE
88+
// ============================================
89+
90+
if (args.includes("--clean")) {
91+
// Force cleanup: list sessions (which auto-removes stale entries)
92+
const sessions = listSessions();
93+
console.error(`Cleaned up stale sessions. ${sessions.length} active session(s) remain.`);
94+
process.exit(0);
95+
}
96+
97+
const sessions = listSessions();
98+
99+
if (sessions.length === 0) {
100+
console.error("No active Plannotator sessions.");
101+
process.exit(0);
102+
}
103+
104+
const openIdx = args.indexOf("--open");
105+
if (openIdx !== -1) {
106+
// Open a session in the browser
107+
const nArg = args[openIdx + 1];
108+
const n = nArg ? parseInt(nArg, 10) : 1;
109+
const session = sessions[n - 1];
110+
if (!session) {
111+
console.error(`Session #${n} not found. ${sessions.length} active session(s).`);
112+
process.exit(1);
113+
}
114+
await openBrowser(session.url);
115+
console.error(`Opened ${session.mode} session in browser: ${session.url}`);
116+
process.exit(0);
117+
}
118+
119+
// List sessions as a table
120+
console.error("Active Plannotator sessions:\n");
121+
for (let i = 0; i < sessions.length; i++) {
122+
const s = sessions[i];
123+
const age = Math.round((Date.now() - new Date(s.startedAt).getTime()) / 60000);
124+
const ageStr = age < 60 ? `${age}m` : `${Math.floor(age / 60)}h ${age % 60}m`;
125+
console.error(` #${i + 1} ${s.mode.padEnd(9)} ${s.project.padEnd(20)} ${s.url.padEnd(28)} ${ageStr} ago`);
126+
}
127+
console.error(`\nReopen with: plannotator sessions --open [N]`);
128+
process.exit(0);
129+
130+
} else if (args[0] === "review") {
64131
// ============================================
65132
// CODE REVIEW MODE
66133
// ============================================
@@ -74,6 +141,8 @@ if (args[0] === "review") {
74141
gitContext.defaultBranch
75142
);
76143

144+
const reviewProject = (await detectProjectName()) ?? "_unknown";
145+
77146
// Start review server (even if empty - user can switch diff types)
78147
const server = await startReviewServer({
79148
rawPatch,
@@ -94,6 +163,16 @@ if (args[0] === "review") {
94163
},
95164
});
96165

166+
registerSession({
167+
pid: process.pid,
168+
port: server.port,
169+
url: server.url,
170+
mode: "review",
171+
project: reviewProject,
172+
startedAt: new Date().toISOString(),
173+
label: `review-${reviewProject}`,
174+
});
175+
97176
// Wait for user feedback
98177
const result = await server.waitForDecision();
99178

@@ -150,6 +229,8 @@ if (args[0] === "review") {
150229
console.error(`Resolved: ${absolutePath}`);
151230
const markdown = await Bun.file(absolutePath).text();
152231

232+
const annotateProject = (await detectProjectName()) ?? "_unknown";
233+
153234
// Start the annotate server (reuses plan editor HTML)
154235
const server = await startAnnotateServer({
155236
markdown,
@@ -167,6 +248,16 @@ if (args[0] === "review") {
167248
},
168249
});
169250

251+
registerSession({
252+
pid: process.pid,
253+
port: server.port,
254+
url: server.url,
255+
mode: "annotate",
256+
project: annotateProject,
257+
startedAt: new Date().toISOString(),
258+
label: `annotate-${path.basename(absolutePath)}`,
259+
});
260+
170261
// Wait for user feedback
171262
const result = await server.waitForDecision();
172263

@@ -204,6 +295,8 @@ if (args[0] === "review") {
204295
process.exit(1);
205296
}
206297

298+
const planProject = (await detectProjectName()) ?? "_unknown";
299+
207300
// Start the plan review server
208301
const server = await startPlannotatorServer({
209302
plan: planContent,
@@ -222,6 +315,16 @@ if (args[0] === "review") {
222315
},
223316
});
224317

318+
registerSession({
319+
pid: process.pid,
320+
port: server.port,
321+
url: server.url,
322+
mode: "plan",
323+
project: planProject,
324+
startedAt: new Date().toISOString(),
325+
label: `plan-${planProject}`,
326+
});
327+
225328
// Wait for user decision (blocks until approve/deny)
226329
const result = await server.waitForDecision();
227330

apps/marketing/src/content/docs/getting-started/configuration.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,26 @@ export PLANNOTATOR_BROWSER="/usr/bin/firefox"
8282
export PLANNOTATOR_BROWSER="/path/to/my-open-script.sh"
8383
```
8484

85+
For one-off overrides without changing your shell profile, use the `--browser` flag:
86+
87+
```bash
88+
plannotator review --browser "Safari"
89+
plannotator annotate plan.md --browser "Firefox"
90+
```
91+
92+
## Session discovery
93+
94+
If you accidentally close a Plannotator browser tab, the server is still running — you just need the URL. The `sessions` subcommand lists active sessions and can reopen them:
95+
96+
```bash
97+
plannotator sessions # list active sessions
98+
plannotator sessions --open # reopen most recent session
99+
plannotator sessions --open 2 # reopen a specific session
100+
plannotator sessions --clean # remove stale session files
101+
```
102+
103+
Sessions are tracked automatically. Stale entries from crashed processes are cleaned up on the next listing.
104+
85105
## Disabling sharing
86106

87107
Set `PLANNOTATOR_SHARE=disabled` to remove all sharing UI — the Share tab, copy link action, and import review option are all hidden. Useful for teams working with sensitive plans.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
title: "Troubleshooting"
3+
description: "Common issues and how to resolve them."
4+
sidebar:
5+
order: 24
6+
section: "Guides"
7+
---
8+
9+
## Lost a Plannotator tab?
10+
11+
If you accidentally close a Plannotator browser tab, the server is still running in the background. You can find and reopen it:
12+
13+
```bash
14+
plannotator sessions
15+
```
16+
17+
This lists all active sessions with their mode, project, URL, and how long they've been running:
18+
19+
```
20+
Active Plannotator sessions:
21+
22+
#1 review my-project http://localhost:54321 3m ago
23+
#2 plan my-project http://localhost:12345 15m ago
24+
25+
Reopen with: plannotator sessions --open [N]
26+
```
27+
28+
To reopen one:
29+
30+
```bash
31+
plannotator sessions --open # reopens the most recent
32+
plannotator sessions --open 2 # reopens session #2
33+
```
34+
35+
Stale sessions from crashed processes are cleaned up automatically. You can also force cleanup with `plannotator sessions --clean`.
36+
37+
## Where does Plannotator store data?
38+
39+
All local data lives under `~/.plannotator/`:
40+
41+
| Directory | What's in it |
42+
|-----------|-------------|
43+
| `plans/` | Snapshots of approved and denied plans. Controlled by the "Save plans" toggle in Settings. |
44+
| `history/` | Automatic version history for every plan, organized by project and heading. Powers the plan diff and version browser. |
45+
| `drafts/` | Auto-saved annotation drafts. If a server crashes mid-review, your in-progress annotations are recovered on the next session. |
46+
| `sessions/` | Temporary session files for active servers. Cleaned up automatically when a server exits. |
47+
48+
Plan saving is enabled by default. You can change the save directory or disable it entirely in the Plannotator UI settings (gear icon).
49+
50+
## Browser doesn't open
51+
52+
If the UI doesn't open automatically, check:
53+
54+
- **Remote/SSH session?** Set `PLANNOTATOR_REMOTE=1` and `PLANNOTATOR_PORT` to a port you'll forward. See the [remote guide](/docs/guides/remote-and-devcontainers/).
55+
- **Wrong browser?** Set `PLANNOTATOR_BROWSER` to the app name or path, or use `--browser` for a one-off override.
56+
- **URL still works** — even if the browser didn't open, the server is running. Check `plannotator sessions` for the URL and open it manually.
57+
58+
## Hook doesn't fire
59+
60+
If `ExitPlanMode` doesn't trigger Plannotator:
61+
62+
1. Make sure the plugin is installed: `/plugin install plannotator@plannotator`
63+
2. Restart Claude Code after installing (hooks load on startup)
64+
3. Verify `plannotator` is on your PATH: `which plannotator`
65+
4. Check that plan mode is enabled in your Claude Code session

apps/marketing/src/content/docs/reference/environment-variables.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ All Plannotator environment variables and their defaults.
1414
|----------|---------|-------------|
1515
| `PLANNOTATOR_REMOTE` | auto-detect | Set to `1` or `true` to force remote mode. Uses fixed port and skips browser auto-open. |
1616
| `PLANNOTATOR_PORT` | random (local) / `19432` (remote) | Fixed server port. When not set, local sessions use a random port; remote sessions default to `19432`. |
17-
| `PLANNOTATOR_BROWSER` | system default | Custom browser to open the UI in. macOS: app name or path. Linux/Windows: executable path. Can also be a script. Takes priority over `BROWSER`. |
17+
| `PLANNOTATOR_BROWSER` | system default | Custom browser to open the UI in. macOS: app name or path. Linux/Windows: executable path. Can also be a script. Takes priority over `BROWSER`. Also settable per-invocation with `--browser`. |
1818
| `BROWSER` | (none) | Standard env var for specifying a browser. VS Code sets this automatically in devcontainers. Used as fallback when `PLANNOTATOR_BROWSER` is not set. |
1919
| `PLANNOTATOR_SHARE` | enabled | Set to `disabled` to turn off sharing. Hides share UI and import options. |
2020
| `PLANNOTATOR_SHARE_URL` | `https://share.plannotator.ai` | Base URL for share links. Set this when self-hosting the share portal. |

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.

packages/server/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"./git": "./git.ts",
1616
"./repo": "./repo.ts",
1717
"./share-url": "./share-url.ts",
18-
"./resolve-file": "./resolve-file.ts"
18+
"./resolve-file": "./resolve-file.ts",
19+
"./sessions": "./sessions.ts",
20+
"./project": "./project.ts"
1921
},
2022
"files": [
2123
"*.ts"

0 commit comments

Comments
 (0)