summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-24 09:09:23 -0600
committeradamelmore <[email protected]>2026-01-24 09:09:27 -0600
commit6d8e9943837a73944911728a698138e23601d556 (patch)
tree462ebd58be367851bf3d42dcfe15dc0baba2e354 /packages/app/src
parentae77ef3370dd0e017aba38eb5403ccf0f3404e76 (diff)
downloadopencode-6d8e9943837a73944911728a698138e23601d556.tar.gz
opencode-6d8e9943837a73944911728a698138e23601d556.zip
fix(app): line selection fixes
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/components/prompt-input.tsx37
-rw-r--r--packages/app/src/context/prompt.tsx1
-rw-r--r--packages/app/src/pages/session.tsx211
3 files changed, 141 insertions, 108 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 5a367aa61..18c1d98a7 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/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">
diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx
index 6b9d5cc0d..99fab6c19 100644
--- a/packages/app/src/context/prompt.tsx
+++ b/packages/app/src/context/prompt.tsx
@@ -44,6 +44,7 @@ export type FileContextItem = {
selection?: FileSelection
comment?: string
commentID?: string
+ commentOrigin?: "review" | "file"
preview?: string
}
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 2fc4ab62c..46b41d509 100644
--- a/packages/app/src/pages/session.tsx
+++ b/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>