Adds a first-class artifact gallery and annotation workflow for GitHub pull requests and GitLab merge requests, including images, GIFs, video timestamps, rendered HTML, Markdown, provenance-aware feedback, authenticated provider resources, and reliable CDN/media handling.
Adds current-architecture GitButler workspace, stack, and branch review support across Bun and Pi while preserving the existing Git, JJ, and P4 paths.
Co-authored-by: Dan Susman <56033661+dansusman@users.noreply.github.com>
* feat(server): support bounded port ranges
* fix(server): harden bounded port retries
* fix(server): preserve non-range port behavior
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* fix(server): return JSON 404 for unknown /api/* routes instead of HTML
All three servers (plan, review, annotate) serve the SPA HTML for any
unmatched route. This means hitting /api/nonexistent returns the full
HTML page with 200 OK — confusing for API clients and wasteful.
Now unknown /api/* paths get a proper JSON 404 response before the SPA
fallback kicks in. Non-API paths (like /some/random/path) still serve
HTML for SPA routing as before.
Tested on all three server types.
* fix(server): guard unknown API routes across runtimes
---------
Co-authored-by: buihongduc132 <buihongduc132@gmail.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Use one shared Light/Dark/System registry across review, plan, annotate, Settings, and published UI consumers. Preserve the public Mode contract, scope wider menu geometry to theme pickers, and keep constrained palettes coherent in rendering and previews.
Co-authored-by: Vladyslav Hrabovyi <vladyslav.hrabovyii@gmail.com>
Co-authored-by: sasha <kredich05@gmail.com>
Use literal light and dark colors at the Mermaid themeVariables boundary while preserving OKLCH for ordinary page CSS.
Add a real Mermaid 11 light/dark render guard and block delivery on empty or error SVG output.
Co-authored-by: FND <fndevve@proton.me>
* fix(ui): keep Ask AI input visible after responses
Long completed responses caused the h-full chat flex item to retain its automatic minimum height, pushing the follow-up composer below the sidebar viewport. Let the panel consume the remaining flex space with min-h-0 so only the message area scrolls.
* chore: remove Ask AI browser harness
* fix(server): bound startup file discovery (#978)
* test: observe the server's own warm cache key so the ordering tests actually pin the fix
Review finding: both bind-before-warm ordering tests raced against
observeWarmState(projectRoot), but on macOS mkdtempSync returns /var/...
while the chdir'd server warms under the realpath /private/var/... —
a different warmFileListCache key. The tests therefore raced a FRESH
warm (always pending at observation time) and passed on the OLD broken
code too. Observing process.cwd() inside onReady uses the server's real
key. Verified: with old resolve-file/server code checked out, all four
ordering tests now fail; on the fix they pass (17/17 across both files).
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
Fixes for the three-agent adversarial sweep findings:
- reference-handlers.ts: seed the user's modified/untracked files BEFORE
the bulk walk — the 5000-file cap (d3c9de1e) filled in raw readdir order
and could silently drop the exact files the user just edited from the
annotate browser (and the truncated latch broke the merge loop on its
first iteration).
- opencode commands.ts: pass `project` to startAnnotateServer at both
call sites (annotate + annotate-last) via detectProjectName, matching
the hook and Pi runtimes — OpenCode annotate history no longer lands in
the shared "_unknown" bucket.
- App.tsx: the Alt-Alt destination double-tap now dismisses the
DestinationSpotlight — the coachmark advertised that exact gesture but
its own keydown handler deliberately ignores modifiers, so performing
the tip left the dim overlay stranded.
- annotate.ts + pi mirror: degradation notice now reads "warning: annotate
history unavailable" so OpenCode's logCliWarnings forwarder (which
filters on \bwarn(ing)?\b) actually surfaces it.
- cli-bridge.ts: a rejected showToast un-marks the URL (the other delivery
path can retry) and logs the failure instead of being fully silent.
- test.yml: register packages/editor/editableDocumentsHook.test.tsx (7
draft/conflict tests, DOM-gated since #936, never ran in CI). Repo-wide
sweep confirms all 19 DOM-gated test files are now registered.
Gates: DOM batch 102/0 across 19 files, review+server 507/0, opencode 70/0,
install harness 85/0, full typecheck incl. strict-consumer, review build OK.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
Adversarial QA findings. (1) install.ps1 and install.cmd spliced the
absolute exe path UNQUOTED into the generated hooks.json commands — any
Windows user with a space in their profile path (C:\Users\John Smith\...)
got hook commands that word-split when the hook shell runs them, so plan
review silently never intercepted (install.sh is immune: it writes the
PATH-resolved bare name). Both hook commands in both installers now wrap
the path in JSON-escaped quotes, pinned by updated harness assertions.
(2) install.cmd still carried 32 em-dashes + 2 ellipses — several in
user-facing echo lines — which render as mojibake on cmd.exe's default
non-UTF-8 codepages; #1021 fixed exactly this class in install.ps1 but
left cmd untouched. Full ASCII purge, with a new ASCII-only regression
test mirroring the ps1 one. Harness: 85 pass / 0 fail.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
Adversarial self-review findings on 1f7d08f5: (1) the cutout was measured
once + on window resize only, so async header siblings (platform error
banner, partial-diff notice) shifting the destination button left the ring
on empty space — now also re-measured on a 250ms interval while the
transient overlay is up. (2) The capture-phase Escape handler plus the z-90
click-trap could strand a keyboard-opened surface (search at z-50) dimmed
and unclickable underneath — now ANY non-modifier keypress dismisses the
spotlight, with only Escape stopping propagation (topmost-overlay
semantics); shortcuts proceed and their surfaces never open under the dim.
(3) "a MR" → "an MR" for GitLab via a vowel-sound article check.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
Self-review hardening of e5fcc415: the hey-api SDK returns {error} for HTTP
failures (404 on pre-toast hosts is safe), but a fetch-level failure (host
server restarting) REJECTS the promise, and `void promise` doesn't catch
that. Both toast call sites now .catch(() => {}) when the result is
thenable, so the cosmetic toast path is strictly quieter than the
surrounding app.log pattern.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
QA probe: with PLANNOTATOR_DATA_DIR on a read-only mount, `plannotator
annotate file.md` crashed before the UI opened — twice. (1) #961's
saveToHistory call on annotate open was unguarded, so the new version-diff
enhancement gated startup that was stateless at v0.22.0; now wrapped
(both the Bun server and the Pi mirror) and degrades to no-version-diff
with a stderr notice. (2) registerSession's mkdir/writeFileSync (pre-existing
since #795) threw after the server was already listening; the session
registry only powers `plannotator sessions` discovery, so it is now
best-effort, and unregisterSession/listSessions tolerate an unwritable
sessions dir too. Verified: read-only data dir now starts, serves
/api/plan with no versionInfo, and prints the session URL.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
First-time PR reviewers get a driver.js-style coachmark (zero new
dependencies: rounded cutout + oversized box-shadow dim, portaled to body)
pointing at the header's Agent/GitHub destination dropdown, explaining that
feedback can post to the PR or go to the agent session and can be switched
at any time (Alt Alt). Cookie-gated one-time flag
(plannotator-review-dest-spotlight-seen, versioned) mirroring the
guide-intro/review-setup gates. Renders strictly after the first-run dialog
chain (guide intro → look-and-feel → review setup) has cleared and only in
PR mode; opening the destination menu organically also marks it seen.
Escape/click-anywhere/Got-it dismiss. GitLab-aware copy via
platformLabel/mrLabel.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
QA traced the repeatedly-regressed remote-URL invariant to its root cause on
OpenCode: every URL path (logPlannotatorReady, the cli-bridge stderr
forwarder, and the ready-file poller) funneled exclusively through
client.app.log, which OpenCode documents as "write a log entry to the server
logs" — it is never rendered in the TUI. Remote users therefore never saw
the session URL. All three paths now ALSO call tui.showToast (the SDK's
visible surface), best-effort with optional chaining so older hosts without
/tui/show-toast no-op. A shared per-run toastedUrls set dedupes the stderr
and ready-file deliveries so one session never stacks two toasts.
Also: recognize the current binary's "Plannotator session ready" stderr
phrasing in formatUserFacingCliStderrLine (the old "Open this link" match no
longer fires; only the bare-URL line was being forwarded), and add
data-print-hide to the resize-handle cursor tooltip portal so print.css hides
it (QA print-clipping finding).
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
QA audit found four DOM-gated test files added since v0.22.0 (srcdoc,
InlineMarkdown.resolveLinkedDoc, MarkdownDiff.frozen,
MarkdownEditor.extensions) that self-skip under plain `bun test` and were
never added to the DOM_TESTS batch — so the mounted-seam contracts for the
published markdown-editor/atomic-editor packages ran nowhere in CI. Also
registers components/sidebar/FileBrowser.test.ts (pre-existing gap; its new
filtering tests from 1267cd50 were equally unexercised). All 5 files pass
locally: 40 tests, 0 fail.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
The launchable-jobs Codex catalog (review jobs, tour, guide, GuideEmptyState)
is a hardcoded list, so yesterday's GPT-5.6 release was missing from the
selectors. Ask AI needs no change — the codex-app-server provider discovers
models dynamically via model/list. The server passes -m through verbatim
(codex-review.ts), so the catalog addition is the complete fix. Defaults stay
on gpt-5.5. Also relocks bun.lock's workspace entry for ui 0.28.0 (missed in
the bump commit).
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
The CommentPopover focus effect scheduled a setTimeout(0) keyed on [mode],
but in popover mode the textarea only renders after `position` is measured
by a later effect. Whether the 0ms timer fires before or after that commit
is engine-dependent task ordering: Chromium commits the re-render first, so
focus lands; WebKit fires the timer first, so textareaRef.current is still
null and the comment editor opens unfocused (reproduced in a WKWebView host
— the glimpseui integration — where users had to tab or click into the
field for every annotation).
Focus from a ref callback on the textarea instead: it runs exactly when the
element mounts, so there is nothing to race. Mode switches (popover/dialog)
still refocus because the textarea remounts. The deferred focus and
caret-to-end behavior are preserved; isConnected guards the popover closing
before the timer fires.
fetchModels() built the dropdown label from the model name alone,
so models with identical names from different providers (e.g.
deepseek-v4-pro via DeepSeek vs. via OpenRouter) were indistinguishable
in the UI even though their ids were already unique.
Fixes#988
Since Claude Code 2.1.199, a PermissionRequest "allow" decision for
ExitPlanMode is silently discarded unless it echoes updatedInput, because
ExitPlanMode requires user interaction and is not an MCP tool. The CLI then
falls back to its built-in approval dialog, so clicking Approve in the
Plannotator UI never returned control to the agent session (deny was
unaffected). This matches the reported behavior on 2.1.199 through 2.1.202.
Echo the original tool_input (in scope as event.tool_input) as updatedInput
in the Claude Code allow decision. Backward compatible: older Claude Code
versions treat the echoed input as unchanged (verified no-op on 2.1.198).
Fixes#995
Documents the 0.28.0 round for the Workspaces consumer: the
components/MarkdownDiff shim (frozen two-revision diff — newer revision as
the real document, deletions projected in place, toolbar/overview/gutter),
the byte contract on MarkdownDiffHandle (exact input strings incl. CRLF),
the frozen state/view edit rejection, and the extensions composition with
its capture-once caveat. Adds the supported-imports row and the 0.28.0
dependency note (wrapper ^0.4.0, engine ^0.8.0, new @codemirror/merge peer).
Publish is manual and owner-gated per HANDOFF (bun pm pack, then
npm publish --provenance, ui only — core is unchanged at 0.22.0).
Adds components/MarkdownDiff.tsx, a theme-bridging shim over
@plannotator/markdown-editor's frozen two-revision diff, mirroring the
MarkdownEditor shim exactly: ThemeProvider mode bridge, plannotator.css
theme import, extensions passthrough (wikiLinks composes), gridEnabled ->
the same design-system card chrome. Hosts import it from
@plannotator/ui/components/MarkdownDiff; AtomicDiffEditor and
@plannotator/atomic-editor stay off the allowlist.
Deps: @plannotator/markdown-editor ^0.4.0, @plannotator/atomic-editor
^0.8.0 (single lockfile copy each), plus @codemirror/merge — the engine's
new required peer — declared directly per the CM6 single-copy discipline.
Tests pin the seam end-to-end: public export surface + types, byte-exact
getMarkdown()/getOriginalMarkdown() incl. CRLF/trailing-space fixtures,
contenteditable="false" frozen contract, change navigation, wiki-link
composition through the shim, and theme/host-class forwarding. The
strict-consumer tsconfig now typechecks the new module.
0.6.0 and 0.3.1 will never be published; the engine is stamped 0.7.0 (adds
preferResolvedLabel on WikiLinksConfig — labeled links opting into resolved
titles, the flag the consumer needs) and the wrapper 0.3.2 (peer widened to
^0.5 || ^0.6 || ^0.7). The previous ^0.6.0 range would have excluded 0.7.0
under 0.x caret semantics and stripped preferResolvedLabel from the
re-exported type.
- @plannotator/atomic-editor ^0.7.0, @plannotator/markdown-editor ^0.3.2
- HANDOFF/README version references updated
- wikiLinks re-export test now sets preferResolvedLabel: true — a type-level
seam assertion (excess-property checking rejects unknown keys, verified by
negative control)
Re-verified against fresh npm-pack tarballs of both packages: full repo
typecheck, ui tests (354+66 skip plain / 420 DOM, 0 fail), build:css — all
green. Install still resolves only after the user publishes 0.7.0 + 0.3.2.
Documents the 0.27.0 round for the Workspaces consumer: the MarkdownEditor
'extensions' seam with its capture-once-per-documentId caveat, the wikiLinks
re-export through the ui surface, InlineMarkdown's sync resolveLinkedDoc
semantics, and the retirement of the H-ask-1 html-viewer patch. Marks HANDOFF
rough edge #4 (no live-collab extensions) resolved.
Publish is manual and user-gated per HANDOFF (bun pm pack, then npm publish
--provenance, core then ui); requires @plannotator/markdown-editor 0.3.1 and
@plannotator/atomic-editor 0.6.0 on npm first.
Workspaces carried two byte-identical one-line fixes against 0.25/0.26 for
strict tsc (--noUnusedLocals) on the shipped source. Verified with tsc under
the strict-consumer flags scoped to components/html-viewer: exactly two
TS6133s, no other strict errors.
- HtmlViewer.tsx: drop the unused React default import (jsx: react-jsx;
zero React.* uses)
- useHtmlAnnotation.ts: drop the unused 'annotations' destructured binding;
the UseHtmlAnnotationOptions property stays (callers pass it — removing it
from the options type would be a breaking API change)
New optional prop:
resolveLinkedDoc?: (target: string) =>
{ label?: string; status?: 'active' | 'deleted' } | null
Semantics (consumer-contracted, sync-only by design — the host backs it with
an in-memory cache; no async variant, no loading states, no phantom docs):
- absent or null → exactly today's rendering (stored label, live link)
- label → displayed instead of the stored label; stored label is the
fallback, the target the last resort
- status 'deleted' → muted non-link span (no anchor, no pointer, no link
icon, title "Document deleted"), even when onOpenLinkedDoc is present
- the callback receives the RAW stored target (doc_01XYZ), before the
.md-appending path normalization; onOpenLinkedDoc keeps receiving the
normalized path for non-deleted links, exactly as before
Threaded through all six recursive InlineMarkdown call sites so wiki-links
inside emphasis/strikethrough resolve too. Tests pin every branch, including
null → byte-identical innerHTML with and without onOpenLinkedDoc.
Hosts' import allowlist forbids @plannotator/atomic-editor; @plannotator/ui is
the single contract. wikiLinks and its types (WikiLinksConfig,
WikiLinkSuggestion, WikiLinkResolvedTarget, WikiLinkStatus) now re-export from
components/MarkdownEditor — colocated with the 'extensions' prop they feed,
already on the supported-import allowlist and under the strict-consumer gate.
Usage: build wikiLinks(config) and pass it via the shim's extensions prop.
Test pins identity with the engine export and exercises the type surface.