Browse Source

wip(app): line selection

Adam 2 months ago
parent
commit
0eb523631d

+ 1 - 0
packages/app/src/components/prompt-input.tsx

@@ -1568,6 +1568,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                         class="h-5 w-5"
                         onClick={(e) => {
                           e.stopPropagation()
+                          if (item.commentID) comments.remove(item.path, item.commentID)
                           prompt.context.remove(item.key)
                         }}
                         aria-label={language.t("prompt.context.removeFile")}

+ 255 - 44
packages/app/src/pages/session.tsx

@@ -39,6 +39,7 @@ import { useTerminal, type LocalPTY } from "@/context/terminal"
 import { useLayout } from "@/context/layout"
 import { Terminal } from "@/components/terminal"
 import { checksum, base64Encode, base64Decode } from "@opencode-ai/util/encode"
+import { getFilename } from "@opencode-ai/util/path"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { DialogSelectFile } from "@/components/dialog-select-file"
 import { DialogSelectModel } from "@/components/dialog-select-model"
@@ -1866,6 +1867,258 @@ export default function Page() {
                       return `L${sel.startLine}-${sel.endLine}`
                     })
 
+                    let wrap: HTMLDivElement | undefined
+                    let textarea: HTMLTextAreaElement | undefined
+
+                    const fileComments = createMemo(() => {
+                      const p = path()
+                      if (!p) return []
+                      return comments.list(p)
+                    })
+
+                    const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection))
+
+                    const [openedComment, setOpenedComment] = createSignal<string | null>(null)
+                    const [commenting, setCommenting] = createSignal<SelectedLineRange | null>(null)
+                    const [draft, setDraft] = createSignal("")
+                    const [positions, setPositions] = createSignal<Record<string, number>>({})
+                    const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
+
+                    const commentLabel = (range: SelectedLineRange) => {
+                      const start = Math.min(range.start, range.end)
+                      const end = Math.max(range.start, range.end)
+                      if (start === end) return `line ${start}`
+                      return `lines ${start}-${end}`
+                    }
+
+                    const getRoot = () => {
+                      const el = wrap
+                      if (!el) return
+
+                      const host = el.querySelector("diffs-container")
+                      if (!(host instanceof HTMLElement)) return
+
+                      const root = host.shadowRoot
+                      if (!root) return
+
+                      return root
+                    }
+
+                    const findMarker = (root: ShadowRoot, range: SelectedLineRange) => {
+                      const line = Math.max(range.start, range.end)
+                      const node = root.querySelector(`[data-line="${line}"]`)
+                      if (!(node instanceof HTMLElement)) return
+                      return node
+                    }
+
+                    const markerTop = (wrapper: HTMLElement, marker: HTMLElement) => {
+                      const wrapperRect = wrapper.getBoundingClientRect()
+                      const rect = marker.getBoundingClientRect()
+                      return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2)
+                    }
+
+                    const updateComments = () => {
+                      const el = wrap
+                      const root = getRoot()
+                      if (!el || !root) {
+                        setPositions({})
+                        setDraftTop(undefined)
+                        return
+                      }
+
+                      const next: Record<string, number> = {}
+                      for (const comment of fileComments()) {
+                        const marker = findMarker(root, comment.selection)
+                        if (!marker) continue
+                        next[comment.id] = markerTop(el, marker)
+                      }
+
+                      setPositions(next)
+
+                      const range = commenting()
+                      if (!range) {
+                        setDraftTop(undefined)
+                        return
+                      }
+
+                      const marker = findMarker(root, range)
+                      if (!marker) {
+                        setDraftTop(undefined)
+                        return
+                      }
+
+                      setDraftTop(markerTop(el, marker))
+                    }
+
+                    const scheduleComments = () => {
+                      requestAnimationFrame(updateComments)
+                    }
+
+                    createEffect(() => {
+                      fileComments()
+                      scheduleComments()
+                    })
+
+                    createEffect(() => {
+                      commenting()
+                      scheduleComments()
+                    })
+
+                    createEffect(() => {
+                      const range = commenting()
+                      if (!range) return
+                      setDraft("")
+                      requestAnimationFrame(() => textarea?.focus())
+                    })
+
+                    const renderCode = (source: string, wrapperClass: string) => (
+                      <div
+                        ref={(el) => {
+                          wrap = el
+                          scheduleComments()
+                        }}
+                        class={`relative overflow-hidden ${wrapperClass}`}
+                      >
+                        <Dynamic
+                          component={codeComponent}
+                          file={{
+                            name: path() ?? "",
+                            contents: source,
+                            cacheKey: cacheKey(),
+                          }}
+                          enableLineSelection
+                          selectedLines={selectedLines()}
+                          commentedLines={commentedLines()}
+                          onRendered={() => {
+                            requestAnimationFrame(restoreScroll)
+                            requestAnimationFrame(updateSelectionPopover)
+                            requestAnimationFrame(scheduleComments)
+                          }}
+                          onLineSelected={(range: SelectedLineRange | null) => {
+                            const p = path()
+                            if (!p) return
+                            file.setSelectedLines(p, range)
+                            if (!range) setCommenting(null)
+                          }}
+                          onLineSelectionEnd={(range: SelectedLineRange | null) => {
+                            if (!range) {
+                              setCommenting(null)
+                              return
+                            }
+
+                            setOpenedComment(null)
+                            setCommenting(range)
+                          }}
+                          overflow="scroll"
+                          class="select-text"
+                        />
+                        <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",
+                              }}
+                            >
+                              <button
+                                type="button"
+                                class="size-5 rounded-md flex items-center justify-center bg-surface-warning-base border border-border-warning-base text-icon-warning-active shadow-xs hover:bg-surface-warning-weak hover:border-border-warning-hover focus:outline-none focus-visible:shadow-xs-border-focus"
+                                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="speech-bubble" size="small" />
+                              </button>
+                              <Show when={openedComment() === comment.id}>
+                                <div class="absolute top-0 right-[calc(100%+12px)] z-40 min-w-[200px] max-w-[320px] rounded-md bg-surface-raised-stronger-non-alpha border border-border-base shadow-md p-3">
+                                  <div class="flex flex-col gap-1.5">
+                                    <div class="text-12-medium text-text-strong whitespace-nowrap">
+                                      {getFilename(comment.file)}:{commentLabel(comment.selection)}
+                                    </div>
+                                    <div class="text-12-regular text-text-base whitespace-pre-wrap">
+                                      {comment.comment}
+                                    </div>
+                                  </div>
+                                </div>
+                              </Show>
+                            </div>
+                          )}
+                        </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 bg-surface-warning-base border border-border-warning-base text-icon-warning-active shadow-xs hover:bg-surface-warning-weak hover:border-border-warning-hover focus:outline-none focus-visible:shadow-xs-border-focus"
+                                  onClick={() => textarea?.focus()}
+                                >
+                                  <Icon name="speech-bubble" size="small" />
+                                </button>
+                                <div class="absolute top-0 right-[calc(100%+12px)] z-40 min-w-[200px] max-w-[320px] rounded-md bg-surface-raised-stronger-non-alpha border border-border-base shadow-md p-3">
+                                  <div class="flex flex-col gap-2">
+                                    <div class="text-12-medium text-text-strong">
+                                      Commenting on {getFilename(path() ?? "")}:{commentLabel(range())}
+                                    </div>
+                                    <textarea
+                                      ref={textarea}
+                                      class="w-[320px] max-w-[calc(100vw-48px)] resize-vertical p-2 rounded-sm bg-surface-base border border-border-base text-text-strong text-12-regular leading-5 focus:outline-none focus:shadow-xs-border-focus"
+                                      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()
+                                        const value = draft().trim()
+                                        if (!value) return
+                                        const p = path()
+                                        if (!p) return
+                                        addCommentToContext({ file: p, selection: range(), comment: value })
+                                        setCommenting(null)
+                                      }}
+                                    />
+                                    <div class="flex justify-end gap-2">
+                                      <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
+                                          const p = path()
+                                          if (!p) return
+                                          addCommentToContext({ file: p, selection: range(), comment: value })
+                                          setCommenting(null)
+                                        }}
+                                      >
+                                        Comment
+                                      </Button>
+                                    </div>
+                                  </div>
+                                </div>
+                              </div>
+                            </Show>
+                          )}
+                        </Show>
+                      </div>
+                    )
+
                     const updateSelectionPopover = () => {
                       const el = scroll
                       if (!el) {
@@ -2107,27 +2360,7 @@ export default function Page() {
                           </Match>
                           <Match when={state()?.loaded && isSvg()}>
                             <div class="flex flex-col gap-4 px-6 py-4">
-                              <Dynamic
-                                component={codeComponent}
-                                file={{
-                                  name: path() ?? "",
-                                  contents: svgContent() ?? "",
-                                  cacheKey: cacheKey(),
-                                }}
-                                enableLineSelection
-                                selectedLines={selectedLines()}
-                                onRendered={() => {
-                                  requestAnimationFrame(restoreScroll)
-                                  requestAnimationFrame(updateSelectionPopover)
-                                }}
-                                onLineSelected={(range: SelectedLineRange | null) => {
-                                  const p = path()
-                                  if (!p) return
-                                  file.setSelectedLines(p, range)
-                                }}
-                                overflow="scroll"
-                                class="select-text"
-                              />
+                              {renderCode(svgContent() ?? "", "")}
                               <Show when={svgPreviewUrl()}>
                                 <div class="flex justify-center pb-40">
                                   <img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
@@ -2135,29 +2368,7 @@ export default function Page() {
                               </Show>
                             </div>
                           </Match>
-                          <Match when={state()?.loaded}>
-                            <Dynamic
-                              component={codeComponent}
-                              file={{
-                                name: path() ?? "",
-                                contents: contents(),
-                                cacheKey: cacheKey(),
-                              }}
-                              enableLineSelection
-                              selectedLines={selectedLines()}
-                              onRendered={() => {
-                                requestAnimationFrame(restoreScroll)
-                                requestAnimationFrame(updateSelectionPopover)
-                              }}
-                              onLineSelected={(range: SelectedLineRange | null) => {
-                                const p = path()
-                                if (!p) return
-                                file.setSelectedLines(p, range)
-                              }}
-                              overflow="scroll"
-                              class="select-text pb-40"
-                            />
-                          </Match>
+                          <Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
                           <Match when={state()?.loading}>
                             <div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
                           </Match>

+ 12 - 7
packages/ui/src/components/session-review.css

@@ -75,13 +75,18 @@
     overflow: hidden;
   }
 
-  [data-component="popover-content"] {
-    position: absolute !important;
-  }
-
-  .session-review-comment-popover-content {
-    left: auto !important;
-    right: calc(100% + 12px) !important;
+  [data-slot="session-review-comment-popover-content"] {
+    position: absolute;
+    top: 0;
+    right: calc(100% + 12px);
+    z-index: 40;
+    min-width: 200px;
+    max-width: min(320px, calc(100vw - 48px));
+    border-radius: var(--radius-md);
+    background-color: var(--surface-raised-stronger-non-alpha);
+    border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
+    box-shadow: var(--shadow-md);
+    padding: 12px;
   }
 
   [data-slot="session-review-trigger-content"] {

+ 163 - 218
packages/ui/src/components/session-review.tsx

@@ -1,6 +1,5 @@
 import { Accordion } from "./accordion"
 import { Button } from "./button"
-import { Popover } from "./popover"
 import { RadioGroup } from "./radio-group"
 import { DiffChanges } from "./diff-changes"
 import { FileIcon } from "./file-icon"
@@ -151,6 +150,7 @@ function markerTop(wrapper: HTMLElement, marker: HTMLElement) {
 
 export const SessionReview = (props: SessionReviewProps) => {
   let scroll: HTMLDivElement | undefined
+  let focusToken = 0
   const i18n = useI18n()
   const diffComponent = useDiffComponent()
   const anchors = new Map<string, HTMLElement>()
@@ -201,6 +201,9 @@ export const SessionReview = (props: SessionReviewProps) => {
     const focus = props.focusedComment
     if (!focus) return
 
+    focusToken++
+    const token = focusToken
+
     setOpened(focus)
 
     const comment = (props.comments ?? []).find((c) => c.file === focus.file && c.id === focus.id)
@@ -211,31 +214,35 @@ export const SessionReview = (props: SessionReviewProps) => {
       handleChange([...current, focus.file])
     }
 
-    requestAnimationFrame(() => {
-      requestAnimationFrame(() => {
-        const root = scroll
-        if (!root) return
-
-        const anchor = root.querySelector(`[data-comment-id="${focus.id}"]`)
-        if (anchor instanceof HTMLElement) {
-          const rootRect = root.getBoundingClientRect()
-          const anchorRect = anchor.getBoundingClientRect()
-          const offset = anchorRect.top - rootRect.top
-          const next = root.scrollTop + offset - rootRect.height / 2 + anchorRect.height / 2
-          root.scrollTop = Math.max(0, next)
-          return
-        }
-
-        const target = anchors.get(focus.file)
-        if (!target) return
-
-        const rootRect = root.getBoundingClientRect()
-        const targetRect = target.getBoundingClientRect()
-        const offset = targetRect.top - rootRect.top
-        const next = root.scrollTop + offset - rootRect.height / 2 + targetRect.height / 2
-        root.scrollTop = Math.max(0, next)
-      })
-    })
+    const scrollTo = (attempt: number) => {
+      if (token !== focusToken) return
+
+      const root = scroll
+      if (!root) return
+
+      const anchor = root.querySelector(`[data-comment-id="${focus.id}"]`)
+      const ready =
+        anchor instanceof HTMLElement && anchor.style.pointerEvents !== "none" && anchor.style.opacity !== "0"
+
+      const target = ready ? anchor : anchors.get(focus.file)
+      if (!target) {
+        if (attempt >= 24) return
+        requestAnimationFrame(() => scrollTo(attempt + 1))
+        return
+      }
+
+      const rootRect = root.getBoundingClientRect()
+      const targetRect = target.getBoundingClientRect()
+      const offset = targetRect.top - rootRect.top
+      const next = root.scrollTop + offset - rootRect.height / 2 + targetRect.height / 2
+      root.scrollTop = Math.max(0, next)
+
+      if (ready) return
+      if (attempt >= 24) return
+      requestAnimationFrame(() => scrollTo(attempt + 1))
+    }
+
+    requestAnimationFrame(() => scrollTo(0))
 
     requestAnimationFrame(() => props.onFocusedCommentChange?.(null))
   })
@@ -519,207 +526,145 @@ export const SessionReview = (props: SessionReviewProps) => {
                     </Accordion.Trigger>
                   </StickyAccordionHeader>
                   <Accordion.Content data-slot="session-review-accordion-content">
-                    <Switch>
-                      <Match when={isImage()}>
-                        <div data-slot="session-review-image-container">
-                          <Show
-                            when={imageSrc()}
-                            fallback={
-                              <div data-slot="session-review-image-placeholder">
-                                <Switch>
-                                  <Match when={imageStatus() === "loading"}>Loading image...</Match>
-                                  <Match when={true}>Image preview unavailable</Match>
-                                </Switch>
-                              </div>
-                            }
-                          >
-                            <img data-slot="session-review-image" src={imageSrc()!} alt={getFilename(diff.file)} />
-                          </Show>
-                        </div>
-                      </Match>
-                      <Match when={isAudio()}>
-                        <div data-slot="session-review-audio-container">
-                          <Show
-                            when={audioSrc() && audioStatus() !== "error"}
-                            fallback={
-                              <div data-slot="session-review-audio-placeholder">
-                                <Switch>
-                                  <Match when={audioStatus() === "loading"}>Loading audio...</Match>
-                                  <Match when={true}>Audio preview unavailable</Match>
-                                </Switch>
-                              </div>
-                            }
+                    <div
+                      data-slot="session-review-diff-wrapper"
+                      ref={(el) => {
+                        wrapper = el
+                        anchors.set(diff.file, el)
+                        scheduleAnchors()
+                      }}
+                    >
+                      <Dynamic
+                        component={diffComponent}
+                        preloadedDiff={diff.preloaded}
+                        diffStyle={diffStyle()}
+                        onRendered={() => {
+                          props.onDiffRendered?.()
+                          scheduleAnchors()
+                        }}
+                        enableLineSelection={props.onLineComment != null}
+                        onLineSelected={handleLineSelected}
+                        onLineSelectionEnd={handleLineSelectionEnd}
+                        selectedLines={selectedLines()}
+                        commentedLines={commentedLines()}
+                        before={{
+                          name: diff.file!,
+                          contents: typeof diff.before === "string" ? diff.before : "",
+                        }}
+                        after={{
+                          name: diff.file!,
+                          contents: typeof diff.after === "string" ? diff.after : "",
+                        }}
+                      />
+
+                      <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",
+                            }}
                           >
-                            <audio
-                              data-slot="session-review-audio"
-                              controls
-                              preload="metadata"
-                              onError={() => {
-                                setAudioStatus("error")
+                            <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)
                               }}
                             >
-                              <source src={audioSrc()!} type={audioMime()} />
-                            </audio>
-                          </Show>
-                        </div>
-                      </Match>
-                      <Match when={true}>
-                        <div
-                          data-slot="session-review-diff-wrapper"
-                          ref={(el) => {
-                            wrapper = el
-                            anchors.set(diff.file, el)
-                            scheduleAnchors()
-                          }}
-                        >
-                          <Dynamic
-                            component={diffComponent}
-                            preloadedDiff={diff.preloaded}
-                            diffStyle={diffStyle()}
-                            onRendered={() => {
-                              props.onDiffRendered?.()
-                              scheduleAnchors()
-                            }}
-                            enableLineSelection={props.onLineComment != null}
-                            onLineSelected={handleLineSelected}
-                            onLineSelectionEnd={handleLineSelectionEnd}
-                            selectedLines={selectedLines()}
-                            commentedLines={commentedLines()}
-                            before={{
-                              name: diff.file!,
-                              contents: beforeText(),
-                            }}
-                            after={{
-                              name: diff.file!,
-                              contents: afterText(),
-                            }}
-                          />
-
-                          <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",
-                                }}
-                              >
-                                <Popover
-                                  portal={false}
-                                  open={isCommentOpen(comment)}
-                                  class="session-review-comment-popover-content"
-                                  onOpenChange={(open) => {
-                                    if (open) {
-                                      openComment(comment)
-                                      return
-                                    }
-                                    if (!isCommentOpen(comment)) return
-                                    setOpened(null)
-                                  }}
-                                  trigger={
-                                    <button
-                                      type="button"
-                                      data-slot="session-review-comment-button"
-                                      onMouseEnter={() =>
-                                        setSelection({ file: comment.file, range: comment.selection })
-                                      }
-                                    >
-                                      <Icon name="speech-bubble" size="small" />
-                                    </button>
-                                  }
-                                >
-                                  <div data-slot="session-review-comment-popover">
-                                    <div data-slot="session-review-comment-popover-label">
-                                      {getFilename(comment.file)}:{selectionLabel(comment.selection)}
-                                    </div>
-                                    <div data-slot="session-review-comment-popover-text">{comment.comment}</div>
-                                    <Show when={selectionPreview(diff, comment.selection)}>
-                                      {(preview) => <pre data-slot="session-review-comment-preview">{preview()}</pre>}
-                                    </Show>
+                              <Icon name="speech-bubble" 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-label">
+                                    {getFilename(comment.file)}:{selectionLabel(comment.selection)}
                                   </div>
-                                </Popover>
+                                  <div data-slot="session-review-comment-popover-text">{comment.comment}</div>
+                                </div>
                               </div>
-                            )}
-                          </For>
-
-                          <Show when={draftRange()}>
-                            {(range) => (
-                              <Show when={draftTop() !== undefined}>
-                                <div data-slot="session-review-comment-anchor" style={{ top: `${draftTop() ?? 0}px` }}>
-                                  <Popover
-                                    portal={false}
-                                    open={true}
-                                    class="session-review-comment-popover-content"
-                                    onOpenChange={(open) => {
-                                      if (open) return
+                            </Show>
+                          </div>
+                        )}
+                      </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="speech-bubble" 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">
+                                    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()
+                                      const value = draft().trim()
+                                      if (!value) return
+                                      props.onLineComment?.({
+                                        file: diff.file,
+                                        selection: range(),
+                                        comment: value,
+                                        preview: selectionPreview(diff, range()),
+                                      })
                                       setCommenting(null)
                                     }}
-                                    trigger={
-                                      <button type="button" data-slot="session-review-comment-button">
-                                        <Icon name="speech-bubble" size="small" />
-                                      </button>
-                                    }
-                                  >
-                                    <div data-slot="session-review-comment-popover">
-                                      <div data-slot="session-review-comment-popover-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()
-                                          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">
-                                        <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>
-                                    </div>
-                                  </Popover>
+                                  />
+                                  <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>
                                 </div>
-                              </Show>
-                            )}
+                              </div>
+                            </div>
                           </Show>
-                        </div>
-                      </Match>
-                    </Switch>
+                        )}
+                      </Show>
+                    </div>
                   </Accordion.Content>
                 </Accordion.Item>
               )