* fix(ui): in-page anchor navigation for headings
Intercept `#`-prefixed link clicks and smooth-scroll inside the sticky
scroll viewport instead of letting the browser jump (which broke when the
content lives inside a scrollable container). Headings now expose both a
canonical slug and a legacy slug via `data-anchor-aliases` so older shared
URLs keep resolving. Also listen for `hashchange` / initial hash so direct
links land on the right heading.
Adds `packages/ui/utils/anchors.ts` with `normalizeAnchorText`,
`decodeAnchorHash`, `slugifyHeadingAnchor`, `legacySlugifyHeadingAnchor`,
and `getHeadingAnchorAliases` (+ tests).
Also ignore `.serena/` tooling state.
* refactor(ui): drop unjustified slug aliasing
Anchor navigation didn't work before this branch, so no shared URLs exist
using any slug format — the legacy slug alias was preserving nothing.
The canonical slug was also a redefinition: heading ids are already
written by `utils/slugify.ts` (unicode-aware, `\p{L}\p{N}`). The new
ASCII-only `slugifyHeadingAnchor` would miss unicode headings ("Café"
writes `id="café"` but the nav code looked for `cafe`).
Nav path is now just: decode the hash and `getElementById`. The
sticky-header offset math — the part that actually mattered — stays.
* fix(ui): anchor-nav edge cases — share-hash collision and raw HTML blocks
Two issues flagged by cross-model review of 9978a40:
1. `useSharing` treated any non-empty `window.location.hash` as a share
payload. Plain anchor hashes like `#section-overview` fell into
`parseShareHash`, failed decompression, and popped the "Shared Plan
Could Not Be Loaded" dialog. Added an `isPlainAnchorHash` guard
(lowercase ASCII + digits + hyphen) that short-circuits before the
share path in both the initial load and the hashchange listener.
Share payloads are base64url-encoded deflate output, so they'll
always contain uppercase/`=`/`_` and never match the guard.
2. `HtmlBlock` (raw `<details>`, `<summary>`, etc.) skipped the new
nav handler. `rewriteRelativeRefs` returned early for any `#` href,
so anchors inside raw HTML fell back to native browser jump which
doesn't target the OverlayScrollArea. Now intercepts those clicks
and routes through `onNavigateAnchor` to match the `InlineMarkdown`
path. Threaded the callback through `BlockRenderer`.
* fix(ui): widen share-payload detection to cover Unicode + uppercase ids
Previous guard rejected any hash that wasn't lowercase ASCII + digits +
hyphen, so Unicode heading ids (`#café`, `#中文-标题`) and raw HTML ids
(`#MySection`) still fell into `parseShareHash` and popped the shared-
plan error dialog.
Flip the predicate: a share payload is base64url (`[A-Za-z0-9_-]`),
realistically ≥30 chars, and virtually always contains at least one
uppercase letter because deflate output has high entropy. Anything else
is handed back to Viewer to scroll to (or silently ignored).
Also add tests/test-fixtures/14-anchor-links.md exercising the full
character range — ASCII, Unicode (accented, CJK, Cyrillic), numeric
start, raw HTML ids — across paragraph, list, blockquote, alert,
directive, table, and raw `<details>` contexts.
Tests
This directory contains manual testing scripts for Plannotator.
UI Tests (manual/local/)
These scripts test the UI components and require a browser.
Plan review UI:
./tests/manual/local/test-hook.sh # Claude Code simulation
./tests/manual/local/test-hook-2.sh # OpenCode origin badge test
Code review UI:
./tests/manual/local/test-opencode-review.sh # Code review UI test
./tests/manual/local/test-worktree-review.sh # Worktree support test (creates sandbox with 4 worktrees)
See UI-TESTING.md for detailed UI testing documentation.
Integration & Utility Tests (manual/local/)
These scripts test integrations, releases, and provide utilities.
Binary release testing:
./tests/manual/local/test-binary.sh # Test installed binary from ~/.local/bin/
Tests the installed plannotator binary to verify releases work correctly.
Bulk plan testing (Obsidian integration):
./tests/manual/local/test-bulk-plans.sh # Iterate through ~/.claude/plans/
Opens each .md file from ~/.claude/plans/ in Plannotator. Great for testing Obsidian integration with multiple
plans.
OpenCode integration sandbox:
./tests/manual/local/sandbox-opencode.sh [--disable-sharing] [--keep] [--no-git]
Creates a temporary sandbox with a sample React/TypeScript project, initializes git with uncommitted changes, sets up the local OpenCode plugin, and launches OpenCode for full integration testing.
Options:
--disable-sharing: Createsopencode.jsonwith sharing disabled--keep: Don't clean up sandbox on exit--no-git: Skip git initialization (tests non-git fallback)
Obsidian utility:
./tests/manual/local/fix-vault-links.sh /path/to/vault/plannotator
Adds Obsidian backlinks ([[Plannotator Plans]]) to existing plan files in your vault.
SSH Remote Testing (manual/ssh/)
Tests SSH session detection and port forwarding for remote development scenarios.
cd tests/manual/ssh/
docker-compose up -d
./test-ssh.sh
See manual/ssh/DOCKER_SSH_TEST.md for detailed setup instructions.