Contribute rewrite Phase 3: tracked-change overlay in MarkdownPreview
Reintroduces the §8.10 inline tracked-change layer Phase 1 dropped when
Tiptap left, this time anchored to the rendered preview rather than a
writable editor. Each accepted change on the branch decorates the
preview DOM with a `<span class="tracked-insert">` at the proposed text
and a `<span class="tracked-delete">` strikethrough for the original;
hover surfaces the same ChangeTooltip DiffView uses (badge + prompt +
quote + reason), now extracted to its own file so both surfaces share
the affordance.
Design calls per the Phase 3 prompt's open list:
• Contribute pane: default-on. The pane exists for editorial review
of your own work; the overlay reinforces that.
• Discuss pane: opt-in via a new "Show tracked changes" toolbar
toggle on non-main branches. Clean prose stays the default.
• DiffView's "Review changes" toolbar is untouched — DiffView dies in
Phase 7 and overloading its toggle now creates surface to unwind.
Pre-fancy stance on the known overlay subtleties:
• Drift — if `proposed` no longer appears verbatim (later manual
edit touched it) we skip the decoration rather than mis-anchor.
• Overlap — decorate in `acted_at` ascending order; later spans win
on whatever text is still un-wrapped.
• Mermaid — text nodes inside `.mermaid-block` are skipped; a tracked
span that intersects diagram source drops silently. Flagged as a
known limitation rather than worked around.
• Code — `<pre>`/`<code>` parents skipped too; matching inside
verbatim code produced noisy false positives in fixtures.
Backend tests: 125 passed. Helper verified via Vite preview sandbox
eval across eight cases (single-paragraph match, distinct-original
strike, no-match skip, mermaid skip, two-change overlap ordering,
declined/pending ignored, whitespace-normalized cross-newline match,
code-block skip), plus computed-style proof for the new
`.markdown-preview .tracked-insert` / `.tracked-delete` rules. The
end-to-end CM6 → accept → overlay flow against a live branch wasn't
exercised (backend not running this session); the simpler unit-level
verification looked clean, but a future session with the backend up
should drive that golden path before Phase 4 piles on top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// ChangeTooltip.jsx — the §8.10 hover affordance for a tracked-change
|
||||
// span. Extracted from DiffView in Phase 3 so the MarkdownPreview
|
||||
// tracked-changes overlay can reuse it. Same shape: badge row,
|
||||
// user-prompt + selection-quote, and the AI's reason.
|
||||
|
||||
import { MODEL_STYLES } from '../modelStyles'
|
||||
|
||||
function tooltipStyle(x, y) {
|
||||
const vw = window.innerWidth, vh = window.innerHeight
|
||||
const style = {}
|
||||
if (x > vw * 0.55) style.right = vw - x + 10
|
||||
else style.left = x + 14
|
||||
if (y > vh * 0.55) style.bottom = vh - y + 10
|
||||
else style.top = y + 14
|
||||
return style
|
||||
}
|
||||
|
||||
export function changeContext(change, messages) {
|
||||
if (!change?.source_message_id || !messages) return {}
|
||||
const idx = messages.findIndex(m => m.id === change.source_message_id)
|
||||
if (idx < 0) return {}
|
||||
const assistant = messages[idx]
|
||||
const userMsg = [...messages].slice(0, idx).reverse().find(m => m.role === 'user')
|
||||
return { assistant, userMsg }
|
||||
}
|
||||
|
||||
export default function ChangeTooltip({ change, messages, position }) {
|
||||
const { assistant, userMsg } = changeContext(change, messages)
|
||||
const style = { ...tooltipStyle(position.x, position.y), position: 'fixed' }
|
||||
const modelStyle = MODEL_STYLES[assistant?.model_id] || MODEL_STYLES.default
|
||||
|
||||
return (
|
||||
<div className="diff-tooltip" style={style}>
|
||||
<div className="diff-tooltip-header">
|
||||
{change.kind === 'ai' ? (
|
||||
<span className="diff-tooltip-badge" style={{ background: modelStyle.bg, color: modelStyle.color }}>
|
||||
{modelStyle.label}
|
||||
</span>
|
||||
) : (
|
||||
<span className="diff-tooltip-badge diff-tooltip-badge--manual">Manual edit</span>
|
||||
)}
|
||||
{change.was_edited_before_accept && (
|
||||
<span className="diff-tooltip-badge diff-tooltip-badge--edited">Edited before accept</span>
|
||||
)}
|
||||
</div>
|
||||
{userMsg && (
|
||||
<div className="diff-tooltip-prompt">
|
||||
{userMsg.quote && (
|
||||
<div className="diff-tooltip-quote">
|
||||
"{userMsg.quote.length > 120 ? userMsg.quote.slice(0, 120) + '…' : userMsg.quote}"
|
||||
</div>
|
||||
)}
|
||||
<div className="diff-tooltip-prompt-text">
|
||||
{userMsg.text.length > 240 ? userMsg.text.slice(0, 240) + '…' : userMsg.text}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{change.reason && (
|
||||
<div className="diff-tooltip-reason">
|
||||
<span className="diff-tooltip-reason-label">Reason</span>
|
||||
{change.reason}
|
||||
</div>
|
||||
)}
|
||||
{!userMsg && change.kind === 'ai' && (
|
||||
<div className="diff-tooltip-no-context">
|
||||
No linked conversation message in this session.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user