* fix(ai): drive Codex Ask AI via app-server + answer-first review prompts (#971)
Codex Ask AI previously ran via @openai/codex-sdk (codex exec), which forces
approval_policy=never and breaks in enterprise-managed Codex environments that
ban it (#971). Replace the transport with a long-lived 'codex app-server'
process over JSON-RPC.
- New provider packages/ai/providers/codex-app-server.ts (registered as
'codex-sdk' to preserve cookie/agents.ts/UI-gate); omits approvalPolicy so
Codex resolves the user's + managed policy, pins read-only sandbox, and
surfaces interactive approvals through the existing PermissionCard.
- Delete codex-sdk.ts and drop the @openai/codex-sdk dependency (and its 6
prebuilt platform binaries); gate registration on 'which codex'.
- SessionManager: additive, optional dispose?() hook to kill the spawned
process on evict/remove — a no-op for Claude/OpenCode/Pi (they don't
implement it).
Also rework the Ask AI prompts (all providers, separate from the transport):
- Every mode now instructs the agent to answer the user's message directly and
not launch an unprompted review of the context.
- Code review stops pasting the whole diff for git-reproducible diff types and
instead tells the agent how to inspect it (git diff <base>..HEAD, three-dot
for merge-base); non-git/PR/workspace types still paste.
- Claude gains the Bash tool so it can run git (still gated by approvals).
- The UI passes diffType/base (session) and what the user is viewing (per
question) into the context.
Verified: full typecheck, full test suite (101 ai tests), and a live
end-to-end smoke against codex app-server.
* fix(review): pin AI approval card above the input/model bar
Render pending approval cards just above the input + provider/model bar in both the document chat (DocumentAIChatPanel) and code-review AI tab, instead of at the top of the scroll, so the user sees them where they act.
* fix(ai): harden Codex abort/cancel + add Ask AI Stop button
Addresses code-review findings on the codex app-server provider:
- turn/interrupt was sent as a notification (no id) so Codex ignored it and
abort never took effect. It's now a proper JSON-RPC request.
- Filter turn events/approvals by turnId and reject an aborted turn's
stragglers, so a stopped turn can no longer leak output into — or
prematurely finish — the next turn (ask-stop-ask race).
- Guard listeners by query generation and end the drain loop on the abort
signal, so a superseded/stopped turn can't touch the live one and abort
returns promptly instead of waiting for turn/completed.
- Handle abort during startup: once the turn id is known, interrupt it
instead of running it in the background.
- Add a sendAndWait timeout so a stalled (alive-but-unresponsive) process
errors instead of hanging forever.
- Drain stderr (stdio 'ignore') to avoid a pipe-buffer deadlock.
Also add a Stop button to both Ask AI surfaces (plan/annotate
DocumentAIChatPanel and code-review AITab via ReviewSidebar). It replaces
Send while streaming and calls the hook's abort -> /api/ai/abort ->
session.abort(); the hook already exposed abort but nothing surfaced it.
* feat(ai): drive Codex models + reasoning levels from model/list
- Codex provider fetches model/list at startup (throwaway app-server, like
Pi/OpenCode) and populates the real models plus each model's actual
supportedReasoningEfforts + defaultReasoningEffort. Replaces the hardcoded
model list and the static AI_REASONING_EFFORTS (which mislabeled xhigh as
'Max' and omitted minimal).
- AIProviderBar + AIConfigBar now show the selected model's real efforts and
hide the control when a model reports none. xhigh is shown verbatim.
- Fix: the Stop button now also appears in the populated code-review chat
state (a prior edit missed the second GeneralInput due to indentation).
* fix(ai): scope Claude Ask AI Bash to read-only git; clear stale approval cards on Stop
- Claude Ask AI no longer auto-allows bare Bash (which ran arbitrary shell
with no Allow/Deny prompt). Replace it with scoped read-only git rules
(Bash(git diff:*), show, log, status, rev-parse, merge-base, ls-files).
git reads auto-run so the agent can inspect large diffs itself; any other
command (write git, arbitrary, or injected compound) falls through to the
permission card. Keeps the git-inspect approach (large diffs don't fit in
the prompt) while closing the auto-exec hole.
- useAIChat.abort() now drops still-undecided permission cards: abort cancels
them server-side, so leaving them visible was a dead Allow/Deny.
* feat(ai): code-review Ask AI shares the agent-review prompt machine, delivered as user messages
Code-review Ask AI built its own diff description (gitInspectInstruction) in
the system prompt from just diffType+base, which was wrong for full-stack,
hide-whitespace, untracked files, and PR worktrees. Replace it with the same
machine the review jobs use, delivered on the user's messages.
- Server: buildCurrentAiReviewContext() reuses buildAgentReviewUserMessageForTarget
(contextOnly) for the current view and ships it as aiReviewContext in every
diff payload (/api/diff + switch/PR/scope). Mirrored in the Pi server.
- Client: review-editor latches aiReviewContext onto each question via the pure
buildReviewContextPreamble (packages/ui/utils/aiPrompt.ts) and buildDefaultPrompt
— full block on the first message / when the view changes (incl. after a
provider switch via the !sessionId fresh-session check), a short reminder
otherwise (never re-pastes a large diff).
- context.ts: delete the duplicate gitInspectInstruction; code-review system
prompt is now role-only. Providers untouched (provider-agnostic user message).
- Tests: machine scenario gaps (plain PR, full-stack default, PR-worktree
origin/<base> + stale-main warning, untracked mention, jj-evolog, workspace
lines); composition (first/reminder, command/pasted, preamble ordering).
* fix(ai): agentic remote-PR context, UTF-8 stream decode, real Stop on supersede/disconnect
Addresses review findings on the Ask AI prompt work:
- Remote PR without a confirmed local checkout no longer gets URL-only. The
agent is told it's in a PR worktree that's being prepared, to verify the PR
files exist before relying on them, and to diff with git diff origin/<base>...
HEAD (URL fallback). Inform + trust the agent rather than pasting. Shared
machine, so review jobs get the same framing.
- Decode Codex stdout with a streaming TextDecoder instead of per-chunk
toString(), so multi-byte UTF-8 split across chunks no longer corrupts into
U+FFFD (matches the Pi provider).
- Stop now actually stops the server turn: ask() awaits /api/ai/abort when a
new question supersedes a streaming one (awaiting avoids racing the new query
into session_busy), and the /api/ai/query SSE stream gains a cancel handler
so tab-close/navigation aborts the turn too. Both reuse the existing
per-provider session.abort(); factored a shared postServerAbort helper.
* fix(ai): per-model reasoning effort, shared PR-checkout readiness, Stop-then-ask race, type hole
- Reasoning effort is now tracked per model (a map keyed by model) instead of
one global value, so switching to a model that doesn't support the prior
level (e.g. xhigh) no longer posts a stale/unsupported effort that Codex
rejects. Each model keeps its own level; nothing leaks across.
- Extract resolvePoolCwd into packages/shared/worktree-pool.ts (ready/pending/
absent) and use it from both servers' resolvePRLocalCwd so the readiness rule
can't drift. Fix the Pi Ask AI helper to ready-check like Bun, so a warming
PR checkout no longer claims 'checked out at PR head' and misdirects the diff.
- Stop-then-ask no longer races into session_busy: the abort promise (from Stop
or a superseding question) is stashed and the next ask() awaits it before
sending. Stop still kills the turn instantly; a follow-up just waits the one
abort round-trip.
- Declare aiReviewContext on the initial /api/diff response type.
* fix(ai): surface real Codex error messages, skip transient retries, auto-deny permission escalations
Validated against codex-rs:
- Codex's ErrorNotification nests the text at params.error.message
(TurnError.message); we read params.message → always 'Unknown error'. Read
the nested field (top-level fallback) so auth/usage-limit/stream failures show
their real, actionable text.
- Skip transient error notifications (willRetry=true): Codex retries on its own
and the turn continues, so surfacing them flashed a spurious failure before
the real answer.
- Handle item/permissions/requestApproval: respond {permissions:{}, scope:'turn'}
— byte-for-byte Codex's own cancel response (codex_delegate.rs) — instead of
'Unsupported request'. Ask AI is read-only, so denying the escalation lets the
turn continue sandboxed rather than failing. Interactive grant deferred.
* refactor(ai): share AI provider/model config in one hook; aggregate paginated model/list
- Extract useAIProviderConfig (packages/ui/hooks): one home for provider/model/
reasoning-effort selection — initial state, auto-resolve on capabilities load,
per-model effort (no leak across models), and persistence. Both the plan and
code-review apps now call it and only compose the session reset (the hook can't
own reset without a cycle through useAIChat). Plan editor adapted to the
effect-based resolve (adds aiDefaultProvider state). This fixes the plan
editor's stale-effort-on-model-switch bug by construction and stops the two
apps' copies from drifting again.
- fetchModels now follows model/list's nextCursor and aggregates every page into
one list, so larger model catalogs aren't silently truncated (with a page
guard against a misbehaving cursor).
Skipped per discussion: legacy v1 approval methods (we're a v2 client).
* feat(agents): per-agent review default; bound Codex model discovery so it can't stall the AI panel
- The selected review profile is now tracked per review engine (claude/codex/
cursor/opencode) instead of one flat global value, so each agent keeps its own
review default. Public hook API (reviewProfileId/setReviewProfileId) is
unchanged — getter derives the current engine's value, setter writes it — so
AgentsTab needs no changes. One-shot migration seeds every engine with any
existing flat pick. Adds parseReviewProfileByEngine + tests.
- fetchModels (Codex model discovery) now uses a short 6s timeout for its
initialize + model/list RPCs instead of the 30s default. /api/ai/capabilities
awaits model discovery, so an installed-but-unauthenticated codex could
otherwise block the AI panel for ~30s; it now falls back to the static model
list fast. Authed codex is unaffected (discovery completes in tens of ms).
* fix(cli): print per-subcommand help instead of launching the UI
`plannotator review --help` (and other subcommands) fell through to their
command branch because only top-level `--help` was handled. For `review`,
`--help` was parsed as a non-URL positional, dropping into local review
mode and opening a browser tab. When Claude Code probes the CLI with
`--help`, that stray tab's close injects a bogus "no feedback → proceed"
signal into the session.
Handle `--help`/`-h` for every user-facing subcommand (review, annotate,
annotate-last/last, setup-goal, archive, sessions) before any subcommand
branch can run: print command-specific usage on stdout and exit 0. Also
accept `-h` at the top level and advertise per-command help there.
Fixes#964
* fix(cli): handle `improve-context --help` too
The top-level help advertises `plannotator <command> --help`, but
`improve-context` (the only internal hook command listed there) had no
help entry, so `improve-context --help` fell through to the hook branch
and emitted additionalContext JSON instead of usage. Add a help entry so
every advertised command responds to --help.
The Plannotator app runs in a nested cross-origin iframe inside the VS Code
webview, where two input paths break:
- Clipboard: the iframe document never holds focus, so the async Clipboard
API is rejected ("Document is not focused") and native copy/cut/paste
events never fire. Copy, cut, and paste all silently fail.
- Keybindings: keydown events don't cross the iframe boundary, so VS Code
never sees global shortcuts (Cmd+P, etc.) while a Plannotator tab is focused.
Fix both off the one input signal the iframe still receives — keydown:
- Route clipboard reads/writes through the extension host, which owns the
system clipboard via vscode.env.clipboard. The injected script overrides
navigator.clipboard and handles Cmd+C/X/V, relaying through the wrapper to
an onDidReceiveMessage handler and back.
- Forward all other keystrokes to the wrapper, which re-dispatches a synthetic
KeyboardEvent on the webview document so VS Code can resolve its keybindings.
Undo/redo/select-all stay native.
Fixes#864Fixes#969
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ai-code-review.md Codex example showed --sandbox workspace-write, but the
runtime (codex-review.ts) uses --full-auto. Align the doc with the actual
command.
Custom reviews: pick an Agent Skill and its SKILL.md body becomes the review prompt (read live from global skill folders, never copied; default review byte-identical). Agent findings can be a line, a whole file, or a general review-level comment, and are never silently dropped. Plus: agent-icon engine picker, icon-only file-header opener, semantic diff restored as its own dock panel, and a dead-code sweep.
Surface "Everything the plugin does" immediately after the hero/social
proof so visitors see the full feature set before scrolling to demos.
Reorder commands to lead with annotate and review, and clarify the
review description.
Adds a server-side "open the current file in an external app" control to the code-review and annotate surfaces (split-button with host-detected apps; last-used becomes the default; cross-platform launch mirrored across the Bun and Pi runtimes), plus a code-review UX pass: file-header change letters and line counts, the diff-settings cog grouped with Split/Unified, an all-files collapse/expand-all toggle, and the semantic diff moved to a resizable sidebar accordion. Also fixes Cmd+click code navigation (worker token transformer).
Hardened over three adversarial review rounds: shared, tested path-containment for open-in (Bun + Pi), annotate scoping aligned to /api/doc reference roots, strict PR-checkout rooting (never the launch repo), deduped app-catalog and semantic-diff fetches, macOS bundle-only availability, Windows launch fixes (reveal + terminal off cmd's parser), and PR-checkout tracking across switch and pool warmup.
* feat(annotate): add live file tree workspace status
* fix(annotate): guard dirty feedback and rename stats
* fix(annotate): tighten live file tree status
* fix(annotate): surface deleted file browser roots
* fix(annotate): normalize file tree status paths
* fix(annotate): avoid optional git locks for workspace status
* fix(annotate): tighten workspace status git metadata
* fix(annotate): refresh file tree after reconnect
* fix(pi): expose file browser stream route
* fix(server): always show remote URL + close share-html symlink gap
Two pre-release fixes found by the QA sweep.
1. Remote URL stranding: handleServerReady only opened a browser (silent on
a headless box) and wrote a ready-file; the user-visible URL came solely
from writeRemoteShareLink, gated on sharingEnabled. So a remote/SSH user
with sharing disabled (PLANNOTATOR_SHARE=disabled or config.json
{share:"disabled"}, widened by #921) saw no URL and the agent hung on
waitForDecision() forever. Now handleServerReady prints the reachable
localhost URL whenever the session is remote, independent of sharing; the
share link stays an extra. Pi/OpenCode already printed the URL
unconditionally and are unaffected.
2. share-html symlink gap: the #927 symlink-containment fix hardened the
/api/html-assets asset sinks but missed packages/server/annotate.ts's
/api/share-html containment, which stayed lexical. A symlinked *.html
inside the doc directory pointing outside it leaked the target's contents
into the share payload. Now realpath-resolves both root and target like
the other sinks. Bun-only — Pi's copy was already hardened.
Adds regression tests for both (handleServerReady remote stderr; share-html
returns 403 on symlink escape).
* refactor(server): single shared isWithinDirectory for all asset/share sinks
The symlink-containment check existed as four byte-identical copies (Bun
html-assets route, share inliner, annotate /api/share-html, Pi server). That
duplication is exactly why the escape was missed in one sink before — #927
hardened three and missed the fourth, and the #929 fix added a fifth-in-waiting.
Export the single canonical isWithinDirectory from the shared (Pi-vendored)
html-assets-node module and have every sink import it; delete the three
duplicate bodies and their now-dead realpathSync/relative/isAbsolute imports.
A new sink can no longer silently diverge. Regression tests + 690 server/shared
tests pass; build:pi clean.
* fix(pi): surface the session URL in the in-turn 'opened' notice for remote
Remote Pi users never saw a review/annotate/last URL: it was emitted from
openBrowserForServer AFTER the command's turn ended (fire-and-forget), and
Pi only renders a notify during an active turn — so it silently dropped.
The 'X opened. You can keep chatting' notice fires in-turn and DOES show,
so fold the URL into it for remote sessions (single-line, matching the
notify convention). Local sessions are unchanged (browser auto-opens).
* fix(annotate): add /api/save-notes POST endpoint to both servers
Copies the save-notes route from the plan review server into the
annotate server (Bun source and Pi extension copy), enabling Save to
Obsidian in annotation mode.
Fixes#844
* test(annotate): add saveToObsidian unit tests and HTTP endpoint tests
Verifies saveToObsidian writes files correctly and handles missing
vaults. HTTP endpoint tests cover success, empty integrations, and
integration-level error (not 500).
Imports consolidation from ./integrations into a single statement.
* fix(annotate): normalize server port fallback
* refactor(server): extract shared handleSaveNotes handler, fix catch-block bug
Move the /api/save-notes logic into shared handler modules
(shared-handlers.ts for Bun, handlers.ts for Pi) following the existing
pattern for handleImage, handleUpload, handleDraftSave. Replaces four
inline copies with two canonical implementations.
Fixes:
- Bun annotate catch block now correctly returns 500 (was logging only)
- Misindented brace in Pi serverAnnotate.ts resolved by extraction
- Revert unrelated port fallback change (keep server.port! for consistency)
- Static imports in integrations.test.ts
- Add /api/save-notes to CLAUDE.md Annotate Server API table
* fix(opencode): inject annotate server starter instead of global mock.module
commands.test.ts mocked the annotate server with
`mock.module("@plannotator/server/annotate", ...)`. Bun module mocks are
process-global and cannot be unset (oven-sh/bun#7823, #12823), so the stub
leaked into every suite that runs after it — in particular any test that boots
the real annotate server received a stub with no `.url`.
Make `startAnnotateServer` injectable through the existing CommandDeps
(defaulting to the real import, so production is unchanged) and have the test
pass its stub that way. This keeps the fake local to the opencode suite and
unblocks real annotate-server integration tests.
* test(server): cover save-notes — handler unit tests + annotate e2e wiring
- shared-handlers.test.ts: unit-test handleSaveNotes directly (Obsidian write,
empty integrations, integration-error reported not thrown, 500 on bad body).
- annotate.test.ts: boot the real annotate server and POST /api/save-notes,
asserting it is served as JSON (not the SPA HTML catch-all) — the regression
guard for #844. Now possible because the opencode suite no longer installs a
global annotate module mock.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* fix(annotate): resolve symlinks before HTML asset containment check
The /api/html-assets route and the share-payload inliner checked path
containment lexically, so an in-directory symlink pointing outside the
HTML's folder (e.g. evil.css -> ~/.ssh/id_rsa) passed the check and was
served or base64-inlined into the share payload. In remote mode the
inliner auto-fires at startup, so this could upload symlinked local
files to the paste service with no user action.
Resolve symlinks with realpathSync on both the asset and the root before
the relative-path check, in all three runtime copies (Bun route handler,
shared node inliner, Pi route handler). Non-existent assets fall back to
the lexical path and 404 on read. Adds regression tests for both sinks.
* test(pi): build rich git state in sandbox-pi.sh for review diff modes
Expand the Pi sandbox harness to create multiple commits, a feature
branch, and a rename+delete+modify commit so /plannotator-review can
exercise every diff mode (uncommitted, staged, branch, merge-base).
* docs: fix broken verification link in READMEs
The READMEs pointed at a non-existent anchor
(installation/#verifying-your-install); the verification guide is a
standalone reference page. Point to /docs/reference/verifying-your-install/
(and split the hook README's link so version pinning -> installation,
verification -> the reference page).
Allow disabling URL sharing through ~/.plannotator/config.json
({ "share": "disabled" }) in addition to the PLANNOTATOR_SHARE
env var. Adds a resolveSharingEnabled() helper (env var > config >
default enabled) and routes all sharing checks through it across the
hook server, OpenCode plugin, and Pi extension. Docs updated.
* feat(annotate): render html files by default
* fix(annotate): support raw html assets and sharing
* fix(annotate): address html first review followups
* fix(editor): avoid raw html sidebar init crash
* fix(annotate): support portable html shares
* fix(annotate): harden html share support
* fix(share): clear attachments when loading shared payloads
* fix(share): warn on remote share link failures
* perf(annotate): lazy-build html share payloads
* test(annotate): guard lazy html share generation
* test(annotate): drop flaky html share server test
Two QA follow-ups found while validating the release:
- Route SIGINT/SIGTERM through process.exit() so the existing "exit"
handlers actually run on Ctrl-C / termination. A signal death
previously skipped them, leaking background PR-checkout warmup
children and stale `git worktree` registrations. A second signal
still force-quits if cleanup hangs.
- Append the review-denied triage suffix for PR-mode feedback, not just
local diffs. The old `!isPRMode` gate suppressed it for every PR
review. Gate on whether the reviewer actually sent annotations
instead: genuine feedback always carries annotations, while platform
PR actions (approve/comment posted to the host) return an empty
annotation set + status message and correctly get no suffix. Applied
consistently across hook, OpenCode, and Pi.
* feat(review): large GitHub PR fallback + non-blocking PR checkout
Two PR-mode improvements:
1. Large GitHub PRs no longer fail to load. When `gh pr diff` is refused
(HTTP 406 for oversized diffs), fetchGhPR pages through the pulls files
API and stitches the per-file patches into a unified diff — mirroring
the existing GitLab raw_diffs fallback. Path quoting matches git's
exact rules (bare spaces unquoted) so downstream parsers round-trip;
truncation at the API's 3000-file cap is surfaced, never silent.
2. The --local worktree/clone no longer blocks startup. The review server
opens as soon as the platform diff arrives; the checkout warms in the
background as a seeded not-ready pool entry. Consumers that need real
files (agent jobs, full-stack diff, code-nav, semantic diff, AI
sessions) await pool.ensure(), with creations serialized so concurrent
fetches can't clobber the shared FETCH_HEAD. Cross-repo clone steps
converted from spawnSync to async spawns; warmup children are killed
on exit (plus `git worktree prune`) so aborted sessions can't leak
stale registrations; failed checkouts degrade honestly (no agent runs
in the wrong directory claiming local access) with a 30s retry
cooldown.
* fix(review): survive long PR checkout warmups + classify reconstructed renames
Stress-testing against oven-sh/bun#30412 (2,188 files) surfaced three bugs:
- Bun.serve's default 10s idleTimeout killed /api/semantic-diff while it
parked on the background checkout warmup (a clone that can take minutes).
Disable the idle timeout on all servers — AI SSE streams can also stall
>10s between bytes while a permission prompt waits.
- The file-badge hook memoized that failed fetch in a module-level cache
keyed by patch, pinning every badge to empty until a hard refresh. Never
cache failures; retry with backoff (5s/15s/30s).
- reconstructGhPatch/reconstructPatch omitted the `similarity index` line,
which Pierre's parser keys rename classification off — pure renames
rendered as blank plain changes with no old path. Emit 100% for
patch-less renames/copies (exactly accurate) and a synthetic 99% for
patched ones (consumers only branch on 100% vs not).
* feat(review): local full-diff upgrade for PRs whose API diff is truncated
On oversized PRs the platform APIs withhold per-file patch content entirely
(bun#30412: 1,066 of 2,188 files came back with status added/modified, zeroed
counts, and no patch). Those files rendered as empty stubs with no diff.
- fetchGhPR/fetchGlMR flag the result `patchIncomplete` when patch-less
non-rename entries exist or the 3000-file cap truncates the listing.
- New runPRLayerLocalDiff (pr-stack.ts) recomputes the exact layer diff in
the local checkout: platform merge-base + head SHA two-dot diff (three-dot
vs baseSha fallback), fetch-by-SHA for objects missing from shallow clones,
-l0 so rename detection doesn't silently degrade on huge PRs.
- The review UI shows a "Partial diff · Load full diff" notice in layer
scope; clicking re-requests the layer scope and the server swaps in the
recomputed full diff (waiting out the background clone if needed).
- PR scope/switch state writes are epoch-guarded: a request parked on the
checkout warmup can no longer overwrite a newer scope select or pr-switch.
- draftKey follows the upgraded patch so annotation drafts survive pr-switch
round-trips; recompute failures surface in the response error field.
- Pi server mirrors all of it, including an agentCwd fallback so the upgrade
works for PRs switched-to under a cross-repo clone pool.
* fix(review): use GitLab's too_large/collapsed flags for withheld-diff detection
External review caught a false negative: a too-large ADDED file comes back
new_file:true with an empty diff — indistinguishable from a legitimately
empty new file under the old heuristic, so the partial-diff upgrade was
never offered for exactly the files that matter most on big MRs.
The REST /diffs endpoint marks withheld content explicitly per entry
(verified against gitlab.com): too_large/collapsed are now authoritative in
both directions — withheld adds/deletes are flagged, binaries and empty
files are never misflagged. Older GitLab without the fields keeps the
empty-diff-on-modification heuristic.
* feat(prompts): unify review-denied suffix — triage first, no coding off raw feedback
The per-runtime defaults map (#627) gave OpenCode and Pi a different
review-denied suffix than every other runtime; updating one meant the
others silently kept "you must address all of them" — an instruction to
start coding immediately. Claude Code, Amp, Droid, Codex, Copilot, Gemini,
and Kiro were all still on it.
One default for every runtime now: triage the feedback, verify it against
the code, discuss before changing anything. Per-runtime customization
remains available via config (prompts.review.runtimes.<rt>.denied), which
resolves above the built-in default as before.
* fix(prompts): generalize review-denied suffix — 'from review', not 'external AI reviewers'
Review feedback isn't always from AI reviewers or agent jobs; often it's
the human reviewer's own annotations. Neutral wording covers both.
* fix(review): non-blocking 'Load full diff' + flag-handling hardenings
Self-review findings:
- The partial-diff upgrade reused the scope-switch handler, so clicking
"Load full diff" raised the full-screen PRSwitchOverlay — blocking the
entire UI, potentially for minutes behind a cold clone, with no text and
no cancel. The upgrade now has its own loading state: the notice shows a
spinner ("Loading full diff…") and the reviewer keeps working with the
partial diff while the request parks. Server-side epoch guards already
handle scope/PR changes made during the wait.
- GitLab too_large/collapsed: treat explicit null like absent (flags
inconclusive → legacy heuristic decides) instead of silently exonerating.
- Rename-limit lift uses -l100000 instead of -l0 ("0 = unlimited" only
holds on git >= 2.29; on older git it could disable detection outright).
* fix(review): stop scroll-driven sem stampede when semantic diff is failing
The badge retry change (a2d19a4e) cleared the client-side sem cache on
failure so transient errors could recover. But file-header badges mount and
unmount on every scroll in the virtualized all-files view, and each mount
re-requests /api/semantic-diff — and the server only cached SUCCESSFUL runs.
With sem erroring, scrolling spawned a continuous stream of sem processes,
pegging the CPU and making scrolling severely choppy.
Bound retry rate by time, not by mount events:
- client: keep the failed result memoized and expire it after a 60s
cooldown instead of clearing immediately
- server (Bun + Pi): memoize failed sem runs for 30s in
SemanticDiffResponseCache — request rate can no longer drive execution
rate
* fix(review): eliminate all-files scroll jank (pre-existing on main, from #885)
The CodeView migration introduced severe scroll chop; scrolling UP could
freeze the viewport entirely ("scrolling but nothing changes"). Three
compounding causes, diagnosed against Pierre 1.2.8 source:
1. Lazy full-content augmentation landed updateItem() mid-scroll-gesture:
the full-content parse counts collapsed-context regions the raw-patch
parse doesn't, so the item GROWS — re-render + re-tokenize hitches both
directions, and when the grown item sat above CodeView's scroll anchor,
its corrective scrollTo() killed wheel momentum (the up-scroll freeze).
Fetches still start as items enter the window; the item mutation now
waits for 150ms of scroll quiet (staleness re-checked at apply time).
2. reportVisibleFile read container.scrollTop/clientHeight/scrollHeight on
EVERY scroll event — a forced synchronous layout right after each
frame's DOM writes. Replaced with CodeView's cached accessors and
coalesced the handler to once per animation frame.
3. Missing containment CSS: Pierre's own production wrapper uses
contain:strict + will-change:scroll-position so forced layouts stay
scoped to the scroller instead of the whole document. Adopted.
Also: __devOnlyValidateItemHeights now requires explicit opt-in
(VITE_PIERRE_VALIDATE_HEIGHTS=1) — it runs getBoundingClientRect() per
rendered item per frame and made dev-server scrolling choppy by itself.
* feat(review): change-type status in headers + tree, diffshub CSS parity
Adopts two diffshub practices identified in the architecture comparison:
- DiffFile now carries a derived status (added/deleted/renamed/modified)
from the chunk's git metadata lines. FileHeader shows a status icon and
renders renames as "old/path → new/path" (dimmed old, arrow — diffshub's
treatment, including its rename blue); the file tree shows A/D/R markers.
'modified' is deliberately undecorated so the others pop. Works in both
the all-files surface and the single-file panel, including header-only
pure renames from the large-PR reconstruction.
- CodeView container gains diffshub's remaining perf CSS: overflow-anchor:
none (native scroll anchoring fights CodeView's own anchor resolution
whenever item heights change — exactly our augmentation applies),
overflow-x-clip, and overflow-clip containment on item elements.
* feat(review): worker-pool syntax highlighting (diffshub parity)
A performance trace of scrolling a small local diff attributed 2.2s of
2.6s main-thread CPU to findNextMatchSync — shiki's TextMate regex
scanner tokenizing on the main thread. diffshub avoids this entirely by
running tokenization in Pierre's worker pool; we never opted in.
Wires WorkerPoolContextProvider around the review app (pool size
min(cores-1, 3), 100-entry AST LRU, common languages preloaded), gates
the all-files surface on pool readiness with a 5s escape hatch (a dead
pool degrades to plaintext-then-highlight, never a blank view), and
syncs the UI theme pair into the long-lived pool.
Single-file build constraint solved with Vite's ?worker&inline (base64
blob worker) + worker.format 'es' with inlineDynamicImports — the
worker's lazy import("shiki/wasm") branch collapses into the bundle and
is never taken (shiki-js engine: the win is moving work off the main
thread, with no .wasm asset to smuggle into one HTML file). Bundle
+850KB.
* fix(review): un-poison worker-pool theme dedup on failed setRenderOptions
A failed round-trip recorded the theme as synced and never retried,
pinning the pool to the wrong palette for the session.
* fix(review): report partial diffs without a checkout; fail fast on missing checkout
Dogfood review of this PR (via plannotator itself) caught two valid issues:
- prPatchIncomplete was gated on the worktree pool, so a --no-local session
showed a truncated diff with no indication at all. Partiality is
information; upgradability is a capability. The flag is now always
reported, with a separate prPatchUpgradeAvailable — the UI shows the
amber notice either way, with the "Load full diff" button only when a
checkout can exist (otherwise a "re-run with --local" hint).
- After a FAILED checkout warmup, Ask AI sessions and agent jobs fell back
to process.cwd() (or a wrong revision on Pi) — running in the wrong tree
instead of failing. Both launch points now refuse with a clear "Local
PR checkout unavailable — retry shortly" error (503); the job handlers
surface buildCommand refusals instead of mislabeling them "Invalid
JSON". Bun and Pi mirrored.
A third finding (sem availability stuck after warmup) was triaged invalid:
the availability probe detects the sem binary, which is cwd-independent.
* fix(review): runtime-neutral copy for the no-checkout partial-diff hint
--local is a CLI remedy; OpenCode sessions have no such flag. Visible
text states the fact, the tooltip carries the CLI guidance.
Replaces the all-files renderer with Pierre's virtualized CodeView (one scroller, identical look, scales to huge diffs); deletes the legacy AllFilesDiffView/LazyFileDiff and their flag; single-file panels stay on FileDiff. @pierre/diffs pinned exact 1.2.8 (1.2.9's tree is broken on npm — see #880).
Also ships: diff staleness detection ('Diff out of date · Refresh' toolbar notice backed by shared per-VCS fingerprints + GET /api/diff/fresh on both Bun and Pi servers), a stale-content guard so files edited mid-review degrade to raw-patch view instead of breaking virtualization, the Ask AI wrong-file fix (pre-existing), and toolbar/header UI polish (global settings cog, aligned sem badges, no all-files split dragger).
Hardened through five review waves; tested with 1384 passing tests incl. real-git fingerprint and patch-consistency suites. Known minor gaps documented in the PR.
- When a file exists but has an unsupported type (.cs, .ts, .py, etc.),
show 'File type not supported' instead of misleading 'File not found'
- Display supported file types (.md, .mdx, .html, .htm)
- Suggest using 'plannotator review' for code files
- Maintains backward compatibility for actual missing files
Fixes#757
Co-authored-by: ishowman <ishowman@users.noreply.github.com>
Upstream @pierre/diffs@1.2.x depends on @pierre/theming@0.0.1, which is
not published on npm, so any fresh install resolving our ^1.1.12 range
floated to 1.2.9 and failed with a 404. Our lockfile masked this for
repo builds, but npm consumers of @plannotator/pi-extension resolve
fresh and hit it (#880). Pin every declaration to exact 1.1.20, whose
dependency tree is fully resolvable (@pierre/theme@0.0.28).
- apps/hook/README.md: 'four' -> 'three' slash commands (archive row was
removed in #873 but the count wasn't reverted).
- LookAndFeelAnnouncementDialog: 'Full release notes' now deep-links to the
v0.20.0 release tag instead of the generic releases list.
* feat(plan): default to the grid look + look-and-feel image chooser
- Flip gridEnabled default back to true (classic grid / floating-card look),
reverting #863's flat-by-default so existing users without an explicit
choice return to the grid view they're used to.
- Rewrite the look-and-feel announcement into a two-image chooser: Grid
(classic, now the default) vs Clean (the new flat look), each a clickable
screenshot that hover-zooms to preview — mirroring the code-review
DiffTypeSetupDialog pattern.
- Bump the announcement version (1 -> 2) so existing users are re-offered the
choice once.
- Add look-grid.png / look-flat.png preview assets (resized app screenshots).
* fix(review): restore GitLab /diffs fallback for MR review
#871's pr-gitlab refactor moved to the raw_diffs endpoint and deleted the
paginated /diffs path, hard-breaking MR review on self-hosted GitLab too old to
expose raw_diffs (opaque "Failed to fetch MR diff") and silently rendering zero
files when raw_diffs returns empty for oversized MRs.
Keep raw_diffs as the primary path (it preserves binary markers + collapsed
content), but fall back to the restored paginated /diffs + reconstructPatch when
raw_diffs fails or is empty, and throw a clear "diff is empty / too large" error
if both come back empty. Restores parsePaginatedArray / reconstructPatch and
adds tests for the fallback paths.
* fix(install): bound the optional sem sidecar download
The semantic-diff 'sem' download (#871) ran curl / Invoke-WebRequest with no
timeout. The sidecar installs after plannotator itself, so a slow or hung fetch
could wedge an otherwise-complete install. Add --connect-timeout 10
--max-time 120/60 (sh, cmd) and -TimeoutSec 120/60 (ps1) so it times out and
skips gracefully, and document the PLANNOTATOR_SKIP_SEM_INSTALL opt-out in the
installer help.
* chore(install): remove the Glimpse install option from the installers
The installers offered to `npm install -g glimpseui` (a wizard prompt +
--glimpse/--no-glimpse flags + saved pref). Drop all of it across install.sh,
install.ps1, and install.cmd: flags, usage text, the wizard question, the
glimpse_present detection, the saved-pref read/write, the persistence clauses,
and the install block. The guided wizard is now two questions (extras,
model-invocable).
Runtime Glimpse support is unchanged: the app still opens in the native window
if glimpseui is on PATH (packages/server/browser.ts), still gated by
PLANNOTATOR_GLIMPSE. Updated install.test.ts (asserts the installers are
glimpse-free) and the installation docs.
* feat(plan): 0.20.0 release announcement dialog
Turn the first-run look chooser into a two-page 0.20.0 announcement.
- Page 1: header with a "Full release notes" link, a four-up feature
grid (fresh look, semantic review, multi-repo review, leaner install),
and the grid/clean plan-look chooser.
- Page 2: "Workspaces are coming" teaser with the waitlist image + link.
- Footer actions grouped bottom-right on both pages (shimmering
"Workspaces are coming" teaser via TextShimmer + the primary action)
so focus stays in one corner across the page turn.
- Reshoot the grid/clean chooser screenshots on a clean prose plan and
add the workspaces teaser image.
* feat(plan): add full-page HTML feature card, lead with leaner install
Add a fifth what's-new card for --render-html (annotate HTML reports and
explainers rendered full-screen) and move Leaner install to the front of
the grid.
* feat(review): add semantic diff overview
* fix(review): address semantic diff review findings
* fix(review): harden semantic diff fallback
* fix(review): avoid repo-local sem execution
* fix(review): normalize semantic diff cwd
* test(review): cover semantic diff local cwd
* fix(gitlab): fetch raw MR diffs
* fix(review): harden sem path resolution
* docs(review): add semantic diff handoff
* feat(review): semantic diff cards, header sem badges, jump-to-line
- Restyle the semantic panel as real bordered cards (shadcn surface) instead
of ASCII box-drawing; centered column, grid-aligned entity rows.
- Hide orphan (module-level) changes from the rows, matching sem's default;
the count still surfaces in the summary line.
- Add a 'sem · N' hover popover to each diff file header showing that file's
semantic changes, reusing the same row component as the panel.
- Clicking an entity (panel or popover) scrolls the diff to the lines via
pierre's [data-selected-line]; centers only when off-screen so manual
drag-selection isn't disturbed.
- Extract shared rows/helpers into semanticDiffShared; share one cached
/api/semantic-diff fetch across header badges.
* feat(review): land on All files by default
Semantic diff stays available via the file-tree nav entry and header badges,
but it's no longer the initial landing view.
* chore(review): drop dead change-symbol entries, log badge fetch failures
- Remove unreachable 'moved'/'renamed' entries from the changeSymbols table
(getChangeSymbol early-returns for those before the lookup).
- Log a console.error once per patch when the file-header badge's semantic
diff fetch fails or returns a non-ok status, so a systemic failure leaves a
trace instead of every badge silently showing nothing.
* feat(review): flatten semantic diff panel into grouped list
- Dissolve the per-file bordered cards into flat sections: the file path is now
a quiet underlined header (single hairline) with entity rows flush beneath,
hierarchy from whitespace + hover tint instead of boxes.
- Split the path into muted directory + emphasized filename for faster scanning.
- Nudge the add/remove glyphs (⊕/⊖) ~15% larger, line-height pinned so rows
don't grow; modified/rename/reorder glyphs unchanged.
Two agent command-surface cleanups. Both remove only the command entry points; all underlying infrastructure stays.
1. /plannotator-status (Pi): removed — it echoed phase/plan-file/progress on
demand, but that state is already shown ambiently (status bar + live
checklist widget). The phase/checklist state machine is untouched.
2. /plannotator-archive (all agents): removed the command/skill entry points
across every surface — Claude/Codex/Kiro skills, Pi, OpenCode (handler +
dispatch + cli-bridge + embedded + stub), Droid, the Kiro agent prompt, all
three installers, docs, marketing, and the CI deprecated-command guard. The
installers also gained a stale-skill cleanup so upgraders drop a previously
installed plannotator-archive skill.
Kept (infrastructure) — archive browsing stays available in-review via the
sidebar: the `plannotator archive` CLI subcommand (apps/hook/server), the
mode:"archive" server path + /api/archive endpoints, ArchiveBrowser/useArchive,
the sidebar Archive tab, sessions.ts "archive" mode, and ~/.plannotator/plans
storage.
Verified: bun test scripts/install.test.ts → 72 pass; pi-extension typecheck +
build:opencode pass; repo-wide residual scan clean; KEEP-set integrity
confirmed; one orphaned import (opencode commands.ts) caught in self-review and
removed.
Follow-up to #850 (unreleased), fixing two pre-release regressions vs v0.19.27.
- Claude Code: add apps/skills/claude/* skills using `!`plannotator <sub> $ARGUMENTS``
+ allowed-tools + disable-model-invocation, so /plannotator-review|annotate|last|
archive auto-run with no permission prompt and pass arguments through — matching
the old slash commands. Codex keeps the prose apps/skills/core/*. Installers route
~/.claude/skills <- apps/skills/claude and ~/.agents/skills <- apps/skills/core.
- Installers: gate the wizard so an unattended-but-open /dev/tty can't hang
(PLANNOTATOR_PROMPT_TIMEOUT, default 30s; timeout/EOF resolves to safe "no").
Don't gate on $CI (explicit --reconfigure/--extras must win). Don't persist
timed-out wizard answers as install-prefs. Keep ask_yes_no's read in a tested
context so set -e never aborts on a timeout.
72 installer tests pass.
* feat: single-source skills into core/extra, replace Claude commands with skills, de-hardcode installers
- apps/skills/core/{review,annotate,last,archive}: single authoritative
source for the always-installed skills (archive is new); all carry
disable-model-invocation + agents/openai.yaml sidecars
- apps/skills/extra/{compound,setup-goal,visual-explainer}: no longer
default-installed (except Kiro); installers print an
`npx skills add backnotprop/plannotator/apps/skills/extra` suggestion
- Claude Code: apps/hook/commands/ deleted, command heredocs removed;
core skills in ~/.claude/skills are the slash commands now
- Installers: OpenCode/Gemini command files copied from an extended
sparse checkout instead of heredocs; install.cmd gains the previously
missing OpenCode command install; aggressive cleanup of legacy
~/.claude/commands and ~/.codex/skills artifacts
- Codex: core skills install to ~/.agents/skills (official path);
~/.codex/skills install removed
- Pi: extension no longer bundles skills; #670 settings filter removed
* fix: review findings — old-tag soft guards, cmd replace-not-merge, plugin-update hint, frontmatter test
- install.sh: a --version tag predating apps/skills/core no longer aborts
the whole copy subshell (which also skipped OpenCode/Gemini commands);
core skills now soft-skip with an accurate message, matching ps1/cmd
- install.sh: subshell failure message no longer claims "git required"
when git was present (clone/network errors get their own wording)
- install.cmd: pre-remove skill dirs before xcopy so upgrades replace
rather than merge (stale files from renamed/deleted skill files no
longer linger; parity with sh/ps1)
- all installers + docs: tell upgraders to run /plugin marketplace update
so the plugin's old namespaced plannotator:* commands disappear (#817)
- install.test.ts: assert every core SKILL.md sets
disable-model-invocation: true — the load-bearing line that keeps core
skills out of Pi's system prompt (#842 regression guard)
* test: pin old-tag soft-guard behavior, dedupe core-skill list in tests
* fix: interrogation review findings — cross-installer diagnostic parity
- install.ps1/install.cmd: emit the "predates the core/extra skill
layout" diagnostic on old pinned tags instead of silently skipping
core skills (parity with install.sh)
- install.ps1: clone/network failure no longer claims "git required"
(git was already verified present); the outer catch now reports the
actual exception
- install.sh: "Installed OpenCode/Gemini commands" echoes are guarded
on the copy actually having a source, so old pinned tags don't print
false success (ps1/cmd already gated this way)
- AGENTS.md: opencode-plugin commands/ comment now reflects all four
command stubs
- install.test.ts: shared test asserts the soft-skip diagnostic exists
in all three installers and pins ps1's honest failure wording
* fix: respect CODEX_HOME for Codex home directory (#852)
Codex stores config and state under $CODEX_HOME when set, falling back
to ~/.codex (developers.openai.com/codex/config-advanced). Plannotator
hardcoded ~/.codex in two places:
- runtime: codex-session.ts scanned ~/.codex/sessions for rollout
files, so `plannotator last` failed with "No rendered assistant
message found" when CODEX_HOME pointed elsewhere. Now resolved the
same way copilot-session.ts handles COPILOT_HOME and session-log.ts
handles CLAUDE_CONFIG_DIR.
- installers: detection, config.toml/hooks.json paths, manual-setup
instructions, and the stale-skills cleanup now derive from
CODEX_HOME in all three scripts.
Tests: codex-session.test.ts covers rollout discovery under a
CODEX_HOME temp dir; install.test.ts asserts all three installers
respect the variable and that the fallback is the only hardcoded
~/.codex path left in install.sh.
* fix: hard-fail skill install, guard command cleanup, one-time extras migration
External review triage on PR #850 surfaced two real installer issues:
P1 — commands deleted before replacement: the Claude command cleanup
ran before the git-gated skill install, so a missing git, a failed
clone, or an old pinned tag deleted the user's slash commands and
installed nothing (a regression — the old installer needed no git).
Now:
- missing git is a hard failure before anything is touched ("install
git, then run this installer again")
- a failed fetch is a hard failure ("something went wrong — run the
installer again") instead of a silent skip
- the legacy command cleanup runs AFTER the install and only removes a
command file when its same-name replacement skill exists on disk
- old pinned tags keep the soft-skip (no deletion happens, commands
survive, CI e2e against old tags stays green)
P2 — recurring extras deletion: the extras cleanup ran on every
invocation, deleting copies users reinstalled via the suggested
`npx skills add` (the copies are byte-identical, so only provenance
can tell them apart). The cleanup is now a one-time migration recorded
in a migrations ledger under the Plannotator data dir
(<PLANNOTATOR_DATA_DIR|~/.plannotator>/migrations/), the same
record-what-you-did pattern package managers use.
All three installers (sh/ps1/cmd) updated in parity; tests pin the
guard condition, the ledger gating, and the hard-fail messages.
* test: tripwire — install.cmd must never contain /dev/null redirects
* fix: every skill sets disable-model-invocation — no exceptions
Maintainer rule: all Plannotator skills are user-invoked, never
model-auto-invoked. setup-goal (missing since #665) and the three Kiro
skills now carry the flag. The frontmatter test scans every SKILL.md in
apps/skills/core, apps/skills/extra, and apps/kiro-cli/skills
dynamically — with a floor of 10 — so a future skill cannot ship
without it.
* docs: git is a hard installer requirement; clarify post-gate sections complete on re-run
* docs: align ps1/cmd comments with hard-fail semantics
* feat: guided install — extras opt-in via skills CLI, model-invocation picker
Interactive terminals get a two-question wizard on first run:
1. Install the extra skills? Yes delegates to `npx skills add
backnotprop/plannotator/apps/skills/extra` (its UI picks the agents),
wired to /dev/tty so piped curl|bash installs still work. Skipped
when extras already exist on disk.
2. Make any skills callable by the model? Yes opens a space-toggle
checkbox (sh/ps1) or numbered toggles (cmd), listing all skills if
extras were chosen, core-only otherwise. Chosen skills get
disable-model-invocation stripped from their INSTALLED copies and the
Codex sidecar's allow_implicit_invocation flipped — re-applied every
run since installs replace skill folders. Repo sources stay locked.
Answers persist to <data dir>/install-prefs (shared format across all
three installers) and re-runs reuse them silently; --reconfigure
re-opens the wizard. Automation is untouched: no terminal means no
prompts and today's defaults; --extras/--no-extras/--model-invocable/
--non-interactive give scripts explicit control.
* fix: self-review of guided install — cmd pipe expansion bug, flag/wizard interplay
- install.cmd: the checkbox preselection used `echo !var! | findstr` —
each side of a cmd pipe runs in a child WITHOUT delayed expansion, so
the saved choices passed through as literal !var! text and
preselection never matched. Replaced with a substring-replace
containment test (no pipe).
- all three: a wizard question whose answer was already provided by a
CLI flag (--extras/--no-extras/--model-invocable) is no longer asked
and then silently overridden — the flag pre-answers it.
- install.cmd: unknown-option usage line now lists the wizard flags.
* feat: guided install question 3 — install Glimpse (native window)
glimpseui (third-party npm package, PR #840) gives Plannotator a native
WebView window instead of a browser tab; the runtime already
auto-detects it on PATH, so a global install is all that's needed.
- Wizard asks "Install Glimpse?" (default yes) after the skills
questions; skipped when glimpseui is already on PATH
- Yes runs `npm install -g glimpseui` (bun fallback on sh/ps1; printed
instruction when neither exists) — wizard or explicit flag only,
silent re-runs never install software
- --glimpse / --no-glimpse flags for automation; choice persisted to
install-prefs like the others
- docs + tests updated (glimpse detection, install command, flags, and
persist-condition assertions across all three installers)
* fix: self-review of Glimpse question — cmd bun fallback, stale usage text
* fix: merge-window hardening — guard Codex cleanup, remove old-installer junk dirs
plannotator.ai serves install.sh live from main (public/ symlink,
deployed on push), while the script fetches repo files at the LATEST
RELEASE TAG. Between merging the core/extra restructure and cutting the
release that ships it, the live script runs against the old-layout tag.
Two hazards in that window:
1. The Codex stale-skill cleanup removed working ~/.codex/skills with
no successor installed (core skills soft-skip on old tags). Now the
cleanup runs AFTER the install and removes a core skill only once
its replacement exists in ~/.agents/skills — same guard the Claude
command cleanup uses. The compound/setup-goal stale copies stay
unconditional (never Codex's to begin with).
2. The reverse combo (cached OLD script + NEW release tag) wholesale-
copies apps/skills/* and leaves junk core/ and extra/ directory
copies in ~/.claude/skills. Never valid skill names — all three
installers now remove them on every run.
* fix: glimpseui is a devDependency — consumers never use it from node_modules
PR #840 added glimpseui to dependencies in @plannotator/server and
@plannotator/pi-extension, but nothing imports it: both runtimes detect
the CLI on PATH (Bun.which / a manual PATH walk) and spawn it. The dep
only ever mattered in repo development, where `bun run` prepends
node_modules/.bin to PATH. For consumers it was inert download weight —
OpenCode plugin installs and `pi install` pulled a third-party package
that could never be detected (Pi's loader does not expose
node_modules/.bin; verified). Moved to devDependencies in both: dev
flows keep working, published packages stop shipping it. The sanctioned
end-user path is the guided installer's global `npm install -g
glimpseui`.
* fix: clean stale plugin command files from the installed plugin checkout (#817)
The installer already manages hooks.json inside
~/.claude/plugins/marketplaces/plannotator/apps/hook/, so the earlier
"don't reach into plugin storage" rationale for leaving the old
namespaced plannotator:* command files there was inconsistent. All
three installers now remove them — same replacement-skill guard as the
bare ~/.claude/commands cleanup — making the #817 duplicate menu
entries die on a single installer run + restart instead of waiting for
/plugin marketplace update. Hints/docs reworded accordingly.
* Revert "fix: clean stale plugin command files from the installed plugin checkout (#817)"
This reverts commit 3df37da92c.
* fix: Windows CI — assert the new no-commands contract; gate cmd wizard on a real console
The cmd e2e CI step still asserted the OLD contract (installer writes
~/.claude/commands/plannotator-*.md with the ! prefix). Commands are
dead; the step now guards the NEW contract: a fresh install must write
NO plannotator command files. The Gemini TOML assertions stay — they
now verify the verbatim checkout copy delivers intact files (the tag
the e2e pins, v0.17.1, contains apps/gemini/commands).
The failure also exposed that install.cmd ran the wizard on redirected
stdin (set /p falls through to defaults at EOF) — and Q3's default
being yes meant CI silently ran `npm install -g glimpseui`. cmd now
probes for a real console via `timeout /t 0` (errors when stdin is
redirected), matching sh's /dev/tty and ps1's IsInputRedirected gates:
no console, no wizard, no wizard-only installs.
Add glimpse config option to PlannotatorConfig and resolveUseGlimpse()
resolver. Wire into both Bun and Pi browser-opening paths. Document
PLANNOTATOR_GLIMPSE, PLANNOTATOR_GLIMPSE_WIDTH, and
PLANNOTATOR_GLIMPSE_HEIGHT in env var reference and AGENTS.md.
For provenance purposes, this commit was AI assisted.
* feat: message picker for annotate-last (#800)
When running /plannotator-last after /rewind, the newest transcript
entry is no longer the message the user intended to annotate, and there
was no affordance to pick a different one.
Adds a picker UI that surfaces the recent assistant messages so the
user can choose which one to annotate:
- A "Message N of M" button in the Viewer's sticky-top action bar
(alongside Copy / Global comment / Attachments), so it stays
accessible while scrolling.
- A "Messages" tab in the left sidebar with the full list
(newest-first, preview + timestamp, default ★), mirroring the
existing Files / Versions / Archive tab pattern.
Wired for Claude Code, Codex, and Droid (all share apps/hook/server).
OpenCode, Pi, and Copilot still get the original single-message
behavior — they don't emit recentMessages, so the picker affordances
hide cleanly.
Default selection (index 0) matches today's "last message" behavior,
so users who don't interact with the picker see no change.
* feat: extend annotate-last picker to Copilot and OpenCode
The picker UI from #800 was wired for Claude / Codex / Droid only. Pull
Copilot and OpenCode onto the same shape so users on those harnesses
also get the recent-messages picker when annotating the last assistant
message.
- Copilot: replace getLastCopilotMessage with getRecentCopilotMessages,
walking events.jsonl newest-first up to 25 assistant.message events.
- OpenCode: rewrite the session walk to collect up to 25 messages
(newest first) instead of bailing on the first hit; normalize the SDK
time.created (ms epoch) to ISO to match the shared picker contract.
- Both pass recentMessages to startAnnotateServer only when length > 1,
matching the existing Claude/Codex/Droid behavior.
Also trims a leftover narrating comment in MessagesBrowser and refreshes
the stale Copilot session-parser header.
Pi parity follows in the next commit (needs round-trip of the picker
selection through /api/feedback so its post-submit anchoring quotes the
right message).
* feat(pi): wire annotate-last picker with feedback round-trip
Extends the picker UI (#800) to Pi and fixes a Pi-specific anchoring bug
the picker would otherwise introduce.
Picker plumbing
- assistant-message: getRecentAssistantMessages walks the active branch
newest-first, returning { messageId, text, timestamp? } in the same
shape the other harnesses produce.
- Plumbed through plannotator-browser / plannotator-events so the Bun
server's recentMessages option is populated when the branch has more
than one assistant message.
Anchoring fix
- Pi quotes the targeted assistant message back to the agent because its
UX is async — the conversation may have moved on by feedback time.
With the picker, that target is no longer guaranteed to be the
snapshot taken when the UI opened. The editor now sends the user's
selectedMessageId with /api/feedback; Pi looks it up in the current
branch via findAssistantMessageByEntryId and quotes that message
instead. Falls back to the original snapshot if the entry is gone.
- The round-trip field is optional and only meaningful in annotate-last
mode; other harnesses (and other modes) ignore it.
Timestamp safety
- Pi's SDK currently types SessionEntryBase.timestamp as string, but the
picker contract everywhere else is ISO. Treat the value as unknown and
normalize string/number(ms)/Date to ISO; drop anything else, rather
than blind-casting and risking silent drift if the SDK changes.
* chore: strip issue-number references from comments
Comments shouldn't rely on external references — issue numbers age out
of context, link rot is a thing, and a reader shouldn't need to open
GitHub to understand why a line exists. Strip the `(#800)` and `(#570)`
parentheticals from comments and doc strings across the picker and
review-gate code; the surrounding "why" content is preserved.
* fix: prevent removeChild crash when switching annotate-last messages
Switching the picked message remounted nothing, so React reconciled new
content against DOM that web-highlighter had mutated with <mark> nodes,
throwing removeChild. Drive the Viewer key (and StickyHeaderLane's
remount token) off a shared viewerContentKey so a message switch fully
remounts the Viewer and re-anchors the sticky-header observer.
Also cap MessagesBrowser row previews via previewText() and drop the
redundant 'block' class that was overriding line-clamp-2.
* feat: persist annotate-last feedback across messages
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* feat: open plannotator in glimpse
* fix: harden Glimpse browser integration
* chore: refresh lockfile after rebase
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Universal, auto-detected Kiro CLI support — no flag, no separate installer.
When ~/.kiro exists (or kiro-cli is on PATH), the installer copies Plannotator
skills and a custom agent into ~/.kiro, the same convention used for Codex and
Gemini.
- packages/shared: add the kiro-cli agent origin (badge, prompt runtime,
PLAN_TOOL_NAMES). Origin is cosmetic for Kiro (no dedicated AI provider).
- apps/kiro-cli: 3 origin-baked skills (review/annotate/archive) + an example
custom agent that wires every skill via skill:// resources and a
plannotator-scoped shell tool. setup-goal + visual-explainer install from
apps/skills (no content duplication).
- scripts/install.sh: auto-detect ~/.kiro, sparse-checkout apps/kiro-cli,
install 3 kiro + 2 shared skills + the agent (never clobbering an existing
one); covered by scripts/install.test.ts.
- scripts/install.ps1 + install.cmd: Windows parity mirroring the Codex
pattern. Statically verified only; not yet runtime-tested on Windows.
- docs: installation + Kiro guides, AGENTS.md, env-var reference updated.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>