Răsfoiți Sursa

fix(app): line selection fixes

adamelmore 1 lună în urmă
părinte
comite
6d8e994383

+ 33 - 4
packages/app/src/components/prompt-input.tsx

@@ -170,6 +170,36 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const tabs = createMemo(() => layout.tabs(sessionKey))
   const view = createMemo(() => layout.view(sessionKey))
 
+  const commentInReview = (path: string) => {
+    const sessionID = params.id
+    if (!sessionID) return false
+
+    const diffs = sync.data.session_diff[sessionID]
+    if (!diffs) return false
+    return diffs.some((diff) => diff.file === path)
+  }
+
+  const openComment = (item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }) => {
+    if (!item.commentID) return
+
+    comments.setFocus({ file: item.path, id: item.commentID })
+    view().reviewPanel.open()
+
+    if (item.commentOrigin === "review") {
+      tabs().open("review")
+      return
+    }
+
+    if (item.commentOrigin !== "file" && commentInReview(item.path)) {
+      tabs().open("review")
+      return
+    }
+
+    const tab = files.tab(item.path)
+    tabs().open(tab)
+    files.load(item.path)
+  }
+
   const recent = createMemo(() => {
     const all = tabs().all()
     const active = tabs().active()
@@ -1481,6 +1511,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             selection: item.selection,
             comment: item.comment,
             commentID: item.commentID,
+            commentOrigin: item.commentOrigin,
             preview: item.preview,
           })
         }
@@ -1547,6 +1578,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           selection: item.selection,
           comment: item.comment,
           commentID: item.commentID,
+          commentOrigin: item.commentOrigin,
           preview: item.preview,
         })
       }
@@ -1700,10 +1732,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                         "cursor-pointer hover:bg-surface-interactive-weak": !!item.commentID,
                       }}
                       onClick={() => {
-                        if (!item.commentID) return
-                        comments.setFocus({ file: item.path, id: item.commentID })
-                        view().reviewPanel.open()
-                        tabs().open("review")
+                        openComment(item)
                       }}
                     >
                       <div class="flex items-center gap-1.5">

+ 1 - 0
packages/app/src/context/prompt.tsx

@@ -44,6 +44,7 @@ export type FileContextItem = {
   selection?: FileSelection
   comment?: string
   commentID?: string
+  commentOrigin?: "review" | "file"
   preview?: string
 }
 

+ 107 - 104
packages/app/src/pages/session.tsx

@@ -27,6 +27,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { useCodeComponent } from "@opencode-ai/ui/context/code"
+import { LineCommentAnchor } from "@opencode-ai/ui/line-comment"
 import { SessionTurn } from "@opencode-ai/ui/session-turn"
 import { createAutoScroll } from "@opencode-ai/ui/hooks"
 import { SessionReview } from "@opencode-ai/ui/session-review"
@@ -535,6 +536,7 @@ export default function Page() {
     selection: SelectedLineRange
     comment: string
     preview?: string
+    origin?: "review" | "file"
   }) => {
     const selection = selectionFromLines(input.selection)
     const preview = input.preview ?? selectionPreview(input.file, selection)
@@ -549,6 +551,7 @@ export default function Page() {
       selection,
       comment: input.comment,
       commentID: saved.id,
+      commentOrigin: input.origin,
       preview,
     })
   }
