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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ claude --plugin-dir ./apps/hook
| `PLANNOTATOR_ANNOTATE_HISTORY` | Set to `0` / `false` to disable per-file version history in annotate mode (no copies of annotated files are written to the data dir; the annotate version diff is unavailable). Default: enabled. Can also be set via `~/.plannotator/config.json` (`{ "annotateHistory": false }`); the env var takes precedence. |
| `PLANNOTATOR_GUIDE_HISTORY` | Set to `0` / `false` to disable persisting successful Guided Reviews (no guide copies are written to the data dir; the "Previous guides" list is then never populated, though already-saved guides remain readable and listed). Default: enabled. Can also be set via `~/.plannotator/config.json` (`{ "guideHistory": false }`); the env var takes precedence. |
| `PLANNOTATOR_CURSOR_SANDBOX` | Set to `0` / `false` / `disabled` to stop passing `--sandbox enabled` when launching Cursor's `agent` CLI for review jobs — the flag pair is omitted entirely, deferring to the user's own Cursor Agent sandbox configuration. For systems where Cursor's sandbox cannot start (NixOS, AppArmor-restricted Linux). Default: enabled (`--sandbox enabled` is passed). Can also be set via `~/.plannotator/config.json` (`{ "cursorSandbox": false }`); the env var takes precedence. Note: opting out means the review job's write protection relies on `--mode ask` plus the user's own Cursor configuration. |
| `PLANNOTATOR_TODO_PROVIDER` | Set to `off` / `0` / `false` / `disabled` to stop mirroring the approved plan checklist into an editable todo provider during execution. Default: enabled, which syncs only when a provider is detected (currently pi-todos: detected when its todo directory exists — `<cwd>/.pi/todos` by default, or wherever `PI_TODO_PATH` redirects it when set). The mirror is additive — the progress widget is unaffected either way — and sync is one-way, so provider-side edits never feed back into plan execution. Can also be set via `~/.plannotator/config.json` (`{ "todoProvider": "off" }`); the env var takes precedence. |
| `JINA_API_KEY` | Optional Jina Reader API key for higher rate limits (500 RPM vs 20 RPM unauthenticated). Free keys include 10M tokens. |
| `PLANNOTATOR_DATA_DIR` | Override the base data directory. Supports `~` expansion. Default: `~/.plannotator`. When unset, an existing `~/.plannotator` always wins; if it doesn't exist and `$XDG_DATA_HOME` is set to an absolute path, `$XDG_DATA_HOME/plannotator` is used; otherwise `~/.plannotator` (the XDG spec's implicit `~/.local/share` default is deliberately not applied). All data (plans, history, drafts, config, hooks, sessions, debug logs, IPC registry) is stored under this directory. |
| `PLANNOTATOR_FILE_BROWSER_MAX_FILES` | File-discovery limit: regular files inspected by CLI markdown/folder resolution and startup code-file warming, supported files returned by the file browser, and directories scanned during multi-repo workspace discovery (symlinks may point outside the workspace, so the budget — not the root — bounds that walk). Must be a positive integer; invalid, zero, or negative values use the default of `5000`. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ All Plannotator environment variables and their defaults.
| `PLANNOTATOR_SHARE_URL` | `https://share.plannotator.ai` | Base URL for share links. Set this when self-hosting the share portal. |
| `PLANNOTATOR_DATA_DIR` | `~/.plannotator` | Override the base data directory. Supports `~` expansion. All data (plans, history, drafts, config, hooks, sessions) is stored under this directory.* When unset, an existing `~/.plannotator` is always used; if it doesn't exist and `$XDG_DATA_HOME` is set to an absolute path, `$XDG_DATA_HOME/plannotator` is used; otherwise `~/.plannotator`. (The XDG spec's implicit `~/.local/share` default is deliberately not applied — only an explicitly-set `$XDG_DATA_HOME` moves the directory.) |
| `PLANNOTATOR_PLAN_TIMEOUT_SECONDS` | `345600` | OpenCode only. `submit_plan` wait timeout in seconds. Set `0` to disable timeout. |
| `PLANNOTATOR_TODO_PROVIDER` | auto | Pi/oh-my-pi only. Set to `off` (or `0` / `false` / `disabled`) to stop mirroring the approved plan checklist into an editable todo provider during execution. When enabled, Plannotator syncs the checklist only if a provider is detected — currently [pi-todos](https://github.com/mitsuhiko/agent-stuff), detected by its todo directory existing (`.pi/todos` by default, or wherever `PI_TODO_PATH` redirects it when set). The mirror is additive: the progress widget behaves the same either way, and sync is one-way, so edits made in `/todos` never feed back into plan execution. Can also be set via `~/.plannotator/config.json` (`{ "todoProvider": "off" }`); the env var takes precedence. |

\* If you use the VS Code extension, make sure `PLANNOTATOR_DATA_DIR` is visible to both your terminal and VS Code. On macOS, apps launched from the Dock don't inherit shell env vars — launch VS Code from the terminal (`code .`) or set the variable via `launchctl setenv`.

Expand Down
53 changes: 52 additions & 1 deletion apps/pi-extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/

import { existsSync, readFileSync, statSync } from "node:fs";
import { basename, resolve } from "node:path";
import { basename, relative, resolve } from "node:path";
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
import { Type } from "@earendil-works/pi-ai";
import type {
Expand Down Expand Up @@ -46,6 +46,7 @@ import {
type PlannotatorPlanApprovedEvent,
registerPlannotatorEventListeners,
} from "./plannotator-events.ts";
import { resolveTodoProvider, type TodoProvider } from "./todo-providers/index.ts";
import {
findAssistantMessageByEntryId,
getAssistantMessageText,
Expand Down Expand Up @@ -263,6 +264,10 @@ export default function plannotator(pi: ExtensionAPI): void {
let phaseAddedTools: string[] = [];
let plannotatorConfig = {};
let justApprovedPlan = false;
/** Resolved once per execution phase; undefined means widget-only. */
let todoProvider: TodoProvider | undefined;
/** Latch: no provider found, or one sync failed. Cleared on return to idle. */
let todoProviderDisabled = false;

pi.on("session_start", (_event, ctx) => {
currentPiSession.update(ctx);
Expand Down Expand Up @@ -323,6 +328,46 @@ export default function plannotator(pi: ExtensionAPI): void {
}
}

/**
* Mirror the checklist into an editable todo provider, when one is present.
*
* Additive by design: the progress widget above stays exactly as it was.
* pi-todos renders its list on demand in `/todos` and has no live surface,
* so replacing the widget with it would trade a visible tracker for files
* behind a keystroke. Failures are swallowed after one notification —
* a todo mirror must never break plan execution. Runs even when the
* checklist is empty so a resubmitted-empty plan still reconciles
* (closing todos it used to own) instead of leaving them orphaned.
*/
async function syncTodoProvider(ctx: ExtensionContext): Promise<void> {
if (todoProviderDisabled) return;
if (phase !== "executing" || !lastSubmittedPath) return;
if (!todoProvider) {
todoProvider = resolveTodoProvider(loadConfig(), {
cwd: ctx.cwd,
sessionId: ctx.sessionManager.getSessionId(),
});
if (!todoProvider) {
todoProviderDisabled = true;
return;
}
}
// Tag on the cwd-relative path: it is stable across machines and reads
// cleanly in the /todos detail view, which renders raw tags.
const planId = relative(ctx.cwd, resolve(ctx.cwd, lastSubmittedPath)) || lastSubmittedPath;
try {
await todoProvider.sync(checklistItems, planId);
} catch (error) {
todoProviderDisabled = true;
ctx.ui.notify(
`Plannotator: ${todoProvider.name} sync failed, continuing with the progress widget only. ${
error instanceof Error ? error.message : String(error)
}`,
"warning",
);
}
}

function captureSavedState(ctx: ExtensionContext): void {
savedState = {
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
Expand Down Expand Up @@ -414,6 +459,7 @@ export default function plannotator(pi: ExtensionAPI): void {

updateStatus(ctx);
updateWidget(ctx);
await syncTodoProvider(ctx);
}

async function enterPlanning(ctx: ExtensionContext): Promise<void> {
Expand Down Expand Up @@ -441,6 +487,10 @@ export default function plannotator(pi: ExtensionAPI): void {
phase = "idle";
checklistItems = [];
lastSubmittedPath = null;
// Re-detect for the next plan: a provider that appeared (or a transient
// write failure) should not be decided once for the whole session.
todoProvider = undefined;
todoProviderDisabled = false;

releaseAddedPhaseTools();
await restoreSavedState(ctx);
Expand Down Expand Up @@ -1294,6 +1344,7 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
if (markCompletedSteps(text, checklistItems) > 0) {
updateStatus(ctx);
updateWidget(ctx);
await syncTodoProvider(ctx);
}
persistState();
});
Expand Down
1 change: 1 addition & 0 deletions apps/pi-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"config.ts",
"plannotator.json",
"server/",
"todo-providers/",
"generated/",
"README.md",
"plannotator.html",
Expand Down
3 changes: 2 additions & 1 deletion apps/pi-extension/startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ describe("Pi extension startup boundary", () => {
expect(browser.startMarkdownAnnotationSession).toBeFunction();
});

test("ships the lazy runtime in the npm package", () => {
test("ships the lazy runtime and todo providers in the npm package", () => {
const manifest = JSON.parse(
readFileSync(join(extensionDirectory, "package.json"), "utf-8"),
) as { files?: unknown };

expect(Array.isArray(manifest.files)).toBe(true);
expect(manifest.files).toContain("plannotator-browser-runtime.ts");
expect(manifest.files).toContain("todo-providers/");
});
});
Loading