summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-22 13:10:51 -0600
committerAdam <[email protected]>2026-01-22 22:12:12 -0600
commit0eb523631d6b321960ecbc3893a74d3df086a5d7 (patch)
tree798c04d55fa36df0bf67e4826eaa32ede4dd0d6c
parent99e15caaf6c736e0c8ebc702e264e4f7a0113e3c (diff)
downloadopencode-0eb523631d6b321960ecbc3893a74d3df086a5d7.tar.gz
opencode-0eb523631d6b321960ecbc3893a74d3df086a5d7.zip
wip(app): line selection
-rw-r--r--packages/app/src/components/prompt-input.tsx1
-rw-r--r--packages/app/src/pages/session.tsx299
-rw-r--r--packages/ui/src/components/session-review.css19
-rw-r--r--packages/ui/src/components/session-review.tsx381
4 files changed, 431 insertions, 269 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 10af351cb..8dc64b428 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/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")}
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index dea9c3d44..96de3f117 100644
--- a/packages/app/src/pages/session.tsx
+++ b/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>
diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css
index 26ca73265..f34c8b446 100644
--- a/packages/ui/src/components/session-review.css
+++ b/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"] {
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 4096f341b..7c35c9226 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/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>
)