@@ -1463,7 +1466,7 @@ export default function Page() {
                                 diffs={diffs}
                                 view={view}
                                 diffStyle="unified"
-                                onLineComment={addCommentToContext}
+                                onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
                                 comments={comments.all()}
                                 focusedComment={comments.focus()}
                                 onFocusedCommentChange={comments.setFocus}
@@ -1782,7 +1785,7 @@ export default function Page() {
                                 view={view}
                                 diffStyle={layout.review.diffStyle()}
                                 onDiffStyleChange={layout.review.setDiffStyle}
-                                onLineComment={addCommentToContext}
+                                onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
                                 comments={comments.all()}
                                 focusedComment={comments.focus()}
                                 onFocusedCommentChange={comments.setFocus}
@@ -1974,6 +1977,22 @@ export default function Page() {
                       requestAnimationFrame(() => textarea?.focus())
                     })
 
+                    createEffect(() => {
+                      const focus = comments.focus()
+                      const p = path()
+                      if (!focus || !p) return
+                      if (focus.file !== p) return
+                      if (activeTab() !== tab) return
+
+                      const target = fileComments().find((comment) => comment.id === focus.id)
+                      if (!target) return
+
+                      setOpenedComment(target.id)
+                      setCommenting(null)
+                      file.setSelectedLines(p, target.selection)
+                      requestAnimationFrame(() => comments.clearFocus())
+                    })
+
                     const renderCode = (source: string, wrapperClass: string) => (
                       <div
                         ref={(el) => {
@@ -2016,125 +2035,109 @@ export default function Page() {
                         />
                         <For each={fileComments()}>
                           {(comment) => (
-                            <div
-                              class="absolute right-6 z-30"
-                              style={{
-                                top: `${positions()[comment.id] ?? 0}px`,
-                                opacity: positions()[comment.id] === undefined ? 0 : 1,
-                                "pointer-events": positions()[comment.id] === undefined ? "none" : "auto",
+                            <LineCommentAnchor
+                              id={comment.id}
+                              top={positions()[comment.id]}
+                              open={openedComment() === comment.id}
+                              onMouseEnter={() => {
+                                const p = path()
+                                if (!p) return
+                                file.setSelectedLines(p, comment.selection)
+                              }}
+                              onClick={() => {
+                                const p = path()
+                                if (!p) return
+                                setCommenting(null)
+                                setOpenedComment((current) => (current === comment.id ? null : comment.id))
+                                file.setSelectedLines(p, comment.selection)
                               }}
                             >
-                              <button
-                                type="button"
-                                class="size-5 rounded-md flex items-center justify-center shadow-xs focus:outline-none focus-visible:shadow-xs-border-focus"
-                                style={{
-                                  background: "var(--icon-interactive-base)",
-                                }}
-                                onMouseEnter={() => {
-                                  const p = path()
-                                  if (!p) return
-                                  file.setSelectedLines(p, comment.selection)
-                                }}
-                                onClick={() => {
-                                  const p = path()
-                                  if (!p) return
-                                  setCommenting(null)
-                                  setOpenedComment((current) => (current === comment.id ? null : comment.id))
-                                  file.setSelectedLines(p, comment.selection)
-                                }}
-                              >
-                                <Icon name="comment" size="small" style={{ color: "var(--white)" }} />
-                              </button>
-                              <Show when={openedComment() === comment.id}>
-                                <div class="absolute top-[calc(100%+4px)] right-[-8px] z-40 min-w-[200px] max-w-[320px] rounded-[14px] bg-surface-raised-stronger-non-alpha shadow-lg-border-base p-3">
-                                  <div class="flex flex-col gap-1.5">
-                                    <div class="text-14-regular text-text-strong whitespace-pre-wrap">
-                                      {comment.comment}
-                                    </div>
-                                    <div class="text-12-medium text-text-weak whitespace-nowrap">
-                                      Comment on {commentLabel(comment.selection)}
-                                    </div>
-                                  </div>
+                              <div class="flex flex-col gap-1.5">
+                                <div class="text-14-regular text-text-strong whitespace-pre-wrap">
+                                  {comment.comment}
                                 </div>
-                              </Show>
-                            </div>
+                                <div class="text-12-medium text-text-weak whitespace-nowrap">
+                                  Comment on {commentLabel(comment.selection)}
+                                </div>
+                              </div>
+                            </LineCommentAnchor>
                           )}
                         </For>
                         <Show when={commenting()}>
                           {(range) => (
                             <Show when={draftTop() !== undefined}>
-                              <div class="absolute right-6 z-30" style={{ top: `${draftTop() ?? 0}px` }}>
-                                <button
-                                  type="button"
-                                  class="size-5 rounded-md flex items-center justify-center shadow-xs focus:outline-none focus-visible:shadow-xs-border-focus"
-                                  style={{
-                                    background: "var(--icon-interactive-base)",
-                                    color: "var(--white)",
-                                  }}
-                                  onClick={() => textarea?.focus()}
-                                >
-                                  <Icon name="comment" size="small" style={{ color: "var(--white)" }} />
-                                </button>
-                                <div
-                                  class="absolute top-[calc(100%+4px)] right-[-8px] z-40 w-[380px] rounded-[14px] bg-surface-raised-stronger-non-alpha shadow-lg-border-base p-2"
-                                  onFocusOut={(e) => {
-                                    const target = e.relatedTarget as Node | null
-                                    if (!target || !e.currentTarget.contains(target)) {
+                              <LineCommentAnchor
+                                top={draftTop()}
+                                open={true}
+                                variant="editor"
+                                onClick={() => textarea?.focus()}
+                                onPopoverFocusOut={(e) => {
+                                  const target = e.relatedTarget as Node | null
+                                  if (!target || !e.currentTarget.contains(target)) {
+                                    setCommenting(null)
+                                  }
+                                }}
+                              >
+                                <div class="flex flex-col gap-2">
+                                  <textarea
+                                    ref={textarea}
+                                    class="w-full resize-vertical p-2 rounded-[6px] bg-surface-base border border-border-base text-text-strong text-12-regular leading-5 focus:outline-none focus:shadow-xs-border-select"
+                                    rows={3}
+                                    placeholder="Add comment"
+                                    value={draft()}
+                                    onInput={(e) => setDraft(e.currentTarget.value)}
+                                    onKeyDown={(e) => {
+                                      if (e.key === "Escape") {
+                                        setCommenting(null)
+                                        return
+                                      }
+                                      if (e.key !== "Enter") return
+                                      if (e.shiftKey) return
+                                      e.preventDefault()
+                                      const value = draft().trim()
+                                      if (!value) return
+                                      const p = path()
+                                      if (!p) return
+                                      addCommentToContext({
+                                        file: p,
+                                        selection: range(),
+                                        comment: value,
+                                        origin: "file",
+                                      })
                                       setCommenting(null)
-                                    }
-                                  }}
-                                >
-                                  <div class="flex flex-col gap-2">
-                                    <textarea
-                                      ref={textarea}
-                                      class="w-full resize-vertical p-2 rounded-[6px] bg-surface-base border border-border-base text-text-strong text-12-regular leading-5 focus:outline-none focus:shadow-xs-border-select"
-                                      rows={3}
-                                      placeholder="Add comment"
-                                      value={draft()}
-                                      onInput={(e) => setDraft(e.currentTarget.value)}
-                                      onKeyDown={(e) => {
-                                        if (e.key === "Escape") {
-                                          setCommenting(null)
-                                          return
-                                        }
-                                        if (e.key !== "Enter") return
-                                        if (e.shiftKey) return
-                                        e.preventDefault()
+                                    }}
+                                  />
+                                  <div class="flex items-center gap-2">
+                                    <div class="text-12-medium text-text-weak ml-1">
+                                      Commenting on {commentLabel(range())}
+                                    </div>
+                                    <div class="flex-1" />
+                                    <Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
+                                      Cancel
+                                    </Button>
+                                    <Button
+                                      size="small"
+                                      variant="primary"
+                                      disabled={draft().trim().length === 0}
+                                      onClick={() => {
                                         const value = draft().trim()
                                         if (!value) return
                                         const p = path()
                                         if (!p) return
-                                        addCommentToContext({ file: p, selection: range(), comment: value })
+                                        addCommentToContext({
+                                          file: p,
+                                          selection: range(),
+                                          comment: value,
+                                          origin: "file",
+                                        })
                                         setCommenting(null)
                                       }}
-                                    />
-                                    <div class="flex items-center gap-2">
-                                      <div class="text-12-medium text-text-weak ml-1">
-                                        Commenting on {commentLabel(range())}
-                                      </div>
-                                      <div class="flex-1" />
-                                      <Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
-                                        Cancel
-                                      </Button>
-                                      <Button
-                                        size="small"
-                                        variant="primary"
-                                        disabled={draft().trim().length === 0}
-                                        onClick={() => {
-                                          const value = draft().trim()
-                                          if (!value) return
-                                          const p = path()
-                                          if (!p) return
-                                          addCommentToContext({ file: p, selection: range(), comment: value })
-                                          setCommenting(null)
-                                        }}
-                                      >
-                                        Comment
-                                      </Button>
-                                    </div>
+                                    >
+                                      Comment
+                                    </Button>
                                   </div>
                                 </div>
-                              </div>
+                              </LineCommentAnchor>
                             </Show>
                           )}
                         </Show>

+ 49 - 0
packages/ui/src/components/line-comment.css

@@ -0,0 +1,49 @@
+[data-component="line-comment"] {
+  position: absolute;
+  right: 24px;
+  z-index: var(--line-comment-z, 30);
+}
+
+[data-component="line-comment"] [data-slot="line-comment-button"] {
+  width: 20px;
+  height: 20px;
+  border-radius: var(--radius-md);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: var(--icon-interactive-base);
+  box-shadow: var(--shadow-xs);
+  cursor: pointer;
+  border: none;
+}
+
+[data-component="line-comment"] [data-component="icon"] {
+  color: var(--white);
+}
+
+[data-component="line-comment"] [data-slot="line-comment-button"]:focus {
+  outline: none;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-button"]:focus-visible {
+  box-shadow: var(--shadow-xs-border-focus);
+}
+
+[data-component="line-comment"] [data-slot="line-comment-popover"] {
+  position: absolute;
+  top: calc(100% + 4px);
+  right: -8px;
+  z-index: var(--line-comment-popover-z, 40);
+  min-width: 200px;
+  max-width: min(320px, calc(100vw - 48px));
+  border-radius: 14px;
+  background: var(--surface-raised-stronger-non-alpha);
+  box-shadow: var(--shadow-lg-border-base);
+  padding: 12px;
+}
+
+[data-component="line-comment"][data-variant="editor"] [data-slot="line-comment-popover"] {
+  width: 380px;
+  max-width: min(380px, calc(100vw - 48px));
+  padding: 8px;
+}

+ 53 - 0
packages/ui/src/components/line-comment.tsx

@@ -0,0 +1,53 @@
+import { Show, type JSX } from "solid-js"
+import { Icon } from "./icon"
+
+export type LineCommentVariant = "default" | "editor"
+
+export type LineCommentAnchorProps = {
+  id?: string
+  top?: number
+  open: boolean
+  variant?: LineCommentVariant
+  onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
+  onMouseEnter?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
+  onPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent>
+  class?: string
+  popoverClass?: string
+  children: JSX.Element
+}
+
+export const LineCommentAnchor = (props: LineCommentAnchorProps) => {
+  const hidden = () => props.top === undefined
+  const variant = () => props.variant ?? "default"
+
+  return (
+    <div
+      data-component="line-comment"
+      data-variant={variant()}
+      data-comment-id={props.id}
+      classList={{
+        [props.class ?? ""]: !!props.class,
+      }}
+      style={{
+        top: `${props.top ?? 0}px`,
+        opacity: hidden() ? 0 : 1,
+        "pointer-events": hidden() ? "none" : "auto",
+      }}
+    >
+      <button type="button" data-slot="line-comment-button" onClick={props.onClick} onMouseEnter={props.onMouseEnter}>
+        <Icon name="comment" size="small" />
+      </button>
+      <Show when={props.open}>
+        <div
+          data-slot="line-comment-popover"
+          classList={{
+            [props.popoverClass ?? ""]: !!props.popoverClass,
+          }}
+          onFocusOut={props.onPopoverFocusOut}
+        >
+          {props.children}
+        </div>
+      </Show>
+    </div>
+  )
+}

+ 62 - 109
packages/ui/src/components/session-review.css

@@ -75,17 +75,66 @@
     overflow: hidden;
   }
 
-  [data-slot="session-review-comment-popover-content"] {
-    position: absolute;
-    top: calc(100% + 4px);
-    right: -8px;
-    z-index: 6;
-    min-width: 200px;
-    max-width: min(320px, calc(100vw - 48px));
-    border-radius: 10px;
-    background-color: var(--surface-raised-stronger-non-alpha);
-    box-shadow: var(--shadow-lg-border-base);
-    padding: 12px;
+  [data-slot="session-review-comment-content"] {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+  }
+
+  [data-slot="session-review-comment-text"] {
+    font-family: var(--font-family-sans);
+    font-size: var(--font-size-base);
+    font-weight: var(--font-weight-regular);
+    line-height: var(--line-height-x-large);
+    letter-spacing: var(--letter-spacing-normal);
+    color: var(--text-strong);
+    white-space: pre-wrap;
+  }
+
+  [data-slot="session-review-comment-label"],
+  [data-slot="session-review-comment-draft-label"] {
+    font-family: var(--font-family-sans);
+    font-size: var(--font-size-small);
+    font-weight: var(--font-weight-medium);
+    line-height: var(--line-height-large);
+    letter-spacing: var(--letter-spacing-normal);
+    color: var(--text-weak);
+    white-space: nowrap;
+  }
+
+  [data-slot="session-review-comment-draft"] {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+  }
+
+  [data-slot="session-review-comment-textarea"] {
+    width: 100%;
+    max-width: min(380px, calc(100vw - 48px));
+    resize: vertical;
+    padding: 8px;
+    border-radius: var(--radius-md);
+    background: var(--surface-base);
+    border: 1px solid var(--border-base);
+    color: var(--text-strong);
+    font-family: var(--font-family-sans);
+    font-size: var(--font-size-small);
+    line-height: var(--line-height-large);
+
+    &:focus {
+      outline: none;
+      box-shadow: var(--shadow-xs-border-select);
+    }
+  }
+
+  [data-slot="session-review-comment-actions"] {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  [data-slot="session-review-comment-draft-label"] {
+    margin-right: auto;
   }
 
   [data-slot="session-review-trigger-content"] {
@@ -217,103 +266,7 @@
   [data-slot="session-review-diff-wrapper"] {
     position: relative;
     overflow: hidden;
-  }
-
-  [data-slot="session-review-comment-anchor"] {
-    position: absolute;
-    right: 12px;
-    z-index: 5;
-  }
-
-  [data-slot="session-review-comment-button"] {
-    width: 20px;
-    height: 20px;
-    border-radius: 6px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    background: var(--icon-interactive-base);
-    box-shadow: var(--shadow-xs);
-    cursor: pointer;
-
-    [data-slot="icon-svg"] {
-      color: var(--white);
-    }
-
-    &:focus {
-      outline: none;
-    }
-
-    &:focus-visible {
-      box-shadow: var(--shadow-xs-border-focus);
-    }
-  }
-
-  [data-slot="session-review-comment-hover"] {
-    display: flex;
-    flex-direction: column;
-    gap: 6px;
-    max-width: 320px;
-  }
-
-  [data-slot="session-review-comment-popover"] {
-    display: flex;
-    flex-direction: column;
-    gap: 6px;
-  }
-
-  [data-slot="session-review-comment-hover-label"],
-  [data-slot="session-review-comment-popover-label"] {
-    font-family: var(--font-family-sans);
-    font-size: var(--font-size-small);
-    font-weight: var(--font-weight-medium);
-    color: var(--text-weak);
-  }
-
-  [data-slot="session-review-comment-hover-text"],
-  [data-slot="session-review-comment-popover-text"] {
-    font-family: var(--font-family-sans);
-    font-size: var(--font-size-base);
-    font-weight: var(--font-weight-regular);
-    color: var(--text-strong);
-    white-space: pre-wrap;
-  }
-
-  [data-slot="session-review-comment-preview"] {
-    margin: 0;
-    padding: 8px;
-    border-radius: var(--radius-sm);
-    background: var(--surface-base);
-    border: 1px solid color-mix(in oklch, var(--border-base) 55%, transparent);
-    color: var(--text-base);
-    font-family: var(--font-family-mono);
-    font-size: var(--font-size-small);
-    line-height: 1.4;
-    white-space: pre-wrap;
-  }
-
-  [data-slot="session-review-comment-textarea"] {
-    width: 320px;
-    max-width: calc(100vw - 48px);
-    resize: vertical;
-    padding: 8px;
-    border-radius: var(--radius-sm);
-    background: var(--surface-base);
-    border: 1px solid color-mix(in oklch, var(--border-base) 55%, transparent);
-    color: var(--text-strong);
-    font-family: var(--font-family-sans);
-    font-size: var(--font-size-small);
-    line-height: 1.4;
-
-    &:focus {
-      outline: none;
-      box-shadow: var(--shadow-xs-border-focus);
-    }
-  }
-
-  [data-slot="session-review-comment-actions"] {
-    display: flex;
-    justify-content: flex-end;
-    gap: 8px;
+    --line-comment-z: 5;
+    --line-comment-popover-z: 6;
   }
 }

+ 62 - 79
packages/ui/src/components/session-review.tsx

@@ -4,6 +4,7 @@ import { RadioGroup } from "./radio-group"
 import { DiffChanges } from "./diff-changes"
 import { FileIcon } from "./file-icon"
 import { Icon } from "./icon"
+import { LineCommentAnchor } from "./line-comment"
 import { StickyAccordionHeader } from "./sticky-accordion-header"
 import { useDiffComponent } from "../context/diff"
 import { useI18n } from "../context/i18n"
@@ -559,71 +560,74 @@ export const SessionReview = (props: SessionReviewProps) => {
 
                       <For each={comments()}>
                         {(comment) => (
-                          <div
-                            data-slot="session-review-comment-anchor"
-                            data-comment-id={comment.id}
-                            style={{
-                              top: `${positions()[comment.id] ?? 0}px`,
-                              opacity: positions()[comment.id] === undefined ? 0 : 1,
-                              "pointer-events": positions()[comment.id] === undefined ? "none" : "auto",
+                          <LineCommentAnchor
+                            id={comment.id}
+                            top={positions()[comment.id]}
+                            onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
+                            onClick={() => {
+                              if (isCommentOpen(comment)) {
+                                setOpened(null)
+                                return
+                              }
+
+                              openComment(comment)
                             }}
+                            open={isCommentOpen(comment)}
                           >
-                            <button
-                              type="button"
-                              data-slot="session-review-comment-button"
-                              onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
-                              onClick={() => {
-                                if (isCommentOpen(comment)) {
-                                  setOpened(null)
-                                  return
-                                }
-
-                                openComment(comment)
-                              }}
-                            >
-                              <Icon name="comment" size="small" />
-                            </button>
-                            <Show when={isCommentOpen(comment)}>
-                              <div data-slot="session-review-comment-popover-content">
-                                <div data-slot="session-review-comment-popover">
-                                  <div data-slot="session-review-comment-popover-text">{comment.comment}</div>
-                                  <div data-slot="session-review-comment-popover-label">
-                                    Comment on {selectionLabel(comment.selection)}
-                                  </div>
-                                </div>
+                            <div data-slot="session-review-comment-content">
+                              <div data-slot="session-review-comment-text">{comment.comment}</div>
+                              <div data-slot="session-review-comment-label">
+                                Comment on {selectionLabel(comment.selection)}
                               </div>
-                            </Show>
-                          </div>
+                            </div>
+                          </LineCommentAnchor>
                         )}
                       </For>
 
                       <Show when={draftRange()}>
                         {(range) => (
                           <Show when={draftTop() !== undefined}>
-                            <div data-slot="session-review-comment-anchor" style={{ top: `${draftTop() ?? 0}px` }}>
-                              <button
-                                type="button"
-                                data-slot="session-review-comment-button"
-                                onClick={() => textarea?.focus()}
-                              >
-                                <Icon name="comment" size="small" />
-                              </button>
-                              <div data-slot="session-review-comment-popover-content">
-                                <div data-slot="session-review-comment-popover">
-                                  <div data-slot="session-review-comment-popover-label">
+                            <LineCommentAnchor
+                              top={draftTop()}
+                              onClick={() => textarea?.focus()}
+                              open={true}
+                              variant="editor"
+                            >
+                              <div data-slot="session-review-comment-draft">
+                                <textarea
+                                  ref={textarea}
+                                  data-slot="session-review-comment-textarea"
+                                  rows={3}
+                                  placeholder="Add comment"
+                                  value={draft()}
+                                  onInput={(e) => setDraft(e.currentTarget.value)}
+                                  onKeyDown={(e) => {
+                                    if (e.key !== "Enter") return
+                                    if (e.shiftKey) return
+                                    e.preventDefault()
+                                    const value = draft().trim()
+                                    if (!value) return
+                                    props.onLineComment?.({
+                                      file: diff.file,
+                                      selection: range(),
+                                      comment: value,
+                                      preview: selectionPreview(diff, range()),
+                                    })
+                                    setCommenting(null)
+                                  }}
+                                />
+                                <div data-slot="session-review-comment-actions">
+                                  <div data-slot="session-review-comment-draft-label">
                                     Commenting on {getFilename(diff.file)}:{selectionLabel(range())}
                                   </div>
-                                  <textarea
-                                    ref={textarea}
-                                    data-slot="session-review-comment-textarea"
-                                    rows={3}
-                                    placeholder="Add a comment"
-                                    value={draft()}
-                                    onInput={(e) => setDraft(e.currentTarget.value)}
-                                    onKeyDown={(e) => {
-                                      if (e.key !== "Enter") return
-                                      if (e.shiftKey) return
-                                      e.preventDefault()
+                                  <Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
+                                    Cancel
+                                  </Button>
+                                  <Button
+                                    size="small"
+                                    variant="primary"
+                                    disabled={draft().trim().length === 0}
+                                    onClick={() => {
                                       const value = draft().trim()
                                       if (!value) return
                                       props.onLineComment?.({
@@ -634,33 +638,12 @@ export const SessionReview = (props: SessionReviewProps) => {
                                       })
                                       setCommenting(null)
                                     }}
-                                  />
-                                  <div data-slot="session-review-comment-actions">
-                                    <Button size="small" variant="ghost" onClick={() => setCommenting(null)}>
-                                      Cancel
-                                    </Button>
-                                    <Button
-                                      size="small"
-                                      variant="secondary"
-                                      disabled={draft().trim().length === 0}
-                                      onClick={() => {
-                                        const value = draft().trim()
-                                        if (!value) return
-                                        props.onLineComment?.({
-                                          file: diff.file,
-                                          selection: range(),
-                                          comment: value,
-                                          preview: selectionPreview(diff, range()),
-                                        })
-                                        setCommenting(null)
-                                      }}
-                                    >
-                                      Comment
-                                    </Button>
-                                  </div>
+                                  >
+                                    Comment
+                                  </Button>
                                 </div>
                               </div>
-                            </div>
+                            </LineCommentAnchor>
                           </Show>
                         )}
                       </Show>

+ 1 - 0
packages/ui/src/styles/index.css

@@ -25,6 +25,7 @@
 @import "../components/icon-button.css" layer(components);
 @import "../components/image-preview.css" layer(components);
 @import "../components/keybind.css" layer(components);
+@import "../components/line-comment.css" layer(components);
 @import "../components/text-field.css" layer(components);
 @import "../components/inline-input.css" layer(components);
 @import "../components/list.css" layer(components);