Commit 4139999
authored
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.1 parent 7245546 commit 4139999
File tree
13 files changed
+1567
-127
lines changed- apps/hook
- packages
- editor
- ui
- components/plan-diff
- utils
13 files changed
+1567
-127
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
41 | 45 | | |
42 | 46 | | |
43 | 47 | | |
| |||
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | | - | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
69 | 80 | | |
70 | 81 | | |
71 | 82 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
3 | 18 | | |
4 | 19 | | |
5 | 20 | | |
| |||
58 | 73 | | |
59 | 74 | | |
60 | 75 | | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | 76 | | |
99 | 77 | | |
100 | 78 | | |
| |||
330 | 308 | | |
331 | 309 | | |
332 | 310 | | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
333 | 352 | | |
334 | 353 | | |
0 commit comments