Skip to content

Plan annotator#3043

Merged
azizmejri1 merged 9 commits intodyad-sh:mainfrom
azizmejri1:plan-annotator
Mar 24, 2026
Merged

Plan annotator#3043
azizmejri1 merged 9 commits intodyad-sh:mainfrom
azizmejri1:plan-annotator

Conversation

@azizmejri1
Copy link
Copy Markdown
Collaborator

@azizmejri1 azizmejri1 commented Mar 19, 2026

close #2989


Open with Devin

@wwwillchen
Copy link
Copy Markdown
Collaborator

@BugBot run

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the plan panel by introducing an interactive annotation system. Users can now provide targeted feedback on specific parts of an AI-generated plan, which can then be used to refine the plan. This feature aims to improve the collaborative and iterative process of plan development.

Highlights

  • Plan Annotation Feature: Introduced the ability for users to select text within the implementation plan and add comments to specific sections.
  • Comment Management: Implemented a sidebar to display all annotations for the current plan, allowing users to edit or delete their comments.
  • AI Integration for Plan Refinement: Added functionality to send collected comments back to the AI, prompting it to update the plan based on the feedback.
  • New UI Components: Created dedicated React components for the comment card, comment sidebar, and the floating selection comment button.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with πŸ‘ and πŸ‘Ž on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

