Skip to content

feat(ui): highlight external SSE annotations inline on plan text#511

Merged
backnotprop merged 3 commits intomainfrom
feat/sse-agent-instructions
Apr 7, 2026
Merged

feat(ui): highlight external SSE annotations inline on plan text#511
backnotprop merged 3 commits intomainfrom
feat/sse-agent-instructions

Conversation

@backnotprop
Copy link
Copy Markdown
Owner

Summary

  • External annotations posted via `POST /api/external-annotations` now optionally highlight the matching `originalText` span inline in the rendered plan (`COMMENT` → yellow, `DELETION` → strikethrough). Annotations without `originalText` or of type `GLOBAL_COMMENT` continue to render sidebar-only.
  • New focused hook `useExternalAnnotationHighlights` drives the Viewer's existing imperative `applySharedAnnotations` / `removeHighlight` API, reusing the same DOM text-search path that share-URL restoration uses. No protocol or schema change; App.tsx gains only a single hook call.
  • Tracks applied ids with a `type+originalText` fingerprint so SSE updates correctly remove+reapply; clears bookkeeping only on plan markdown change (where blocks re-render); early-returns while diff view or a linked doc overlay is active so SSE removals that arrive under those conditions still reconcile when the hook re-enables.

Contract

No schema change to `transformPlanInput`. Tools choose inline vs global purely by what they supply:

Inline (highlighted on real plan text):
```json
{ "source": "my-tool", "type": "COMMENT",
"text": "This assumption is wrong because...",
"originalText": "deterministic non-git cold-start MVP" }
```

Global (sidebar-only):
```json
{ "source": "my-tool", "type": "GLOBAL_COMMENT",
"text": "Consider adding a rollback step." }
```

If `originalText` is not found in the rendered DOM the annotation degrades gracefully to sidebar-only (warning logged, no error response). First match wins — tools can disambiguate repeated phrases with more surrounding context.

Test plan

  • POST a `COMMENT` with `originalText` matching a phrase in the current plan → phrase highlights yellow + sidebar entry appears
  • POST a `DELETION` → phrase renders with strikethrough
  • `DELETE /api/external-annotations?id=` → highlight and sidebar entry both vanish
  • `PATCH` with a new `originalText` → old highlight removed, new one applied
  • POST with `originalText` that doesn't exist → degrades to sidebar-only (warning in console)
  • POST `GLOBAL_COMMENT` → sidebar-only, no DOM mark
  • Click an inline external highlight → sidebar entry becomes selected
  • Open diff view, DELETE an external via API, close diff view → stale mark is cleaned up on re-enable
  • Switch plan version (deny → resubmit) → externals re-apply against the new plan, or drop cleanly if their text no longer exists

External annotations posted via /api/external-annotations previously
appeared in the sidebar only. They now highlight the matching `originalText`
span in the rendered plan, giving tools (linters, agents) an optional way
to attach feedback to specific phrases — while `GLOBAL_COMMENT` (or any
annotation without `originalText`) still degrades to sidebar-only.

Implementation is a new focused hook that drives the Viewer's existing
imperative `applySharedAnnotations` / `removeHighlight` API, reusing the
same DOM text-search path that share-URL restoration uses. No protocol
change, no schema change to `transformPlanInput`, and App.tsx gains only
a single hook call.

The hook tracks applied ids with a type+originalText fingerprint so SSE
updates correctly remove+reapply, clears its bookkeeping only on plan
markdown change (where blocks re-render), and early-returns (preserving
state) while diff view or a linked doc overlay is active so SSE removals
arriving under those conditions still reconcile when the hook re-enables.

For provenance purposes, this commit was AI assisted.
…ad code

Addresses three review findings on the external annotation highlight hook:

- Share-import wipe: `importFromShareUrl` merges annotations without
  changing `markdown`, so our `planKey` stayed stable. The share-apply
  effect in App.tsx calls `clearAllHighlights()` to reset the DOM before
  applying imported annotations — which also wiped live external SSE
  highlights. The hook believed they were still painted and never re-drove
  them, leaving sidebar entries with no visible highlight until the next
  SSE event. Fix: hook now exposes a `reset()` that clears its applied-set
  and re-runs the main effect via a counter; App.tsx calls it right after
  `clearAllHighlights()` in the share-apply path.

- Removed a dead `nextIds.has(a.id)` guard inside the apply timer callback.
  `toAdd` is computed as a subset of `eligible`, and `nextIds` was built
  from `eligible.map(.id)`, so the guard was vacuously always true.

- Removed the stable `viewerRef` from the main effect's dep array; React
  ref objects have stable identity for the component's lifetime so it was
  noise. Added a comment noting the intentional omission.

For provenance purposes, this commit was AI assisted.
For provenance purposes, this commit was AI assisted.
@backnotprop backnotprop merged commit 91f57ad into main Apr 7, 2026
5 checks passed
@backnotprop backnotprop deleted the feat/sse-agent-instructions branch April 7, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant