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
3 changes: 1 addition & 2 deletions apps/hook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ If you prefer not to use the plugin system, add this to your `~/.claude/settings
{
"type": "command",
"command": "plannotator",
"timeout": 1800
"timeout": 345600
}
]
}
Expand Down Expand Up @@ -133,4 +133,3 @@ tags: [plan, authentication, typescript, sql]
```

<img width="1190" height="730" alt="image" src="https://github.com/user-attachments/assets/1f0876a0-8ace-4bcf-b0d6-4bbb07613b25" />

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The hook is defined in `hooks.json` inside the plugin directory. When installed
{
"type": "command",
"command": "plannotator",
"timeout": 1800
"timeout": 345600
}
]
}
Expand All @@ -44,7 +44,7 @@ The hook is defined in `hooks.json` inside the plugin directory. When installed
}
```

The `matcher` targets the `ExitPlanMode` tool specifically. The `timeout` is in seconds (30 minutes) — plan reviews can take a while.
The `matcher` targets the `ExitPlanMode` tool specifically. The `timeout` is in seconds (`345600` = 96 hours) — long reviews can stay open without expiring.

## Plugin configuration (OpenCode)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ If you prefer not to use the plugin system, add this to your `~/.claude/settings
{
"type": "command",
"command": "plannotator",
"timeout": 1800
"timeout": 345600
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ All Plannotator environment variables and their defaults.
| `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. |
| `PLANNOTATOR_SHARE` | enabled | Set to `disabled` to turn off sharing. Hides share UI and import options. |
| `PLANNOTATOR_SHARE_URL` | `https://share.plannotator.ai` | Base URL for share links. Set this when self-hosting the share portal. |
| `PLANNOTATOR_PLAN_TIMEOUT_SECONDS` | `345600` | OpenCode only. `submit_plan` wait timeout in seconds. Set `0` to disable timeout. |

## Paste service variables

Expand Down
1 change: 1 addition & 0 deletions apps/opencode-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Restart OpenCode. The `submit_plan` tool is now available.
| `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_SHARE_URL` | Custom share portal URL for self-hosting. Default: `https://share.plannotator.ai`. |
| `PLANNOTATOR_PLAN_TIMEOUT_SECONDS` | Timeout for `submit_plan` review wait. Default: `345600` (96h). Set `0` to disable timeout. |

## Devcontainer / Docker

Expand Down
56 changes: 45 additions & 11 deletions apps/opencode-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Environment variables:
* PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode (devcontainer, SSH)
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
* PLANNOTATOR_PLAN_TIMEOUT_SECONDS - Max wait for submit_plan approval (default: 345600, set 0 to disable)
*
* @packageDocumentation
*/
Expand Down Expand Up @@ -36,6 +37,8 @@ const htmlContent = indexHtml as unknown as string;
import reviewHtml from "./review-editor.html" with { type: "text" };
const reviewHtmlContent = reviewHtml as unknown as string;

const DEFAULT_PLAN_TIMEOUT_SECONDS = 345_600; // 96 hours

export const PlannotatorPlugin: Plugin = async (ctx) => {
// Helper to determine if sharing is enabled (lazy evaluation)
// Priority: OpenCode config > env var > default (enabled)
Expand All @@ -60,6 +63,28 @@ export const PlannotatorPlugin: Plugin = async (ctx) => {
return process.env.PLANNOTATOR_SHARE_URL || undefined;
}

/**
* submit_plan wait timeout in seconds.
* - unset: default to 96h
* - 0: disable timeout
* - invalid/negative: fall back to default
*/
function getPlanTimeoutSeconds(): number | null {
const raw = process.env.PLANNOTATOR_PLAN_TIMEOUT_SECONDS?.trim();
if (!raw) return DEFAULT_PLAN_TIMEOUT_SECONDS;

const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed) || parsed < 0) {
console.error(
`[Plannotator] Invalid PLANNOTATOR_PLAN_TIMEOUT_SECONDS="${raw}". Using default ${DEFAULT_PLAN_TIMEOUT_SECONDS}s.`
);
return DEFAULT_PLAN_TIMEOUT_SECONDS;
}

if (parsed === 0) return null;
return parsed;
}

return {
// Register submit_plan as primary-only tool (hidden from sub-agents)
config: async (opencodeConfig) => {
Expand Down Expand Up @@ -321,17 +346,26 @@ Do NOT proceed with implementation until your plan is approved.
},
});

const PLANNOTATOR_TIMEOUT_MS = 10 * 60 * 1000; // 10min timeout
let timeoutId: ReturnType<typeof setTimeout>;
const result = await Promise.race([
server.waitForDecision().then((r) => { clearTimeout(timeoutId); return r; }),
new Promise<{ approved: boolean; feedback?: string }>((resolve) => {
timeoutId = setTimeout(
() => resolve({ approved: false, feedback: "[Plannotator] No response within 10 minutes. Port released automatically. Please call submit_plan again." }),
PLANNOTATOR_TIMEOUT_MS
);
}),
]);
const timeoutSeconds = getPlanTimeoutSeconds();
const timeoutMs = timeoutSeconds === null ? null : timeoutSeconds * 1000;

const result = timeoutMs === null
? await server.waitForDecision()
: await new Promise<Awaited<ReturnType<typeof server.waitForDecision>>>((resolve) => {
const timeoutId = setTimeout(
() =>
resolve({
approved: false,
feedback: `[Plannotator] No response within ${timeoutSeconds} seconds. Port released automatically. Please call submit_plan again.`,
}),
timeoutMs
);

server.waitForDecision().then((r) => {
clearTimeout(timeoutId);
resolve(r);
});
});
await Bun.sleep(1500);
server.stop();

Expand Down