summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-25 06:20:44 -0600
committeradamelmore <[email protected]>2026-01-25 06:20:50 -0600
commitddc4e893598b7aeb54d8476e97332ab97c02002f (patch)
treef546f07d05dfbfab92e76975e2c44a312dd1eefa /packages
parenta5c058e584dce24c47af5a88f6c83b69d79211d2 (diff)
downloadopencode-ddc4e893598b7aeb54d8476e97332ab97c02002f.tar.gz
opencode-ddc4e893598b7aeb54d8476e97332ab97c02002f.zip
fix(app): cleanup comment component usage
Diffstat (limited to 'packages')
-rw-r--r--packages/app/src/pages/session.tsx114
-rw-r--r--packages/ui/src/components/line-comment.css61
-rw-r--r--packages/ui/src/components/line-comment.tsx105
-rw-r--r--packages/ui/src/components/session-review.css62
-rw-r--r--packages/ui/src/components/session-review.tsx94
5 files changed, 207 insertions, 229 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 38717317d..a14b6bcf6 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -15,7 +15,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 { LineComment as LineCommentView, LineCommentEditor } 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"
@@ -1885,7 +1885,6 @@ export default function Page() {
})
let wrap: HTMLDivElement | undefined
- let textarea: HTMLTextAreaElement | undefined
const fileComments = createMemo(() => {
const p = path()
@@ -1898,7 +1897,6 @@ export default function Page() {
const [openedComment, setOpenedComment] = createSignal<string | null>(null)
const [commenting, setCommenting] = createSignal<SelectedLineRange | null>(null)
const [draft, setDraft] = createSignal("")
- const [draftError, setDraftError] = createSignal(false)
const [positions, setPositions] = createSignal<Record<string, number>>({})
const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
@@ -1986,7 +1984,6 @@ export default function Page() {
const range = commenting()
if (!range) return
setDraft("")
- requestAnimationFrame(() => textarea?.focus())
})
createEffect(() => {
@@ -2047,7 +2044,7 @@ export default function Page() {
/>
<For each={fileComments()}>
{(comment) => (
- <LineCommentAnchor
+ <LineCommentView
id={comment.id}
top={positions()[comment.id]}
open={openedComment() === comment.id}
@@ -2063,26 +2060,31 @@ export default function Page() {
setOpenedComment((current) => (current === comment.id ? null : comment.id))
file.setSelectedLines(p, comment.selection)
}}
- >
- <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>
- </LineCommentAnchor>
+ comment={comment.comment}
+ selection={commentLabel(comment.selection)}
+ />
)}
</For>
<Show when={commenting()}>
{(range) => (
<Show when={draftTop() !== undefined}>
- <LineCommentAnchor
+ <LineCommentEditor
top={draftTop()}
- open={true}
- variant="editor"
- onClick={() => textarea?.focus()}
+ value={draft()}
+ selection={commentLabel(range())}
+ onInput={setDraft}
+ onCancel={() => setCommenting(null)}
+ onSubmit={(comment) => {
+ const p = path()
+ if (!p) return
+ addCommentToContext({
+ file: p,
+ selection: range(),
+ comment,
+ origin: "file",
+ })
+ setCommenting(null)
+ }}
onPopoverFocusOut={(e) => {
const target = e.relatedTarget as Node | null
if (target && e.currentTarget.contains(target)) return
@@ -2093,79 +2095,7 @@ export default function Page() {
}
}, 0)
}}
- >
- <div class="flex flex-col gap-2">
- <textarea
- ref={textarea}
- classList={{
- "w-full resize-vertical p-2 rounded-[6px] bg-surface-base text-text-strong text-12-regular leading-5 focus:outline-none": true,
- "focus:shadow-xs-border-select": !draftError(),
- "shadow-xs-border-critical-base": draftError(),
- }}
- rows={3}
- placeholder="Add comment"
- value={draft()}
- onInput={(e) => {
- setDraft(e.currentTarget.value)
- setDraftError(false)
- }}
- 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) {
- setDraftError(true)
- return
- }
- const p = path()
- if (!p) return
- 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"
- onClick={() => {
- const value = draft().trim()
- if (!value) {
- setDraftError(true)
- return
- }
- const p = path()
- if (!p) return
- addCommentToContext({
- file: p,
- selection: range(),
- comment: value,
- origin: "file",
- })
- setCommenting(null)
- }}
- >
- Comment
- </Button>
- </div>
- </div>
- </LineCommentAnchor>
+ />
</Show>
)}
</Show>
diff --git a/packages/ui/src/components/line-comment.css b/packages/ui/src/components/line-comment.css
index 36fb14c64..9dc8eb74f 100644
--- a/packages/ui/src/components/line-comment.css
+++ b/packages/ui/src/components/line-comment.css
@@ -52,3 +52,64 @@
padding: 8px;
border-radius: 14px;
}
+
+[data-component="line-comment"] [data-slot="line-comment-content"] {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-text"] {
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-regular);
+ line-height: var(--line-height-x-large);
+ letter-spacing: var(--letter-spacing-normal);
+ color: var(--text-strong);
+ white-space: pre-wrap;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-label"],
+[data-component="line-comment"] [data-slot="line-comment-editor-label"] {
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large);
+ letter-spacing: var(--letter-spacing-normal);
+ color: var(--text-weak);
+ white-space: nowrap;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-editor"] {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-textarea"] {
+ width: 100%;
+ resize: vertical;
+ padding: 8px;
+ border-radius: var(--radius-md);
+ background: var(--surface-base);
+ border: 1px solid var(--border-base);
+ color: var(--text-strong);
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ line-height: var(--line-height-large);
+}
+
+[data-component="line-comment"] [data-slot="line-comment-textarea"]:focus {
+ outline: none;
+ box-shadow: var(--shadow-xs-border-select);
+}
+
+[data-component="line-comment"] [data-slot="line-comment-actions"] {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+[data-component="line-comment"] [data-slot="line-comment-editor-label"] {
+ margin-right: auto;
+}
diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx
index 41462fa8e..f8869748c 100644
--- a/packages/ui/src/components/line-comment.tsx
+++ b/packages/ui/src/components/line-comment.tsx
@@ -1,4 +1,5 @@
-import { Show, type JSX } from "solid-js"
+import { onMount, Show, splitProps, type JSX } from "solid-js"
+import { Button } from "./button"
import { Icon } from "./icon"
export type LineCommentVariant = "default" | "editor"
@@ -52,3 +53,105 @@ export const LineCommentAnchor = (props: LineCommentAnchorProps) => {
</div>
)
}
+
+export type LineCommentProps = Omit<LineCommentAnchorProps, "children" | "variant"> & {
+ comment: JSX.Element
+ selection: JSX.Element
+}
+
+export const LineComment = (props: LineCommentProps) => {
+ const [split, rest] = splitProps(props, ["comment", "selection"])
+
+ return (
+ <LineCommentAnchor {...rest} variant="default">
+ <div data-slot="line-comment-content">
+ <div data-slot="line-comment-text">{split.comment}</div>
+ <div data-slot="line-comment-label">Comment on {split.selection}</div>
+ </div>
+ </LineCommentAnchor>
+ )
+}
+
+export type LineCommentEditorProps = Omit<LineCommentAnchorProps, "children" | "open" | "variant" | "onClick"> & {
+ value: string
+ selection: JSX.Element
+ onInput: (value: string) => void
+ onCancel: VoidFunction
+ onSubmit: (value: string) => void
+ placeholder?: string
+ rows?: number
+ autofocus?: boolean
+ cancelLabel?: string
+ submitLabel?: string
+}
+
+export const LineCommentEditor = (props: LineCommentEditorProps) => {
+ const [split, rest] = splitProps(props, [
+ "value",
+ "selection",
+ "onInput",
+ "onCancel",
+ "onSubmit",
+ "placeholder",
+ "rows",
+ "autofocus",
+ "cancelLabel",
+ "submitLabel",
+ ])
+
+ const refs = {
+ textarea: undefined as HTMLTextAreaElement | undefined,
+ }
+
+ const focus = () => refs.textarea?.focus()
+
+ const submit = () => {
+ const value = split.value.trim()
+ if (!value) return
+ split.onSubmit(value)
+ }
+
+ onMount(() => {
+ if (split.autofocus === false) return
+ requestAnimationFrame(focus)
+ })
+
+ return (
+ <LineCommentAnchor {...rest} open={true} variant="editor" onClick={() => focus()}>
+ <div data-slot="line-comment-editor">
+ <textarea
+ ref={(el) => {
+ refs.textarea = el
+ }}
+ data-slot="line-comment-textarea"
+ rows={split.rows ?? 3}
+ placeholder={split.placeholder ?? "Add comment"}
+ value={split.value}
+ onInput={(e) => split.onInput(e.currentTarget.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Escape") {
+ e.preventDefault()
+ e.stopPropagation()
+ split.onCancel()
+ return
+ }
+ if (e.key !== "Enter") return
+ if (e.shiftKey) return
+ e.preventDefault()
+ e.stopPropagation()
+ submit()
+ }}
+ />
+ <div data-slot="line-comment-actions">
+ <div data-slot="line-comment-editor-label">Commenting on {split.selection}</div>
+ <Button size="small" variant="ghost" onClick={split.onCancel}>
+ {split.cancelLabel ?? "Cancel"}
+ </Button>
+ <Button size="small" variant="primary" disabled={split.value.trim().length === 0} onClick={submit}>
+ {split.submitLabel ?? "Comment"}
+ </Button>
+ </div>
+ </div>
+ </LineCommentAnchor>
+ )
+}
diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css
index 7c77c0e35..dd75b1905 100644
--- a/packages/ui/src/components/session-review.css
+++ b/packages/ui/src/components/session-review.css
@@ -75,68 +75,6 @@
overflow: hidden;
}
- [data-slot="session-review-comment-content"] {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
-
- [data-slot="session-review-comment-text"] {
- font-family: var(--font-family-sans);
- font-size: var(--font-size-base);
- font-weight: var(--font-weight-regular);
- line-height: var(--line-height-x-large);
- letter-spacing: var(--letter-spacing-normal);
- color: var(--text-strong);
- white-space: pre-wrap;
- }
-
- [data-slot="session-review-comment-label"],
- [data-slot="session-review-comment-draft-label"] {
- font-family: var(--font-family-sans);
- font-size: var(--font-size-small);
- font-weight: var(--font-weight-medium);
- line-height: var(--line-height-large);
- letter-spacing: var(--letter-spacing-normal);
- color: var(--text-weak);
- white-space: nowrap;
- }
-
- [data-slot="session-review-comment-draft"] {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
-
- [data-slot="session-review-comment-textarea"] {
- width: 100%;
- max-width: min(380px, calc(100vw - 48px));
- resize: vertical;
- padding: 8px;
- border-radius: var(--radius-md);
- background: var(--surface-base);
- border: 1px solid var(--border-base);
- color: var(--text-strong);
- font-family: var(--font-family-sans);
- font-size: var(--font-size-small);
- line-height: var(--line-height-large);
-
- &:focus {
- outline: none;
- box-shadow: var(--shadow-xs-border-select);
- }
- }
-
- [data-slot="session-review-comment-actions"] {
- display: flex;
- align-items: center;
- gap: 8px;
- }
-
- [data-slot="session-review-comment-draft-label"] {
- margin-right: auto;
- }
-
[data-slot="session-review-trigger-content"] {
display: flex;
align-items: center;
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 9bc7d82c1..1ae0b1a13 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/packages/ui/src/components/session-review.tsx
@@ -4,7 +4,7 @@ import { RadioGroup } from "./radio-group"
import { DiffChanges } from "./diff-changes"
import { FileIcon } from "./file-icon"
import { Icon } from "./icon"
-import { LineCommentAnchor } from "./line-comment"
+import { LineComment, LineCommentEditor } from "./line-comment"
import { StickyAccordionHeader } from "./sticky-accordion-header"
import { useDiffComponent } from "../context/diff"
import { useI18n } from "../context/i18n"
@@ -305,7 +305,6 @@ export const SessionReview = (props: SessionReviewProps) => {
<For each={props.diffs}>
{(diff) => {
let wrapper: HTMLDivElement | undefined
- let textarea: HTMLTextAreaElement | undefined
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
const commentedLines = createMemo(() => comments().map((c) => c.selection))
@@ -396,7 +395,6 @@ export const SessionReview = (props: SessionReviewProps) => {
if (!range) return
setDraft("")
scheduleAnchors()
- requestAnimationFrame(() => textarea?.focus())
})
createEffect(() => {
@@ -565,7 +563,7 @@ export const SessionReview = (props: SessionReviewProps) => {
<For each={comments()}>
{(comment) => (
- <LineCommentAnchor
+ <LineComment
id={comment.id}
top={positions()[comment.id]}
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
@@ -578,83 +576,31 @@ export const SessionReview = (props: SessionReviewProps) => {
openComment(comment)
}}
open={isCommentOpen(comment)}
- >
- <div data-slot="session-review-comment-content">
- <div data-slot="session-review-comment-text">{comment.comment}</div>
- <div data-slot="session-review-comment-label">
- Comment on {selectionLabel(comment.selection)}
- </div>
- </div>
- </LineCommentAnchor>
+ comment={comment.comment}
+ selection={selectionLabel(comment.selection)}
+ />
)}
</For>
<Show when={draftRange()}>
{(range) => (
<Show when={draftTop() !== undefined}>
- <LineCommentAnchor
+ <LineCommentEditor
top={draftTop()}
- onClick={() => textarea?.focus()}
- open={true}
- variant="editor"
- >
- <div data-slot="session-review-comment-draft">
- <textarea
- ref={textarea}
- data-slot="session-review-comment-textarea"
- rows={3}
- placeholder="Add comment"
- value={draft()}
- onInput={(e) => setDraft(e.currentTarget.value)}
- onKeyDown={(e) => {
- if (e.key === "Escape") {
- e.preventDefault()
- e.stopPropagation()
- setCommenting(null)
- return
- }
- 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">
- <div data-slot="session-review-comment-draft-label">
- Commenting on {selectionLabel(range())}
- </div>
- <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
- props.onLineComment?.({
- file: diff.file,
- selection: range(),
- comment: value,
- preview: selectionPreview(diff, range()),
- })
- setCommenting(null)
- }}
- >
- Comment
- </Button>
- </div>
- </div>
- </LineCommentAnchor>
+ value={draft()}
+ selection={selectionLabel(range())}
+ onInput={setDraft}
+ onCancel={() => setCommenting(null)}
+ onSubmit={(comment) => {
+ props.onLineComment?.({
+ file: diff.file,
+ selection: range(),
+ comment,
+ preview: selectionPreview(diff, range()),
+ })
+ setCommenting(null)
+ }}
+ />
</Show>
)}
</Show>