gemini-code-assist[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

dyad-assistant[bot]

This comment was marked as resolved.

@dyad-assistant
Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: β›” NO - Do NOT merge

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

Issues Summary

Severity File Issue
πŸ”΄ HIGH PlanPanel.tsx:69 Annotations cleared synchronously before stream completes β€” data loss on failure
🟑 MEDIUM SelectionCommentButton.tsx:56 Floating UI can overflow the viewport on edge selections
🟑 MEDIUM SelectionCommentButton.tsx:79 Open comment form stays at stale position when scrolling
🟑 MEDIUM SelectionCommentButton.tsx:143 Icon-only buttons lack accessible labels
🟒 Low Priority Notes (0 items)

No low-priority items.

🚫 Dropped False Positives (8 items)
  • New selection silently updates selectedText β€” Dropped: Form is actually closed and reset on new selection via setShowForm(false)
  • getRangeAt(0) can throw if rangeCount is 0 β€” Dropped: Guarded by prior toString().trim().length === 0 check which returns early
  • isSubmitting state never reset β€” Dropped: Pre-existing issue, not introduced by this PR
  • editedText not synced on external changes β€” Dropped: Annotations are only edited by the same user in the same component; external changes are unlikely
  • Collapse state resets on unmount β€” Dropped: Correct behavior; sidebar should re-appear expanded so user sees new comments
  • Delete needs confirmation dialog β€” Dropped: Ephemeral in-memory data with low cost of accidental deletion
  • Redundant wrapper div β€” Dropped: Minor style preference, not a real issue
  • Duplicated style object β€” Dropped: Only 2 occurrences with 4 properties each, premature abstraction

Generated by Dyadbot multi-agent code review

@github-actions github-actions bot added the needs-human:review-issue ai agent flagged an issue that requires human review label Mar 19, 2026
@wwwillchen
Copy link
Copy Markdown
Collaborator

@BugBot run

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 21, 2026

Skipping Bugbot: Unable to authenticate your request. Please make sure Bugbot is properly installed and configured for this repository.

@dyad-assistant
Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: πŸ€” NOT SURE - Potential issues

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

Issues Summary

Severity File Issue
🟑 MEDIUM CommentPopover.tsx:40 Popover has no scroll handling β€” floats away from highlight
🟑 MEDIUM PlanPanel.tsx:52 chatAnnotations creates unstable array ref, causing unnecessary effect reruns
🟑 MEDIUM CommentsFloatingButton.tsx:30 PopoverTrigger wraps a <div>, not keyboard-focusable or labeled
🟒 Low Priority Notes (3 items)
  • Multiline blockquote in prompt β€” PlanPanel.tsx:110: > ${a.selectedText} only quotes the first line if selectedText contains newlines
  • Hardcoded ignore attribute β€” CodeHighlight.tsx hardcodes "data-plan-annotation-ignore" instead of importing PLAN_ANNOTATION_IGNORE_ATTRIBUTE from planAnnotationDom.ts
  • Mark elements lack keyboard accessibility β€” planAnnotationDom.ts:174: <mark> elements have no role="button", tabindex, or hover affordance indicating they're clickable
🚫 Dropped False Positives (6 items)
  • MutationObserver infinite loop β€” Dropped: The observer is disconnected before DOM mutations and reconnected after; requestAnimationFrame coalescing handles any re-triggering
  • Stale segments after highlightAtOffset β€” Dropped: Reverse iteration order ensures earlier segments' text nodes are unaffected; works correctly
  • Duplicate annotation lookup in CommentPopover β€” Dropped: Defensive code; the effect dismissal and render-time check serve different purposes
  • Unused React import β€” Dropped: Project uses explicit React imports consistently
  • getBoundaryTextOffset returns 0 fallback β€” Dropped: The fallback only triggers when boundary is at/before text start, which is correct
  • Layout jump when annotations added/removed β€” Dropped: The float-right + sticky pattern causes minimal reflow; acceptable tradeoff for the sticky behavior

Generated by Dyadbot multi-agent code review

dyad-assistant[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@dyad-assistant dyad-assistant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-agent review: 2 new issue(s) found (after deduplication against 50+ existing comments)


const prompt = currentAnnotations
.map(
(a, i) => `**Comment ${i + 1}:**\n> ${a.selectedText}\n\n${a.comment}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM | data-integrity

Stale annotations sent in prompt when plan content changes mid-session

handleSendComments reads annotations directly from the atom and includes all of them in the prompt, regardless of whether the plan text has changed since the annotation was created. The highlighting logic in applyPlanAnnotationHighlights already validates annotations against current text (actualText === annotation.selectedText) and skips stale ones β€” but this function has no equivalent staleness check.

If the plan content is revised (e.g. by a prior stream completing and updating currentPlan) while annotations still exist for that chatId, the prompt will reference old selectedText excerpts that no longer appear in the plan, sending misleading context to the LLM.

πŸ’‘ Suggestion: Filter currentAnnotations through the same text-match validation that applyPlanAnnotationHighlights uses before building the prompt β€” or add a check like segments + readPlanTextFromSegments to confirm each annotation still matches.

highlightNode.splitText(charsToHighlight);

const mark = document.createElement("mark");
const isFirstFragment = index === 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟒 LOW | edge-case

isFirstFragment can be wrong if an earlier segment is skipped

The reverse loop sets isFirstFragment = index === 0, which is correct when all segments produce a mark. However, if overlappingSegments[0] is skipped (due to charsToHighlight <= 0 or !textNode.parentNode), no mark in the annotation receives role="button" or tabindex="0". The annotation would be visible but unreachable by keyboard.

This is an unlikely edge case but worth noting. A fix would be to track whether any mark has been assigned the interactive attributes yet, rather than relying on index === 0.

@dyad-assistant
Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: βœ… YES - Ready to merge

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

Note: This PR already has 50+ review comments from prior rounds. This review focused exclusively on new issues not already covered.

The major bugs identified in earlier rounds (race condition/data loss in handleSendComments, viewport overflow, focus management) have been addressed in the latest commit. The code is well-structured with good accessibility attributes, proper cleanup in effects, and thorough test coverage.

Issues Summary

Severity File Issue
🟑 MEDIUM src/components/preview_panel/PlanPanel.tsx:138 Stale annotations sent in prompt when plan content changes mid-session
🟒 Low Priority Notes (5 items)
  • isFirstFragment can be wrong if segment is skipped - src/components/preview_panel/plan/planAnnotationDom.ts:177 β€” If an overlapping segment is skipped, no mark gets role="button", making the annotation unreachable by keyboard
  • Magic number 288 tied to Tailwind class w-72 - src/components/preview_panel/plan/SelectionCommentButton.tsx:99 β€” formWidth = 288 must stay manually synchronized with the w-72 class; consider deriving from DOM or using a shared constant
  • No user feedback when overlapping selection is silently rejected - src/components/preview_panel/plan/SelectionCommentButton.tsx:94 β€” The "Add comment" button simply does not appear with no explanation when selecting already-annotated text
  • Deleting last annotation abruptly unmounts CommentsFloatingButton - src/components/preview_panel/plan/CommentsFloatingButton.tsx:25 β€” Returns null when annotations empty, bypassing Radix Popover close animation and dropping focus
  • Inconsistent microcopy casing - src/components/preview_panel/plan/SelectionCommentButton.tsx β€” "Add Comment" (button label) vs "Add comment" (aria-label)
🚫 Dropped False Positives (4 items)
  • clampPosition closure captures stale popover coordinates β€” Dropped: React batches state updates and re-runs the effect with new deps; the race window is negligible
  • readPlanTextFromSegments fails for cross-block selections β€” Dropped: The offset calculation correctly handles block boundaries; the length mismatch check is a valid safety guard, not a bug
  • annotations.find() called twice per render in CommentPopover β€” Dropped: Negligible perf impact on a small array; the pattern is clear and idiomatic
  • Redundant setEditedText in handleCancel β€” Dropped: Explicit reset before state change is a defensive pattern that aids readability; harmless

Generated by Dyadbot multi-agent code review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee35c658f1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

refreshVersions();
invalidateTokenCount();
onSettled?.();
onSettled?.({ success: true });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat cancelled streams as unsuccessful in onSettled

I checked src/ipc/handlers/chat_stream_handlers.ts:1721-1738: cancelling a chat emits chat:response:end with wasCancelled: true, not an error. This callback still reports success: true, so PlanPanel.handleSendComments clears planAnnotationsAtom after a user-cancelled β€œSend Comments” run even though no revised plan was produced. In that cancel path the user loses all of their review comments and has nothing to retry from.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment on lines +72 to +77
const handleClick = (e: MouseEvent) => {
const target = e.target instanceof HTMLElement ? e.target : null;
const mark = target?.closest(ANNOTATION_MARK_SELECTOR) as HTMLElement;
if (!mark) return;

openPopoverForMark(mark);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop annotation clicks from activating underlying links

For plans that contain Markdown links or GFM autolinks, the highlight mark is inserted inside the <a> element, and VanillaMarkdownParser still wires link clicks to ipc.system.openExternalUrl(...) in src/components/chat/DyadMarkdownParser.tsx. Because this handler opens the comment popover without cancelling the event, clicking a commented link both opens the comment UI and follows the link, which makes linked text effectively impossible to review safely.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment on lines +62 to +64
const chatAnnotations = useMemo(
() => (chatId ? (annotations.get(chatId) ?? []) : []),
[chatId, annotations],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop annotations that no longer match a revised plan

I checked src/hooks/usePlanEvents.ts:57-67: every plan:update replaces the plan content for the same chat, but nothing prunes planAnnotationsAtom. Once the text shifts, applyPlanAnnotationHighlights silently stops rendering mismatched annotations, yet this component still uses the raw list for the badge/sidebar and overlap checks. After any manual plan revision or regeneration in the same chat, users can end up with invisible β€œghost” comments that are still sent back to the model and can block creating a new annotation at those offsets.

Useful? React with πŸ‘Β / πŸ‘Ž.

@azizmejri1
Copy link
Copy Markdown
Collaborator Author

πŸ€– Claude Code Review Summary

PR Confidence: 4/5

All review comments have been addressed with code changes; confidence is high but the onSettled signature change touches a shared hook used by multiple callers.

Unresolved Threads

No unresolved threads

Resolved Threads

Issue Rationale Link
P1: Preserve annotations on stream failure Added success flag to onSettled callback; annotations only cleared on success. Prevents data loss on failed plan updates. View
P2: Remount SelectionCommentButton on chat switch Added key={chatId} to reset local state when switching plan chats, preventing cross-chat annotation leakage. View
requestAnimationFrame not cancelled on cleanup Tracked rAF ID and cancel it in effect cleanup to prevent stale callbacks after unmount. View
data-annotation-id selector duplicated across files Extracted ANNOTATION_ID_ATTRIBUTE and ANNOTATION_MARK_SELECTOR constants from planAnnotationDom.ts and updated all usages across 3 files. View
Comment form Y position not clamped to viewport bottom Added Math.min(y, window.innerHeight - estimatedFormHeight - 8) clamping, matching the existing X-axis clamping pattern. View
No keyboard/touch selection support Added debounced selectionchange listener alongside mouseup, enabling annotation creation via keyboard (Shift+Arrow) and touch (long-press) selection. View
Product Principle Suggestions

No suggestions β€” product principles were clear enough for all decisions in this review.


πŸ€– Generated by Claude Code

Copy link
Copy Markdown
Collaborator

@wwwillchen wwwillchen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, see comment below to improve e2e test case and then OK to merge

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets update this e2e test case so it actually tests sending the annotation comments and makes sure the request being sent is correct.

@wwwillchen
Copy link
Copy Markdown
Collaborator

@BugBot run

Copy link
Copy Markdown
Contributor

@dyad-assistant dyad-assistant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-agent review: 2 new issue(s) found

>
<button
onClick={handleCommentClick}
aria-label="Add comment"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM | interaction-design

Floating comment button touch target is too small (~26px)

The button has p-1.5 padding around a 14px icon, producing roughly a 26Γ—26px click target. This is well below the recommended 44Γ—44px minimum (WCAG 2.5.8 / Apple HIG). Users on trackpads or touch-enabled devices will find it difficult to hit.

Similarly, the edit/delete buttons in CommentCard.tsx use p-1 with 12px icons (~20Γ—20px targets).

πŸ’‘ Suggestion: Increase to at least p-2 with a 16px icon on the floating button, and p-1.5 on the CommentCard action buttons. Alternatively, add an invisible hit-area expansion via a pseudo-element or negative margin.

}
clearPlanAnnotationHighlights(container);
};
}, [chatAnnotations, currentPlan]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM | complexity

~60 lines of DOM orchestration logic inline in useEffect

This highlight effect (lines 67–129) defines refreshHighlights, scheduleHighlightRefresh, creates a MutationObserver, and manages frameId / isApplyingHighlights state β€” all inline within the component. Extracting this into a custom hook (e.g., usePlanAnnotationHighlights(containerRef, annotations, currentPlan)) would improve PlanPanel readability and make the highlight logic independently testable.

@dyad-assistant
Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: βœ… YES - Ready to merge

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

This PR adds a solid plan annotation system with good accessibility (ARIA roles, keyboard navigation, focus management) and proper async handling via the new onSettled callback. Many issues from previous review rounds have been addressed in the latest revision.

Issues Summary

Severity File Issue
🟑 MEDIUM SelectionCommentButton.tsx:253 Floating comment button touch target too small (~26px vs 44px minimum)
🟑 MEDIUM PlanPanel.tsx:67-129 ~60 lines of highlight DOM orchestration inline in useEffect β€” extract to custom hook
🟒 Low Priority Notes (5 items)
  • Redundant chatId field on PlanAnnotation β€” planAtoms.ts: The field is redundant since annotations are already keyed by chatId in the Map. createdAt is also set but never read.
  • Magic numbers for form dimensions β€” SelectionCommentButton.tsx:99-100: 288 and 150 are implicit duplicates of the w-72 Tailwind class; extract as named constants.
  • Badge count not in aria-label β€” CommentsFloatingButton.tsx:31: Screen reader users hear "View comments" but not how many. Include count in aria-label.
  • No visual feedback when selection overlaps annotation β€” SelectionCommentButton.tsx:87: Button silently doesn't appear; a brief tooltip would reduce confusion.
  • Dialog popovers lack focus trapping β€” CommentPopover.tsx and SelectionCommentButton.tsx: Both have role="dialog" but Tab can escape to background elements.
🚫 Dropped False Positives (5 items)
  • useLayoutEffect clamping overridden by re-renders β€” Dropped: React's reconciler only updates DOM attributes when virtual DOM values change; unchanged popover.anchorX/Y won't overwrite clamped values.
  • getBoundaryTextOffset wrong offset in ignored regions β€” Dropped: The ignore attribute is only on code block headers, not annotation marks. Selections landing inside ignored regions are filtered upstream.
  • processSelection clears state while form is open β€” Dropped: The comment form is positioned outside the container element; mouseup/selectionchange handlers correctly check containment and won't fire for form interactions.
  • Comment form position stale after scroll β€” Dropped: The scroll handler dismisses the floating button when the form is not open, preventing stale positions.
  • clearPlanAnnotationHighlights fails with nested marks β€” Dropped: The overlap-prevention logic in applyPlanAnnotationHighlights ensures marks are never nested.

Generated by Dyadbot multi-agent code review

@github-actions
Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 259 3 10 6

Summary: 259 passed, 3 failed, 10 flaky, 6 skipped

Failed Tests

🍎 macOS

  • annotator.spec.ts > annotator - capture and submit screenshot
    • TimeoutError: locator.click: Timeout 120000ms exceeded.
  • github.spec.ts > create and sync to existing repo
    • Error: expect(locator).toMatchAriaSnapshot(expected) failed
  • template-create-nextjs.spec.ts > create next.js app
    • Error: expect(locator).toMatchAriaSnapshot(expected) failed

πŸ“‹ Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/annotator.spec.ts \
  e2e-tests/github.spec.ts \
  e2e-tests/template-create-nextjs.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • chat_tabs.spec.ts > right-click context menu: Close other tabs (passed after 1 retry)
  • context_manage.spec.ts > manage context - smart context (passed after 1 retry)
  • context_manage.spec.ts > manage context - exclude paths (passed after 1 retry)
  • local_agent_file_upload.spec.ts > local-agent - upload file to codebase (passed after 1 retry)
  • refresh.spec.ts > preview navigation - forward and back buttons work (passed after 1 retry)
  • select_component.spec.ts > select component (passed after 1 retry)
  • select_component.spec.ts > deselect individual component from multiple (passed after 1 retry)
  • setup_flow.spec.ts > Setup Flow > setup banner shows correct state when node.js is installed (passed after 1 retry)
  • setup.spec.ts > setup ai provider (passed after 1 retry)
  • undo.spec.ts > undo (passed after 1 retry)

πŸ“Š View full report

azizmejri1 and others added 9 commits March 24, 2026 03:10
- Fix race condition in handleSendComments: await streamMessage, clear annotations only on success
- Add viewport clamping for floating comment button/form positioning
- Dismiss floating UI on scroll even when form is open
- Add scroll dismiss handler to CommentPopover
- Add aria-labels to icon-only buttons (comment, edit, delete, view comments)
- Add keyboard shortcut hint to comment form
- Add autoFocus and keyboard shortcuts to edit textarea in CommentCard
- Sync editedText state with annotation.comment prop via useEffect
- Stabilize chatAnnotations reference with useMemo to prevent unnecessary MutationObserver cycles
- Use native button element for CommentsFloatingButton popover trigger
- Show "Sending..." label on send button during submission

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- preserve plan annotation drafts during scroll and add cancel affordances
- improve plan comment accessibility, focus handling, and popover positioning
- reduce unnecessary highlight refreshes and extend plan annotation coverage

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Fix focus effect re-stealing focus during editing by depending on annotationId only
- Apply clamped popover position directly to DOM to avoid redundant re-renders
- Reposition popover on scroll instead of dismissing it
- Use onSettled callback for clearing annotations after stream completes
- Clamp x-coordinate to prevent off-screen positioning
- Pass chatAnnotations as prop to avoid duplicated atom derivation
- Add type="button" to icon action buttons
- Disable Save button when comment is whitespace-only
- Extract annotation Map mutation helpers to reduce duplication
- Add JSDoc comment to getBoundaryTextOffset algorithm

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Preserve annotations on stream failure by only clearing on success via onSettled result
- Add key={chatId} to SelectionCommentButton to reset state on chat switch
- Cancel requestAnimationFrame on effect cleanup to prevent resource leaks
- Extract ANNOTATION_ID_ATTRIBUTE and ANNOTATION_MARK_SELECTOR constants
- Clamp comment form Y position to prevent overflow below viewport
- Add selectionchange listener for keyboard and touch selection support

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@wwwillchen
Copy link
Copy Markdown
Collaborator

@BugBot run

@dyad-assistant
Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: βœ… YES - Ready to merge

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

Most significant issues from prior reviews (race condition on annotation clearing, missing accessibility attributes, popover focus management) have already been addressed in the latest revision.

Issues Summary

Severity File Issue
🟑 MEDIUM src/components/preview_panel/PlanPanel.tsx:138 Multi-line selectedText breaks markdown blockquote in prompt
🟑 MEDIUM src/components/preview_panel/plan/CommentPopover.tsx:101 Escape key closes popover AND cancels edit simultaneously
🟒 Low Priority Notes (10 items)
  • readPlanTextFromSegments returns null when text spans ignored regions β€” planAnnotationDom.ts:117 β€” Annotations across code block headers silently disappear; may be intentional but undocumented
  • MutationObserver observed before first highlight refresh β€” PlanPanel.tsx:73 β€” Redundant initial observe call; harmless but slightly wasteful
  • Selection validation duplicated in handleSelectionChange β€” SelectionCommentButton.tsx:139 β€” selectionchange handler re-checks conditions that processSelection already validates
  • Viewport clamping via useLayoutEffect fights with style prop β€” CommentPopover.tsx:171 β€” Two competing positioning strategies (declarative and imperative)
  • Scroll handler re-queries DOM for mark element β€” CommentPopover.tsx:127 β€” Could use triggerRef.current instead of querySelector on every scroll
  • No visual feedback when selection overlaps annotation β€” SelectionCommentButton.tsx:87 β€” Button silently doesn't appear; user doesn't know why
  • Comment count badge missing from aria-label β€” CommentsFloatingButton.tsx:35 β€” Screen readers hear "View comments" but not how many
  • Textarea lacks a persistent accessible label β€” SelectionCommentButton.tsx:277 β€” Placeholder disappears after typing; no label for screen readers
  • Comments floating button overlaps plan content β€” PlanPanel.tsx:172 β€” Sticky button can obscure text underneath on narrow viewports
  • updatePlanAnnotation creates empty map entry for missing chatId β€” planAtoms.ts:56 β€” Should no-op instead of inserting empty array
🚫 Dropped False Positives (3 items)
  • processSelection resets form on selectionchange β€” Dropped: The textarea is fixed-positioned outside the plan container, so mouseup/selectionchange handlers won't fire for it
  • No focus trap in popover/form dialogs β€” Dropped: These are non-modal popovers with Escape-to-dismiss; focus traps are not required for this pattern and would be over-constraining
  • Hardcoded Tailwind classes in DOM manipulation β€” Dropped: This is standard practice for imperative DOM code in this codebase; Tailwind's content scanner handles string literals in .ts files

Generated by Dyadbot multi-agent code review

Copy link
Copy Markdown
Contributor

@dyad-assistant dyad-assistant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-agent review: 2 medium issue(s) found, 10 low-priority notes


const prompt = currentAnnotations
.map(
(a, i) => `**Comment ${i + 1}:**\n> ${a.selectedText}\n\n${a.comment}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM | data-integrity

Multi-line selectedText breaks markdown blockquote in prompt

When a.selectedText contains newlines (e.g., user selected text spanning multiple paragraphs), the blockquote formatting > ${a.selectedText} only prefixes the first line with >. Subsequent lines appear outside the blockquote, corrupting the prompt structure and making it ambiguous whether those lines are quoted text or comment text.

πŸ’‘ Suggestion: Replace newlines in selectedText with \n> to continue the blockquote:

`> ${a.selectedText.replace(/\n/g, '\n> ')}`

};

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") dismiss({ restoreFocus: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM | interaction

Escape key closes popover AND cancels edit simultaneously

When the CommentCard's edit textarea is active inside this popover, pressing Escape triggers both the textarea's onKeyDown handler (which calls handleCancel to exit edit mode) and this document-level keydown listener (which calls dismiss() to close the entire popover). The user expects Escape to cancel the edit first, then a second Escape to close the popover, but instead both happen at once.

πŸ’‘ Suggestion: In CommentCard's onKeyDown handler for Escape, add e.stopPropagation() to prevent the event from reaching the document-level listener.

@github-actions
Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 259 3 6 6

Summary: 259 passed, 3 failed, 6 flaky, 6 skipped

Failed Tests

🍎 macOS

  • context_manage.spec.ts > manage context - exclude paths with smart context
    • Error: expect(locator).toMatchAriaSnapshot(expected) failed
  • local_agent_generate_image.spec.ts > local-agent - generate image
    • TimeoutError: locator.click: Timeout 30000ms exceeded.
  • select_component.spec.ts > select component next.js
    • Error: expect(locator).toBeVisible() failed

πŸ“‹ Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/context_manage.spec.ts \
  e2e-tests/local_agent_generate_image.spec.ts \
  e2e-tests/select_component.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • annotator.spec.ts > annotator - capture and submit screenshot (passed after 1 retry)
  • context_limit_banner.spec.ts > context limit banner does not appear when within limit (passed after 1 retry)
  • local_agent_consent.spec.ts > local-agent - add_dependency consent: always allow (passed after 1 retry)
  • setup_flow.spec.ts > Setup Flow > setup banner shows correct state when node.js is installed (passed after 1 retry)
  • setup.spec.ts > setup ai provider (passed after 1 retry)
  • switch_versions.spec.ts > switch versions (isomorphic git) (passed after 1 retry)

πŸ“Š View full report

@azizmejri1 azizmejri1 merged commit ed87736 into dyad-sh:main Mar 24, 2026
7 of 9 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 396 2 4 127
πŸͺŸ Windows 397 6 5 127

Summary: 793 passed, 8 failed, 9 flaky, 254 skipped

Failed Tests

🍎 macOS

  • queued_message.spec.ts > editing queued message restores attachments and selected components
    • Error: expect(locator).toBeVisible() failed
  • queued_message.spec.ts > canceling queued message edit clears restored components
    • Error: expect(locator).toBeVisible() failed

πŸͺŸ Windows

  • concurrent_chat.spec.ts > concurrent chat
    • Error: expect(locator).toBeVisible() failed
  • github.spec.ts > create and sync to new repo
    • Error: expect(locator).toHaveClass(expected) failed
  • github.spec.ts > create and sync to new repo - custom branch
    • TimeoutError: locator.click: Timeout 30000ms exceeded.
  • github.spec.ts > create and sync to existing repo
    • Error: expect(locator).toMatchAriaSnapshot(expected) failed
  • github.spec.ts > create and sync to existing repo - custom branch
    • Error: expect(locator).toMatchAriaSnapshot(expected) failed
  • setup_flow.spec.ts > Setup Flow > node.js install flow
    • TimeoutError: locator.click: Timeout 30000ms exceeded.

πŸ“‹ Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/queued_message.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • setup_flow.spec.ts > Setup Flow > setup banner shows correct state when node.js is installed (passed after 1 retry)
  • setup_flow.spec.ts > Setup Flow > node.js install flow (passed after 1 retry)
  • smart_context_balanced.spec.ts > smart context balanced - simple (passed after 1 retry)
  • template-create-nextjs.spec.ts > create next.js app (passed after 1 retry)

πŸͺŸ Windows

  • chat_mode.spec.ts > chat mode selector - ask mode (passed after 1 retry)
  • context_limit_banner.spec.ts > context limit banner shows 'running out' when near context limit (passed after 1 retry)
  • context_manage.spec.ts > manage context - exclude paths with smart context (passed after 1 retry)
  • github.spec.ts > github clear integration settings (passed after 1 retry)
  • setup_flow.spec.ts > Setup Flow > setup banner shows correct state when node.js is installed (passed after 1 retry)

πŸ“Š View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-human:review-issue ai agent flagged an issue that requires human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plan annotator

2 participants