fix(app): line selection fixes

This commit is contained in:
adamelmore
2026-01-24 09:09:23 -06:00
parent ae77ef3370
commit 6d8e994383
8 changed files with 369 additions and 297 deletions

View File

@@ -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>