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