mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
main
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e181b824cc |
Mobile-safe plan and code comment composition (#1297)
* feat: harden mobile comment composition * fix(ui): keep mobile app inside Safari viewport * docs: record physical mobile triage * fix(ui): extend plan canvas behind Safari controls * fix(ui): let mobile plans drive Safari chrome * fix(ui): release Safari top edge on mobile plans * docs: triage mobile feedback and close phase 1b * fix(ui): harden compact touch behavior |
||
|
|
7ad4d39ed9 |
feat(comments): reference agent skills with / or $ in plan review and annotate comments (#1229)
* feat(comments): reference agent skills with / or $ in plan and annotate comments
Typing / or $ at the start of a word in the document-UI comment composer
opens a picker of the user's global agent skills (~/.claude/skills,
~/.codex/skills, ~/.agents skills roots), served by a new GET /api/skills
on the plan and annotate servers in both runtimes (Bun + Pi mirror).
Multiple references per comment are supported; references live in the
comment text itself and are appended to exported feedback as a
'Skills referenced' block so the acting agent knows which skills to apply.
Human-invocation-only skills (disable-model-invocation: true frontmatter)
stay listed and selectable but render dimmed with a badge, warn in the
menu and composer, and are marked in the export so the agent is never
asked to invoke something it cannot.
Discovery reuses the review-skill loader (same roots, precedence, and
skip-and-log discipline), reads only an 8KB head per SKILL.md, caps the
catalog at 500 skills, takes no client input, and is never persisted;
any failure degrades to plain typing.
* fix(comments): harden skill references per review (trigger, IME, seam, fail-closed frontmatter)
Blockers:
- B1: a trigger now requires at least one query character. A bare / or $
no longer opens the catalog, so Enter stays a newline and Tab still
leaves the field ("This costs $" + Enter, "cd /" + Tab, bullets).
- B2: the menu ignores keys mid-IME-composition (nativeEvent.isComposing),
matching the 16 existing guards; Enter committing a Pinyin/Telex/Korean
candidate can no longer insert a skill.
- B3: the catalog request is a host seam (skillCatalogTransport via
configurePlannotatorUI), defaulting to the existing GET /api/skills.
- B4: resetSkillCatalogCache() invalidates outstanding requests
(generation counter), and a late-resolving stale request can no longer
overwrite a newer cached value or the export registry. The catalog
tests reset in beforeEach, so they hold in any file order.
Also:
- F1: skillReferences={false} is fully inert — the human-only notice memo
and the cache seed are gated on the prop.
- F4: frontmatter flag parsing no longer fails open: trailing YAML
comments are stripped, on/1 (and TRUE/yes etc.) read as true, the head
read is 64KB, and truncated unterminated frontmatter fails CLOSED on
disable-model-invocation.
- F5: extraction ignores markdown link destinations ([x](/name)), shell
redirects (cat /x > out), and /-triggered FHS root names (/run, /tmp);
menu insertion switches / to $ for those names so inserted references
always survive extraction.
- F6: the 500-skill cap slices after sorting, so which skills survive no
longer depends on readdir order.
- F3: /api/skills wiring guards for the Bun and Pi plan + annotate
servers (skills-endpoint.test.ts).
- Keyboard state machine tests against the real CommentPopover in
happy-dom (bare trigger, insertion, composition, Escape, highlight
bounding, opt-out inertness), added to the CI DOM step.
- The insertion path dismisses the trigger start so the menu close is
ordering-safe against React's select-plugin re-reading a stale caret.
* feat(comments): redesign the skill reference menu (bare triggers, no preselection, highlighted tokens)
Per maintainer direction, reversing the earlier bare-trigger opt-out
deliberately: typing a bare / or $ at the start of a word now opens the
full skill catalog immediately, and the safety story moves from the
trigger to the menu itself.
No preselection (the load-bearing rule): the menu opens with NO row
active, and while nothing is active every key behaves exactly as if the
menu were closed. "This costs $" + Enter is a newline; "cd /" + Tab
leaves the field (the proven regression that must never return). A row
activates only via ArrowDown/ArrowUp (Down from none lands on the first
row, Up on the last); only then do Enter/Tab insert. Pointer hover never
activates a row, because the menu floats exactly where the mouse rests
over the composer; a click inserts directly and never arms Enter.
Continuing to type re-filters and disarms any active row. Escape clears
the active row and dismisses when the user engaged (query typed or row
active); an unengaged bare-trigger menu passes Escape through so closing
the composer still costs one press.
Menu redesign to the reference look: icon, bold name, dimmed inline
description with ellipsis, right-aligned source column (Agents / Claude
/ Codex from the discovery roots), rounded generously padded rows, and a
subtle active-row background; human-only rows stay dimmed with their
badge and the warning now shows while such a row is ACTIVE.
Inserted references render highlighted in the composer via a mirrored
aria-hidden overlay behind a transparent-text textarea (identical font,
padding and wrapping metrics; scroll synced; tokens change color and
background only, drawn from the --primary theme token so every palette
works in light and dark). The caret keeps --foreground, selection uses a
translucent primary wash, and IME composition temporarily restores
native textarea text so composition underlines render normally.
skillReferences={false} still renders the plain pre-feature textarea.
Also, per review:
- extraction: dropped the over-broad shell-redirect exclusion (false
negatives on prose like "use /animate <- this one"; the motivating
case stays covered by the reserved-path rule)
- frontmatter: an unterminated frontmatter block now fails CLOSED on
disable-model-invocation even in complete (untruncated) files
- the reserved-path / to $ insertion switch stays: extraction still
reads /run as a path, and the new token highlight makes the switch
self-explanatory (an unhighlighted insert would look broken)
The composition guard, transport seam, catalog generation counter,
enabled gating, and export rules are unchanged and re-covered by the
rewritten DOM test matrix.
* fix(comments): give the skill reference menu adaptive, viewport-clamped placement
The menu rendered bottom-full with a fixed max-h-64: always upward, up to
256px, with no viewport awareness. With the comment popover near the top of
the viewport (annotating near the top of a document), typing a trigger ran
the menu off the top of the screen with its upper rows unreachable.
Placement now mirrors the popover's own computePosition idiom: measure the
space above and below the composer wrapper against window.innerHeight,
prefer above (the shipped direction; keeps the action row and human-only
notice visible), flip below when the list fits below but not above, and when
neither side fits pick the roomier side. The list's max height is clamped to
the available space (still capped at the former 256px), so the menu never
extends past a viewport edge. Recomputes on every commit (drag moves,
popover flips, filtering changing the item count, warning-footer toggles)
plus capture-phase scroll and resize listeners, matching the popover's
tracking. Visual design of the menu and rows is unchanged.
* feat(comments): inject human-only skill instructions into exported feedback
A human-only skill (disable-model-invocation: true) referenced in a review
comment used to export as a dead name the agent could do nothing with. A
human referencing a human-only skill IS the human invocation, so the export
now injects the skill's SKILL.md body verbatim (frontmatter stripped) inside
clearly delimited BEGIN/END SKILL INSTRUCTIONS markers, with the absolute
skill directory and the resolve-relative-paths pointer so references/,
scripts/, and assets/ stay actionable. Model-invocable skills keep exporting
as names the agent can invoke itself.
Transport is lazy: a new GET /api/skills/content?name= endpoint (Bun and Pi)
serves one discovered skill's body, capped at 20k chars with an explicit
truncation notice pointing at the file; the client fetches contents only for
the human-only skills actually referenced, keyed off comment state, and the
catalog now carries each skill's absolute dir so every failure path (deleted
skill, unreadable file, race with submit) degrades to naming the skill plus
its directory. Names are matched against discovery only and never used as
paths, so traversal cannot escape the skill roots. A per-export dedupe
injects each skill once even when several comments reference it, and
GLOBAL_COMMENT annotations run through the same block.
The referenced-skills header now says the reviewer is asking for the
invocation, and the human-only menu footer and composer notice explain that
the skill's instructions will be included with the feedback instead of
warning that the reference will not work.
* polish(comments): quiet, progressive human-only skill treatment
The human-only surfaces shipped with too much emphasis: a dimmed row plus
a bordered uppercase badge, an amber warning footer, and a persistent
amber notice in the composer after insertion. Human-only is a property of
a skill, not an error state, so the treatment is now quiet and
progressively disclosed:
- Menu rows render at full strength with a small muted 'human-only' pill
(bg-muted / muted-foreground tokens; no border, no dimming).
- The plain-language explanation (a model cannot invoke it, so its
instructions will be included with your feedback) appears as a muted
footer only while a human-only row is active (keyboard) or hovered
(pointer). Hover disclosure is purely visual state local to the menu;
it never touches activeIndex, so the no-preselection invariant and the
hover-never-arms-Enter rule are unchanged and re-asserted by a new test.
- When not disclosed, the same sentence stays in the DOM sr-only and
human-only rows point at it with aria-describedby, so the state reaches
assistive tech as text rather than as a purely visual badge (this does
not attempt the #1233 combobox semantics, and does not worsen them).
- After insertion, the highlighted token itself carries the quiet inline
marker (a dotted primary underline; text-decoration cannot move glyphs,
so overlay alignment is untouched) and the standing amber notice is
replaced by a native <details> disclosure: a single muted 'Includes
skill instructions' summary line that expands to the full accurate
sentence, operable by pointer, keyboard, and AT alike.
No amber remains; every color is a theme token (muted, muted-foreground,
border, primary, ring), so the treatment follows every palette in light
and dark. Copy is unchanged where it was accurate. Behavior is unchanged:
human-only skills stay selectable and injection still happens.
* fix(comments): harden human-only skill injection per adversarial review
Three findings on the injection path, each with tests that fail pre-fix:
1. Marker forgery: an injected SKILL.md body containing our own
`--- BEGIN/END SKILL INSTRUCTIONS ---` markers (or an
`[Instructions truncated:` notice) could close the block early — making
everything after it read as the reviewer's own words — forge a block for
a skill nobody referenced, or forge a truncation notice pointing at an
attacker-chosen path. Body lines matching the structural marker forms
(leading-whitespace and case variants included) are now visibly
neutralized before injection: kept verbatim but prefixed, never silently
deleted (neutralizeSkillMarkerLines).
2. Forged human invocation: POST /api/external-annotations is
unauthenticated on localhost, so any local process could submit a
comment referencing a human-only skill and cause its instructions to be
injected "at the reviewer's request". Annotations carrying a `source`
now still LIST their skill references but never cause verbatim
injection — human-only references fall back to naming the skill plus
its directory, with an honest reason. The content-prime effect skips
external texts for the same reason. A human referencing a human-only
skill IS the human invocation; a tool is not.
3. Unbounded read: readReferenceSkillContent read the whole SKILL.md
before slicing to the 20k cap, so an unauthenticated no-cors fetch loop
could balloon RSS by file size per request (measured +64.4MB for a 64MB
file). It now uses the same bounded readFileHead as the catalog,
reading only frontmatter allowance + 4 bytes per capped char + slack;
truncation detection is unchanged for any file whose frontmatter fits
the catalog bound, and frontmatter that overflows the read falls back
to null rather than serving raw YAML. Measured: 12 reads of a 64MB
SKILL.md now cost +5.1MB total.
Also: the fast-fail guard no longer rejects legitimately discovered names —
`name.includes("..")` 404'd a real `v1..2` skill dir forever (and `\` is
legal in POSIX names) while defending nothing, since the name is only ever
matched against discovery output and never joined into a path. It now
rejects exactly the names that can never be a readdir entry: empty, `.`,
`..`.
|
||
|
|
c08b188812 |
perf(ui): single Shiki highlighter, palette-matched code blocks, drop highlight.js (#1218)
* perf(build): stub out the dead Oniguruma WASM in every bundle
@pierre/diffs picks its Shiki engine with a runtime ternary:
engine: preferredHighlighter === "shiki-wasm"
? createOnigurumaEngine(import("shiki/wasm"))
: createJavaScriptRegexEngine()
Plannotator pins `preferredHighlighter: 'shiki-js'` (and Pierre's own
default is 'shiki-js'), so the Oniguruma branch never executes. Because
the choice is a runtime ternary, bundlers keep the `import("shiki/wasm")`
edge anyway and inline `@shikijs/engine-oniguruma/wasm-inlined`, a
~622 KB base64 blob, into the single-file HTML builds. The review app
paid for it twice: once on the main thread (via
`highlighter/shared_highlighter.js`) and once inside the `?worker&inline`
Pierre worker.
Alias `shiki/wasm` to a stub that throws if it is ever reached. Wired via
`resolve.alias` rather than a plugin because `resolve.alias` is shared
with Vite's worker build and `plugins` are not.
Highlighting output is unchanged: the JS regex engine was already the one
doing the work. Opting back into 'shiki-wasm' now fails loudly instead of
silently costing every user a megabyte of dead bytes.
apps/review/dist/index.html 19,424,646 -> 18,180,545 (-1,244,101 raw / -463,348 gzip)
apps/hook/dist/index.html 23,032,467 -> 22,410,416 (-622,051 raw / -233,485 gzip)
* perf(ui): consolidate code highlighting onto Shiki, drop highlight.js
The app shipped two highlighters. Shiki already tokenised the code-review
diff pane (via @pierre/diffs, JavaScript regex engine); highlight.js
separately coloured markdown fences and review suggestion snippets at
~982 KB minified for a full build of ~190 grammars. That second
highlighter is now gone.
Every call site moves onto `packages/ui/utils/codeHighlight.ts`, a thin
wrapper over Pierre's SHARED Shiki instance:
CodeBlock, Viewer, PlanCleanDiffView markdown fences
InlineMarkdown code-file hover preview
HighlightedCode review suggestion snippets
Reusing Pierre's instance rather than standing up a second fine-grained
one is deliberate. Pierre imports Shiki's full bundle, so every grammar
and theme is ALREADY inlined in the single-file builds: a separate
highlighter with a curated language list would have duplicated a subset
of bytes that are already there. Sharing costs nothing, gives every
language Shiki bundles instead of a shortlist, and — the point of the
change — guarantees fences resolve the exact same theme the diff pane
resolves.
Theming. `SHIKI_THEME_MAP` / `resolveSyntaxTheme` move from
`packages/review-editor/hooks/usePierreTheme.ts` to
`packages/ui/utils/syntaxTheme.ts`; usePierreTheme re-exports them, so
the review editor's imports are unchanged. `useFenceTheme()` feeds the
components and re-highlights on palette or mode change. Code blocks now
follow the active palette across all ~52 themes in both light and dark,
instead of always rendering github-dark and relying on hand-written
`.hljs-*` override stacks to stay legible. Those stacks are deleted:
`packages/editor/index.css`'s light-mode token palette, and
`colorblind.css`'s hand-tuned tokens which existed to APPROXIMATE
@pierre/theme's protanopia-deuteranopia themes that are now simply used.
Behaviour held fixed:
- Language-less fences stay plain text (#1212). No auto-detection
anywhere, including the hover preview, which previously called
`hljs.highlightAuto`. `HighlightedCode` derives its language from
the caller's file path; an unknown extension renders plain.
- `applyHighlight(el, ...)` keeps the imperative `hljs.highlightElement`
DOM contract the annotation layer reaches into, and writes plain text
at final size first so async highlighting causes no layout shift.
Already-attached grammars highlight synchronously — no flicker on
cached highlights.
- It also verifies the rendered text is byte-identical to the source
and falls back to plain otherwise, because annotations address code
blocks by text offset.
- `@plannotator/ui`'s public API is unchanged: the highlighter is a
module-level default like the package's other seams, no new props.
The `hljs` class on fenced `<code>` becomes `pn-code` (it is a
structural hook for blockTargeting, vim navigation and print.css, and it
named a library we no longer ship). `language-*` stays.
apps/review/dist/index.html 18,180,545 -> 17,270,889 (-909,656 raw / -291,921 gzip)
apps/hook/dist/index.html 22,410,416 -> 21,704,434 (-705,982 raw / -238,096 gzip)
Verified the diff pane is untouched: the rendered Pierre shadow-DOM
markup is byte-for-byte identical between an origin/main build and this
one (SHA-256 aa1ee88a…).
* fix(ui): strip stray NUL bytes from the code-highlight source
Two U+0000 bytes slipped into comments in the previous commit, which made
git treat the file as binary. Replaced with spaces; no behaviour change.
* fix(ui): keep code-block annotation marks across highlight swaps
Fenced code is annotated by hand: one `<mark data-bind-id>` inside the
`<code>` element, which `applyHighlight` also owns. Every highlight swap
(palette change, dark/light toggle, or the first async grammar attach
after load) replaces that element's children, so the mark was silently
wiped and nothing put it back. Annotation state, the sidebar panel and
exports were unaffected; the loss was purely visual, and deterministic.
`applyHighlight` now publishes every write through `onCodeHighlightSwap`,
synchronously, immediately after it. `Viewer` subscribes and re-paints the
fence's mark, so a swapped block ends up with BOTH the new theme's tokens
and its annotation. The shared painter (`paintCodeBlockMark`) moves the
token spans into the mark instead of flattening them to text, so creating
an annotation no longer costs a block its colours either.
Being driven by the swap also fixes the cousin race by ordering rather
than timing: share/draft restore runs on a timer after load, and on a slow
machine the first async swap could land after it and wipe the restored
marks per block. A restore that painted before the swap is now
re-established in the same task the swap ran in, and one that runs after
finds the mark already there.
Removal tombstones the id before re-highlighting, because the host drops
the annotation from state a tick later — without it the swap listener
would paint the just-removed annotation back in, and a fence carrying a
second annotation would end up bare.
Also closes the named gap in the WASM coverage: entry-assets only grepped
source, so a future @pierre/diffs bump could reintroduce the inlined blob
through a different import specifier unnoticed. It now greps the built
`apps/{review,hook}/dist/index.html` for the base64 WASM magic, skipping
on an unbuilt checkout and running for real in the CI job that builds the
bundles.
|
||
|
|
c072a16ca6 |
fix(ui): render language-less code blocks as plain text (#1212)
Bare fenced code blocks (no language tag) were passed to hljs.highlightElement() unconditionally, so highlight.js auto-detected a language and colored ordinary words. Skip highlighting when the block has no language so it renders as plain monospaced text. Fixes #1210 |
||
|
|
cbc6186a15 |
fix(ui): align list markers to first line in clean diff view (#838)
* fix(ui): align list marker to first line in clean diff view * refactor(ui): extract list-item marker and body into ListItemBody Three render paths (BlockRenderer and two in PlanCleanDiffView) duplicated the same marker-plus-paragraph scaffold. A recent alignment fix had to be applied to each surface independently, which exposed the duplication. The shared structure now lives in ListItemBody; call sites keep their own row wrapper with surface-specific concerns (indent, data attributes, hover props, interactivity). |
||
|
|
fcf2ba4cf5 |
fix: indent loose list continuation content under parent bullet (#705)
Closes #704 |
||
|
|
ba2e4d2a1d |
feat(ui): markdown reader parity — HTML blocks, GitHub alerts, GFM inline extras (#597)
* feat(ui): markdown reader parity — HTML blocks, GitHub alerts, GFM inline extras Brings the in-app markdown reader to parity with GitHub's flavored rendering. Additive across the parser + renderer; no behavior change for existing blocks. Refactor: - Extract InlineMarkdown (262 lines) out of Viewer.tsx into its own file - Extract BlockRenderer + block-type components (CodeBlock, HtmlBlock, AlertBlock, Callout) into components/blocks/ — Viewer drops from 1279 to ~770 lines - Each new block-level feature lands in BlockRenderer or a new blocks/*.tsx, not Viewer Block-level features: - Raw HTML blocks (<details>, <summary>, etc.) via balanced-tag parser branch, rendered through marked + DOMPurify for nested-markdown support; inner innerHTML set imperatively so React reconciliation doesn't collapse open <details> - GitHub alerts (> [!NOTE] / [!TIP] / [!WARNING] / [!CAUTION] / [!IMPORTANT]) with inline Octicons, title-case labels, GitHub's Primer colors (light + dark) - Directive containers (:::kind ... :::) with arbitrary kinds for project-specific callouts (note, tip, warning, danger, info, success, question, etc.) - Heading anchor ids — slugifyHeading() strips inline markdown, preserves unicode Inline features (all in InlineMarkdown, all code-span-safe): - Bare URL autolinks (https://...) with trailing-punctuation trimming - @mentions and #issue-refs — render as clickable links when repo is GitHub, styled spans otherwise; threaded via repoInfo.display through BlockRenderer - Emoji shortcodes (👋, 🚀, 29 curated codes) via transformPlainText() - Smart punctuation (curly quotes, em/en dashes, ellipsis) applied only to plain-text fragments after code spans have been consumed Safety: - Render-time transforms live inside InlineMarkdown's plain-text push, which is only reached after code-span regex consumes code content. Backticks stay literal for shell/regex snippets. - DOMPurify allowlist (no on* handlers, no style attrs, no scripts) gates every raw HTML block. Unsafe link protocols (javascript:/data:/vbscript:/file:) stripped by sanitizeLinkUrl. Tests: +40 (149 total). New files: - utils/slugify.test.ts (10) — unicode, markdown stripping, edge cases - utils/inlineTransforms.test.ts (9) — emoji + smartypants - utils/parser.test.ts — alert detection (5 cases), directives (5 cases), HTML block balancing (5 cases) Fixtures for manual verification: - tests/test-fixtures/11-html-blocks.md - tests/test-fixtures/12-gfm-and-inline-extras.md (release-plan-shaped demo) Known limitations (not blockers): - Bare URL regex doesn't balance parens (https://en.wikipedia.org/wiki/Foo_(bar) drops the trailing ")") - Duplicate heading text → duplicate anchor ids (browser picks first on hash nav) - Directive body is inline-only (no nested headings/lists) For provenance purposes, this commit was AI assisted. * fix(ui): wire typecheck for packages/ui, address PR review findings Root-cause fix for the missing-import bug caught in review: the UI package had no tsconfig.json and no typecheck script, so missing references like `getImageSrc` in the extracted InlineMarkdown slipped past vite/esbuild (which only type-strip, they don't resolve imports). Infrastructure: - Added packages/ui/tsconfig.json with proper module resolution, JSX config, and bundler-style paths. - Added globals.d.ts to accept side-effect CSS imports. - Added @types/react, @types/react-dom, @types/bun, @types/dompurify as devDeps on packages/ui so React / Bun / DOMPurify types actually resolve. - Wired `tsc --noEmit -p packages/ui/tsconfig.json` into the top-level `bun run typecheck` script. With the typecheck running, 0 errors remain in this PR's scope. Four pre-existing errors on main (plan-diff SVG type narrowing, sharing.ts SharePayload shape) are unrelated and tracked separately. Review fixes: - InlineMarkdown: import getImageSrc from ImageThumbnail. Was calling the helper without importing it — markdown images with relative paths (``) would throw ReferenceError at render. Regression caused by the InlineMarkdown extraction. - useAnnotationHighlighter: findTextInDOM now retries with the rendered form (transformPlainText) when the raw originalText doesn't match. Annotations made before smart-punctuation / emoji shortcodes shipped (straight quotes, `👋` text) still re-bind after reload. - sanitizeHtml: allow the `open` attribute so `<details open>` preserves its default-expanded state instead of always rendering collapsed. - parser.test.ts: narrow a string->AlertKind assertion to satisfy strict typechecking. Deferred (tracked as known limitation in PR description): - HtmlBlock relative URL rewriting for nested <img src="./logo.png"> / <a href="note.md">. New-feature gap, not a regression. For provenance purposes, this commit was AI assisted. * fix(ui): HtmlBlock rewrites relative <img>/<a> refs to match markdown paths Raw HTML blocks inject sanitized HTML verbatim, so nested <img src="./logo.png"> and <a href="notes.md"> resolved against the plannotator server URL instead of the plan's directory — images 404'd, .md links navigated away instead of opening in the linked-doc overlay. This is the path README.md content hits (hero <img>, YouTube thumbnails, <details> sections with anchors). Fix: after setting innerHTML, walk <img> and <a> elements and apply the same rewriting markdown content uses: - <img> relative src → getImageSrc(src, imageBaseDir), routing through /api/image?path=... with the plan's base directory. - <a> relative href matching .md / .mdx / .html → click handler that calls onOpenLinkedDoc, same pattern as [label](./foo.md) markdown links. - http(s):, data:, blob:, mailto:, tel:, and #anchor hrefs pass through untouched. BlockRenderer now threads imageBaseDir + onOpenLinkedDoc into HtmlBlock. React.memo equality extended to compare those props too, so legitimate changes still re-run the rewrite pass without forcing re-renders on every parent update. Verified against the repo's own README.md — hero image, YouTube thumbnails, and <details> sections all render correctly in annotate mode. For provenance purposes, this commit was AI assisted. * feat(ui): table conveniences — hover toolbar, popout dialog with sort/filter Extracts table rendering into blocks/TableBlock and adds two companion surfaces: a hover toolbar for quick copy, and a full-screen popout dialog with TanStack-powered sort/filter/copy for power use. No pagination — plan tables don't get that big. Hover toolbar (blocks/TableToolbar.tsx): - Floats above the table on mouse enter via React portal, positioned with getBoundingClientRect + scroll/resize listeners, entry/exit animations. Same positional pattern as AnnotationToolbar's top-right mode. - Debounced hover state in Viewer (100ms leave → 150ms exit animation), mirroring hoveredCodeBlock's state machine. - Three actions: Copy markdown (icon), CSV (short uppercase button, RFC 4180 escaping), Expand (opens popout). Popout dialog (blocks/TablePopout.tsx): - Radix Dialog, fullscreen-ish card with ~2rem backdrop visible for click-to-close. max-w-[min(calc(100vw-4rem),1500px)]. - Portaled into Viewer's containerRef so the annotation hook can walk into the popout's text nodes — selection-based annotations, text-search restoration, and shared blockId all work across the collapsed and popped-out views. - TanStack Table for the grid: click column headers to sort (asc → desc → clear), global filter input, no pagination. Row count indicator shows "15 of 27" when filter reduces the set. - Copy / CSV buttons in the header row: filter- and sort-aware. When visible rows < total, tooltips read "Copy 15 rows as markdown" / "Copy 15 rows as CSV". When no filter, copies whole table (normalized whitespace). Read is one-shot on click — no derived state to sync. - Floating X close button (absolute top-right), no header bar. Chrome stacking while popout is open (CSS-only, via :has()): - body:has([data-popout="true"]) drops four element types behind the dialog: annotation sidebar, sticky header lane, app nav header, overlay scrollbars. :has() observes the dialog's presence directly — when the dialog unmounts, the selector stops matching and everything returns to natural stacking. No JS state, no useEffect cleanup. Shared helpers in TableBlock.tsx (exported): - parseTableContent — pipe-delimited markdown → { headers, rows } - buildCsvFromRows / buildMarkdownTable — inverse, from parsed data - buildCsv — thin wrapper for the hover toolbar's raw-block path Dependencies added: - @radix-ui/react-dialog ^1.1.15 (~6 KB gzipped) - @tanstack/react-table ^8.21.3 (~14 KB gzipped) Fixture: - tests/test-fixtures/12-gfm-and-inline-extras.md — added a 27×11 "Detailed feature backlog" table to exercise wide + deep tables, horizontal scroll in the popout, and the sort/filter flows. For provenance purposes, this commit was AI assisted. * fix(ui): table popout — annotation flow, chrome stacking, sidebar tabs Tightens the popout so annotations work inside it and chrome doesn't overlap the dialog. Annotation flow inside popout: - Radix Dialog modal={false} so the focus trap doesn't yank focus back from CommentPopover's textarea (CommentPopover portals to document.body, outside the dialog's DOM subtree). - Dialog.Content onInteractOutside handler whitelists the annotation toolbar, CommentPopover, and FloatingQuickLabelPicker so clicking them doesn't dismiss the dialog. Backdrop click + Escape still close. - aria-describedby={undefined} on Dialog.Content (Radix opt-out; the popout doesn't need a description). - React.memo on TablePopout with a custom comparator (block id/content, open, container, imageBaseDir, githubRepo). Prevents upstream Viewer re-renders from re-running TanStack's flexRender on every cell, which conflicted with web-highlighter's live DOM mutations and caused a NotFoundError in React's reconciler. Widget markers for :has()-based chrome stacking: - [data-comment-popover="true"] on CommentPopover (both popover + dialog variants). - [data-floating-picker="true"] on FloatingQuickLabelPicker. - [data-sidebar-tabs="true"] on SidebarTabs (left-side TOC/Files/Versions flags that sit on top of the dialog otherwise). - theme.css extended: sidebar tabs join the annotation sidebar, sticky header lane, app header, and overlay scrollbars in dropping to z-index -1 while body:has([data-popout="true"]) matches. Known limitation (not addressed): annotations created inside the popout show their <mark> only while the popout is open; when it closes, the <mark> unmounts with the popout's DOM and does not reappear on the collapsed table. The annotation itself persists in state (sidebar, shared URLs, exports). Round-tripping visual marks between popout and collapsed view requires either a second web-highlighter instance or a switch to the CSS Custom Highlight API — out of scope here. For provenance purposes, this commit was AI assisted. * fix(ui): review findings — flags, alerts, forges, tabs, anchors, URL brackets Six targeted fixes from the v0.19 PR review. Each is small and scoped; the riskier items from the review (plan-diff block variants, HTML relative non-doc links) are tracked as follow-ups. Smart punctuation — CLI flags preserved: - Narrowed the `--` → en-dash rule to only fire between digits (`pages 3--5` still converts; `bun --watch` stays literal). GitHub alerts — list/code/heading bodies absorb correctly: - Blockquote merge now always merges into a previous alert blockquote, regardless of whether the new line starts with a block marker. Without this, `> [!NOTE]\n> - item` split the list off into a plain italic quote and emptied the alert. - AlertBlock got a mini block-level renderer for the body so `- item` / `* item` / `1. item` render as real <ul>/<ol>, not flattened prose. Forge-aware mentions/issue refs: - packages/shared/repo: new parseRemoteHost() extracts the host from the git remote URL; RepoInfo gains an optional `host` field. - packages/server/repo: getRepoInfo populates host alongside display. - Viewer only passes githubRepo to InlineMarkdown when the host is exactly "github.com". Non-GitHub repos render mentions/issue refs as styled text, no wrong github.com links. HTML block external links: - rewriteRelativeRefs now forces `target="_blank"` and `rel="noopener noreferrer"` on every external http(s) link inside raw HTML. Fixes two problems in one pass: external links no longer hijack the review tab, and pasted-HTML links can't tab-nab the plannotator tab via window.opener. Heading anchor dedup: - New buildHeadingSlugMap() walks all heading blocks and assigns `foo`, `foo-1`, `foo-2`, ... for repeats (GitHub convention). BlockRenderer receives the anchor id as a prop from Viewer via a memoized map rather than computing per-block; first occurrence keeps the bare slug so existing links stay stable. URL autolink bracket balance: - Trailing `)`/`]`/`}` in bare URLs are kept when they balance an earlier opener inside the URL. Wikipedia-style `https://en.wikipedia.org/wiki/Function_(mathematics)` now keeps its paren; `(see https://x.com)` still trims the orphan. Tests: +8 (157 total). - utils/slugify.test: buildHeadingSlugMap dedup behavior, non-heading skipping, empty-slug skipping. - utils/inlineTransforms.test: CLI flags stay literal, `3--5` still converts. - utils/parser.test: alerts with list body / code fence body, blank line ending an alert. Fixture: - tests/test-fixtures/13-known-issues.md — reproduces each of the review findings end-to-end; useful as a regression check going forward. Deferred (tracked for follow-up): - Plan diff view doesn't render html / directive / alertKind semantics (SimpleBlockRenderer has no cases for the new block variants). - Relative non-doc links inside raw HTML (.pdf, .csv) don't get rewritten — only .md/.mdx/.html are routed through the linked-doc overlay today. Not a regression; narrow audience. For provenance purposes, this commit was AI assisted. * fix(ui): round-3 review — drop host gate, link paren balance, data/blob images - Viewer: remove repoInfo.host === 'github.com' gate so @user/#123 links render for GitHub Enterprise and runtimes (Pi) that don't populate host. - HtmlBlock: treat protocol-relative //host links as external and harden with target=_blank rel=noopener noreferrer. - InlineMarkdown: data:/blob: image sources bypass /api/image rewrite. - InlineMarkdown: replace [text](url) regex with a depth-tracking scanner so URLs with balanced parens (Wikipedia /Function_(mathematics)) and backslash-escapes no longer truncate. Empty text/url guard preserves prior fall-through behavior. - InlineMarkdown: isLocalDoc accepts .md/.mdx/.html/.htm with optional #fragment; fragment stripped before onOpenLinkedDoc so guide.md#setup opens the linked doc instead of a broken anchor. For provenance purposes, this commit was AI assisted. * fix(ui): round-4 review — table pipe escape, callout lists, emoji h-splitter - TableBlock: buildMarkdownTable now re-escapes literal | as \| in each cell. parseTableContent already unescapes on parse; without the mirror on serialize, the popout's copy-as-markdown produces extra columns for tables with pipes in regex, shell, or boolean content. - AlertBlock + Callout: extract the shared paragraph-and-list body renderer into blocks/proseBody.tsx. Fixes directive callouts (:::note with a bulleted list) rendering as literal hyphens instead of a list. Paragraph lines join with '\n' so InlineMarkdown's hard-break handler still fires. Callout passes an empty text-color class so directive color tokens inherited from the container are preserved. - InlineMarkdown: drop `h` from the plaintext chunk-break class; it was splitting emoji shortcodes like ❤️, 👍, 🤔 at the h, so the :word: pattern never reassembled and transformPlainText couldn't replace the shortcode. Bare URL detection moves inline via emitPlainTextWithBareUrls, which scans chunks for https?:// at word boundaries and emits anchors, passing surrounding text through transformPlainText so emoji + smart punctuation still apply to non-URL slices. - InlineMarkdown: extract trimUrlTail (shared between the top-of-loop URL branch and the new inline scanner) — one balanced-paren trim implementation instead of two. +8 unit tests covering the trim cases (Wikipedia parens, unbalanced brackets, stacked punctuation). - Fixture: section 9 in 13-known-issues.md demonstrates the table copy corruption for manual verification. For provenance purposes, this commit was AI assisted. * fix(ui): resolve pre-existing typecheck errors surfacing in CI - PlanCleanDiffView: narrow heading Tag to 'h1'..'h6' so hover props resolve to HTMLHeadingElement instead of the SVGSymbolElement branch of keyof IntrinsicElements. - VSCodeIcon: spread mask-type as a kebab-case attribute; React 19's typings no longer expose the camelCase maskType prop on SVG masks. - useSharing / sharing: cast decompress() result to SharePayload — the shared compress module returns unknown by design; callers were implicitly any and TS 5.x now flags the assignment. For provenance purposes, this commit was AI assisted. |
||
|
|
4139999526 |
feat(plan-diff): word-level inline diff rendering (#565)
* feat(plan-diff): word-level inline diff rendering Two-pass hierarchical diff (diffLines outer + diffWordsWithSpace inner) so modified plan blocks render with inline insertions/deletions in context instead of showing the whole old block struck-through above the whole new block. Resolves #560. Engine (packages/ui/utils/planDiffEngine.ts): - computeInlineDiff runs a second-pass word diff on modified blocks that pass a whitelist gate (paragraph/heading/list-item with matching structural fields). - Sentinel substitution atomizes inline-code spans, markdown links, and fenced code blocks before diffWordsWithSpace runs, so diff markers never land inside backticks, link hrefs, or across fence boundaries. Fence regex uses a backreference so variable-length (e.g., 4-backtick wrapping 3-backtick) fences are matched atomically. - Annotation context for an inline-diffed modified block now captures both old and new content so comments on struck-through words preserve that text in the exported feedback. Renderer (packages/ui/components/plan-diff/PlanCleanDiffView.tsx): - New InlineModifiedBlock component renders a modified block as one structural wrapper with <ins>/<del> wrappers inside, parsed through the local InlineMarkdown in a single pass so markdown delimiter pairs survive across token boundaries. - InlineMarkdown extended to recognize <ins>/<del> tag passthrough (with recursive parsing of the wrapped content) and to recursively parse link anchor text so diff markers inside links render correctly. - Plain-text stop-char scanner includes '<' so <ins>/<del> dispatch re-enters the loop instead of swallowing tag text. - Click-to-annotate works in every editor mode (not just comment), with the block-level onClick opening the popover directly. Mode switcher (packages/ui/components/plan-diff/PlanDiffModeSwitcher.tsx): - Adds a third "Classic" tab between Rendered and Raw. Rendered is the new word-level default (labeled "exp"); Classic forces the legacy block-level stacked fallback for every modified block. Styling (packages/ui/theme.css, packages/editor/index.css): - plan-diff-word-added / plan-diff-word-removed utility classes for inline highlights with box-decoration-break: clone across line wraps. - Inline <code> inside the diff wrappers picks up a tinted background so code-pill changes read unambiguously green/red. - New plan-diff-modified class (amber border) for inline-diff modified blocks, matching the GitHub/VSCode convention of green=add, red=remove, yellow=both. Tests (packages/ui/utils/planDiffEngine.test.ts): - 18 tests covering the engine's qualification gate, structural-field matching, sentinel round-trip (inline code / links / fences), token content for common edit patterns. For provenance purposes, this commit was AI assisted. * chore(demo): restructure default demo, add VITE_DIFF_DEMO stress test Demo content changes that support the word-level diff work but do not alter shipped app behavior — only what other devs see running dev:hook. packages/editor/demoPlan.ts (default V3 editor content): - Added a "Context" section at the top of the plan with prose that showcases the word-level engine in V2→V3 diff: bold phrase swap, inline-code pill swaps, a link URL change, and a single-line code edit inside a config block. - Moved the mermaid architecture diagram and graphviz service map to an "Appendix: Diagrams" section at the end of the plan; they were rendering ugly mid-document. apps/hook/dev-mock-api.ts (Vite mock for the diff API): - PLAN_V1 / PLAN_V2 split into *_DEFAULT (original Real-time Collaboration plan — preserved identically from pre-branch state) and *_DIFF_TEST (the 20-case Auth Service Refactor diff-engine stress test, kept as an opt-in tool). - Resolves which pair to serve based on VITE_DIFF_DEMO env var. Matches the V2 Context section to the new V3 Context, with differences that produce rich word-level inline diffs on first load. - Diagrams moved to Appendix in V2_DEFAULT to match V3. packages/editor/App.tsx: - Both demo imports are active. VITE_DIFF_DEMO=1 swaps DIFF_DEMO_PLAN_CONTENT into the editor's default; unset renders the original Real-time Collaboration plan as before. packages/editor/demoPlanDiffDemo.ts (new): - 20-case stress test (paragraphs, headings, lists, tables, fences, blockquotes, known limitations). Each case has an identical "What to watch for" blockquote label in both V2 and V3 so the diff view cleanly isolates each case. Opt-in only. .gitignore: - Ignore .claude/ runtime lock/state files. Machine-specific content that should not be tracked. For provenance purposes, this commit was AI assisted. * style(plan-diff): refine modified-block visual — amber gutter, no fill Drop the yellow background fill from .plan-diff-modified and keep only a softened amber left border. Added/removed blocks remain loud (full fill + strong border) because add/remove are block-scope events — the whole block matters. Modify is a word-scope event — the individual changed words carry loud inline red/green highlights, and a block-level fill would compete with that inline work. The amber gutter at 75% opacity now reads as a quiet "look inside, the change is in the text" marker that sits coherently with the rest of the palette. For provenance purposes, this commit was AI assisted. * fix(plan-diff): sanitize link hrefs against javascript: / data: schemes PlanCleanDiffView has its own local copy of InlineMarkdown (separate from the one in Viewer.tsx). The link-rendering branch was passing the captured URL directly to href with no validation, so a plan containing [click me](javascript:alert(document.cookie)) would render as a live clickable anchor in the diff view. Plan content is attacker-influenced — Claude pulls from source comments, READMEs, fetched URLs — so this is a real exploit path in the diff flow. Port the same guard Viewer.tsx already has: sanitizeLinkUrl() rejects javascript:, data:, vbscript:, and file: schemes (case-insensitive, with optional leading whitespace). Rejected links render their anchor text as plain text instead of a clickable <a>, so the content is still visible to the reader but no longer dangerous. For provenance purposes, this commit was AI assisted. |
||
|
|
b3fc1f724f |
fix(ui): render numerals for ordered list items (#520)
* feat(parser): detect ordered list markers and compute display indices
The block parser collapsed `*`, `-`, and `\d+.` markers into a single
`list-item` block type, discarding ordered/unordered status. Add
`ordered` + `orderedStart` to Block, capture the numeric marker in the
list regex, and introduce `computeListIndices()` — a pure helper that
walks a list group and assigns each ordered item a CommonMark-correct
display number (sequential renumbering, streak break/restart on
unordered items, deeper-level state truncation, top-level numbering
preserved across nested children).
21 new unit tests cover both the parser changes and the indexing
helper, including the tricky cases: `1./2./99.` renumbers as 1,2,3;
sub-bullets between ordered items keep the top-level streak alive;
nested ordered sublists number independently and reset between
siblings; numeric checkboxes set both `ordered` and `checked`.
For provenance purposes, this commit was AI assisted.
* feat(ui): render numerals for ordered list items
Branch the list-item marker span on `block.ordered`: render
`${index}.` (with `tabular-nums` and a 1.5rem min-width to keep
columns stable across single- and double-digit numerals) when the
source marker was numeric, otherwise fall through to the existing
`•`/`◦`/`▪` bullet symbols. Indices come from `computeListIndices()`
called once per list group; `groupBlocks` is unchanged so mixed
nested lists still share a single `data-pinpoint-group="list"`
hover wrapper and annotation anchoring is unaffected. Checkbox
items still take precedence over numerals.
Adds a real-world manual fixture (06-ordered-list-plan.md) whose
`## Verification` section exercises a 10-item ordered list, the
case that originally surfaced the bug.
For provenance purposes, this commit was AI assisted.
* fix(parser): merge consecutive blockquote lines into one block
Each `>` line was emitted as its own blockquote block, so the
renderer's `my-4` margin produced visible gaps between every line
of a multi-line quote (the parser had a literal TODO comment:
"Check if previous was blockquote, if so, merge? No, separate for
now"). Fix: append to the previous block when it's a blockquote
and the prior line wasn't blank, mirroring the list-continuation
pattern. A blank line still breaks the quote so two `>` runs
separated by a blank line stay distinct.
Adds 5 unit tests (merge, blank-line break, paragraph boundaries,
single-line) and a manual fixture (07-blockquotes.md) covering the
bug case, the blank-line-break case, sandwich-between-paragraphs,
and inline markdown across merged lines.
For provenance purposes, this commit was AI assisted.
* fix(ui): address code review — diff view, task lists, blockquote paragraphs
Three issues surfaced by PR review #520:
1. **Diff view flattened ordered lists to bullets.** PlanCleanDiffView's
SimpleBlockRenderer duplicated Viewer's list-item JSX with hardcoded
bullet symbols, so a denied+resubmitted plan with numbered steps
showed numerals in the main view but `•` in the diff view — exactly
the screen where "which step changed?" matters most. Fixed by
threading computeListIndices through MarkdownChunk and sharing the
marker rendering via a new ListMarker component used by both
renderers, which also removes the root-cause duplication.
2. **Ordered task lists dropped their numbers.** `1. [ ] step` set both
`ordered=true` and `checked=false` in the parser, but the renderer's
checkbox branch took precedence and the numeral was never shown.
GitHub renders `1. [ ]` as numeral + checkbox side by side; we now
match that by rendering both glyphs in ListMarker when an ordered
task list item appears.
3. **Multi-paragraph blockquotes collapsed.** After the blockquote-merge
fix in the previous commit, `> a\n>\n> b` produced content
`"a\n\nb"` but the renderer passed it straight to InlineMarkdown,
which renders `\n\n` as whitespace — so two quoted paragraphs
mashed into one line. Fixed by splitting blockquote content on
`/\n\n+/` in both Viewer and PlanCleanDiffView and emitting one
`<p>` child per paragraph.
Adds one unit test for the multi-paragraph blockquote content shape and
a manual fixture (08-ordered-edge-cases.md) covering ordered task lists,
multi-paragraph quotes, nested-bullet counter preservation, double-digit
alignment, and start-at-N numbering.
The fourth review comment — loose ordered lists with intervening non-list
blocks restarting numbering — is deferred. It requires parser-level
loose-list detection (CommonMark's indented-continuation rule) and the
bug only fires when users rely on lazy `1./1./1.` markers across a
break. Tracked as a follow-up.
For provenance purposes, this commit was AI assisted.
* fix(parser): don't merge blockquote lines containing block-level markers
Round-two review flagged a regression: `> 1. foo\n> 2. bar\n> 3. baz` was
merging into one blockquote whose content was `"1. foo\n2. bar\n3. baz"`.
The renderer split on `\n\n+` (paragraph breaks), found none, and emitted
a single `<p>` — so `\n` collapsed to whitespace in HTML and the user
saw `"1. foo 2. bar 3. baz"` as one run-on line. Worse than the pre-PR
behavior (which at least kept each line in its own box).
Pragmatic fix: don't merge a `>` line whose stripped content starts with
a block-level marker (`*`, `-`, `\d+.`, `#`, `` ``` ``, `>`). Those stay
as separate blockquote blocks so each marker line is visually distinct
(legible, matching pre-PR behavior for quoted lists). Wrapped prose
quotes — the original motivating case — still merge correctly because
prose lines don't start with markers.
Also check the PREVIOUS block's content for markers so a trailing prose
line after a `> 1. item` doesn't glue onto the list-item block.
7 new unit tests cover: quoted ordered list stays separate, quoted
unordered list stays separate, quoted heading stays separate, quoted
code fence stays separate, nested blockquote stays separate, wrapped
prose quote still merges (regression guard), and mixed prose+list where
prose merges and list lines stay separate.
Adds tests/test-fixtures/09-quoted-list-regression.md as a manual repro.
Known follow-ups (tracked separately, not in this PR):
- Consecutive separate blockquote blocks still get individual `my-4`
margins, so a quoted list shows as stacked boxes with gaps between
lines. The proper fix is recursive blockquote parsing (render the
content as its own Block[] tree with an actual nested list inside
the quote). Deferred — requires `children?: Block[]` on Block,
parser rework, and exportAnnotations traversal changes.
- Clean diff view renumbers ordered lists from the start of each diff
chunk when users rely on CommonMark's lazy `1./1./1.` markers. Same
power-user population as the earlier deferred loose-list case.
- Pure code-hygiene items from the second review (non-list-block
handling in computeListIndices, BULLET_BY_LEVEL modulo cycle,
<ListGroup> extraction, CLAUDE.md Block interface drift,
splitBlockquoteParagraphs helper) — batch into a follow-up cleanup.
For provenance purposes, this commit was AI assisted.
|
||
|
|
b7af16a54c | fix: support underscore italics in markdown renderer (#504) | ||
|
|
d850b78ba6 |
fix: handle markdown hard line breaks and list continuations (#483)
* fix: handle markdown hard line breaks and list continuation lines List items with indented continuation lines (no blank line separator) now merge into the preceding bullet instead of becoming orphan paragraphs. InlineMarkdown now converts two-trailing-space and backslash line breaks into <br> elements. Synced to the diff view's InlineMarkdown copy. Closes #482 For provenance purposes, this commit was AI assisted. * fix: allow bold/italic to span across hard line breaks Changed bold/italic regexes from .+? to [\s\S]+? so emphasis can match across newlines (per CommonMark spec). Moved hard break check after all ^-anchored inline patterns so bold/italic get first crack, then the recursive InlineMarkdown call inside <strong>/<em> handles the break. For provenance purposes, this commit was AI assisted. |
||
|
|
2ae4f2a292 |
fix(parser): indented fences, trailing text, table detection, and escaped pipes (#429)
Three fixes to parseMarkdownToBlocks and one to table cell rendering: 1. Indented closing fences — allow leading whitespace so ` ``` ` inside list items closes the code block instead of swallowing to EOF. 2. Trailing text after closing fence — drop end-of-line anchor so ` ``` some text` still closes the block. 3. False table detection — require lines start with `|` instead of matching any line with 2+ pipe characters. 4. Escaped pipes in table cells — split on unescaped `|` only, so `\|` renders as a literal pipe instead of creating extra columns. Closes #427 For provenance purposes, this commit was AI assisted. |
||
|
|
93c0f035b3 |
feat: annotatable diff view with diff context in feedback
* refactor: extract useAnnotationHighlighter hook from Viewer Move annotation plumbing (web-highlighter lifecycle, toolbar/popover state, text-selection handlers, findTextInDOM, applyAnnotations) out of Viewer.tsx into a dedicated hook. Pure refactor — zero behavior change. Viewer consumes the hook and keeps its own code block, global comment, and pinpoint-specific logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: block-level diff annotation with diffContext support Add annotation support to plan diff view using block-level hover. Hovering added/removed/modified sections shows the annotation toolbar. No web-highlighter in diff mode — annotations live in React state only. - diffContext field on Annotation type (added/removed/modified) - PlanCleanDiffView: hover handlers, toolbar, comment/quicklabel flows - Annotated blocks show persistent highlight ring via content matching - View isolation: diff annotations filtered to diff view, normal to normal - Share/draft restore filters diff annotations from Viewer DOM - AnnotationPanel: neutral "diff" badge for diff annotations - Export: [In diff content] label in feedback - Toolstrip visible during diff mode for mode switching - CLAUDE.md updated Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: scroll-to-selected and visible highlight ring for diff annotations Add scroll-to-selected when clicking a diff annotation in the panel — scrolls to the block and briefly glows (same focused effect as Viewer). Replace invisible ring-1 ring-primary/20 with ring-2 ring-accent for annotated blocks so they're visually distinct. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * cleanup: memoize annotation filters, fix timer leaks, use blockId for highlight rings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: preserve normal annotations across diff toggle Replace ternary rendering with display:none so the Viewer stays mounted and web-highlighter DOM marks survive the toggle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: store full block content in diff annotations instead of truncating to 500 chars Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
2f5fd47f23 |
🐛 fix: render links inside bold/italic via recursive InlineMarkdown (#236)
Bold-wrapped links like **[text](url)** were rendered as plain bold text because the bold regex matched first and treated inner content as a string. Fix: recurse into InlineMarkdown for bold/italic inner content so nested markdown (links, code, etc.) is properly parsed. Affected: - Viewer.tsx (main plan view) - PlanCleanDiffView.tsx (diff view duplicate) |
||
|
|
819ba11f77 |
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes Shows what changed between plan iterations when Claude revises after feedback. Adds a +N/-M badge below repo info that toggles the diff view, a shared left sidebar with TOC and Version Browser tabs, and two diff modes: rendered (color-coded borders) and raw markdown (+/- lines). Closes #138, closes #111 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update CLAUDE.md project structure and align first-run dialog labels - Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md - Add new hooks and utils to CLAUDE.md project structure - Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup to match Settings.tsx label Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address code review findings for plan diff UX - Fix badge stats mixing block counts with line counts (modifications now fold into additions/deletions) - Gate hasPreviousVersion on diffBasePlan being loaded to prevent "Show Changes" no-op and ModeSwitcher disappearing - Make sidebar reactive to Settings toggle (useEffect on tocEnabled) - Match PlanDiffViewer badge layout to Viewer (flex-col) so badge doesn't jump position on toggle - Add "Exit Diff" label to the close button in diff view - Remove dead CSS (plan-diff-removed-marker, plan-diff-modified) - Clean up stale header comment and unused lines prop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: second-round review cleanup for plan diff UX - Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green) - Rename sidebar tab from "diff" to "versions" for clarity - Gate VersionBrowser fetch on versionInfo being available - Move .sidebar-tab-flag CSS into its own Sidebar section Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add loading state for version selection in sidebar Add isSelectingVersion to selectBaseVersion, mirroring the existing isLoadingVersions pattern. Shows "Loading..." on the selected version button while the fetch is in progress. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address third-round code review findings - Fix duplicate border/backdrop on TOC inside sidebar (className override) - Fix loading indicator targeting wrong version button (fetchingVersion state) - Fix "Show Changes" button silent no-op (gate on hasPreviousVersion) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: move date to slug suffix, improve Other Plans UX - Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD - Other Plans: single "coming soon" banner instead of per-item labels - Strip date suffix from plan names in sidebar for readability - Remove cursor-not-allowed from Other Plans items Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure - Document plan diff feature: engine, view modes, state management, sidebar - Update slug format documentation to {heading}-YYYY-MM-DD - Show native alert when version fetch fails instead of silent swallow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add table rendering to clean diff view Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |