fix(app): cleanup comment component usage

This commit is contained in:
adamelmore
2026-01-25 06:20:44 -06:00
parent a5c058e584
commit ddc4e89359
5 changed files with 207 additions and 229 deletions

View File

@@ -15,7 +15,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { useCodeComponent } from "@opencode-ai/ui/context/code" 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 { SessionTurn } from "@opencode-ai/ui/session-turn"
import { createAutoScroll } from "@opencode-ai/ui/hooks" import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { SessionReview } from "@opencode-ai/ui/session-review" import { SessionReview } from "@opencode-ai/ui/session-review"
@@ -1885,7 +1885,6 @@ export default function Page() {
}) })
let wrap: HTMLDivElement | undefined let wrap: HTMLDivElement | undefined
let textarea: HTMLTextAreaElement | undefined
const fileComments = createMemo(() => { const fileComments = createMemo(() => {
const p = path() const p = path()
@@ -1898,7 +1897,6 @@ export default function Page() {
const [openedComment, setOpenedComment] = createSignal<string | null>(null) const [openedComment, setOpenedComment] = createSignal<string | null>(null)
const [commenting, setCommenting] = createSignal<SelectedLineRange | null>(null) const [commenting, setCommenting] = createSignal<SelectedLineRange | null>(null)
const [draft, setDraft] = createSignal("") const [draft, setDraft] = createSignal("")
const [draftError, setDraftError] = createSignal(false)
const [positions, setPositions] = createSignal<Record<string, number>>({}) const [positions, setPositions] = createSignal<Record<string, number>>({})
const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined) const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
@@ -1986,7 +1984,6 @@ export default function Page() {
const range = commenting() const range = commenting()
if (!range) return if (!range) return
setDraft("") setDraft("")
requestAnimationFrame(() => textarea?.focus())
}) })
createEffect(() => { createEffect(() => {
@@ -2047,7 +2044,7 @@ export default function Page() {
/> />
<For each={fileComments()}> <For each={fileComments()}>
{(comment) => ( {(comment) => (
<LineCommentAnchor <LineCommentView
id={comment.id} id={comment.id}
top={positions()[comment.id]} top={positions()[comment.id]}
open={openedComment() === comment.id} open={openedComment() === comment.id}
@@ -2063,26 +2060,31 @@ export default function Page() {
setOpenedComment((current) => (current === comment.id ? null : comment.id)) setOpenedComment((current) => (current === comment.id ? null : comment.id))
file.setSelectedLines(p, comment.selection) file.setSelectedLines(p, comment.selection)
}} }}
> comment={comment.comment}
<div class="flex flex-col gap-1.5"> selection={commentLabel(comment.selection)}
<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>
)} )}
</For> </For>
<Show when={commenting()}> <Show when={commenting()}>
{(range) => ( {(range) => (
<Show when={draftTop() !== undefined}> <Show when={draftTop() !== undefined}>
<LineCommentAnchor <LineCommentEditor
top={draftTop()} top={draftTop()}
open={true} value={draft()}
variant="editor" selection={commentLabel(range())}
onClick={() => textarea?.focus()} 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) => { onPopoverFocusOut={(e) => {
const target = e.relatedTarget as Node | null const target = e.relatedTarget as Node | null
if (target && e.currentTarget.contains(target)) return if (target && e.currentTarget.contains(target)) return
@@ -2093,79 +2095,7 @@ export default function Page() {
} }
}, 0) }, 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>
)} )}
</Show> </Show>

View File

@@ -52,3 +52,64 @@
padding: 8px; padding: 8px;
border-radius: 14px; 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;
}

View File

@@ -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" import { Icon } from "./icon"
export type LineCommentVariant = "default" | "editor" export type LineCommentVariant = "default" | "editor"
@@ -52,3 +53,105 @@ export const LineCommentAnchor = (props: LineCommentAnchorProps) => {
</div> </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>
)
}

View File

@@ -75,68 +75,6 @@
overflow: hidden; 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"] { [data-slot="session-review-trigger-content"] {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -4,7 +4,7 @@ import { RadioGroup } from "./radio-group"
import { DiffChanges } from "./diff-changes" import { DiffChanges } from "./diff-changes"
import { FileIcon } from "./file-icon" import { FileIcon } from "./file-icon"
import { Icon } from "./icon" import { Icon } from "./icon"
import { LineCommentAnchor } from "./line-comment" import { LineComment, LineCommentEditor } from "./line-comment"
import { StickyAccordionHeader } from "./sticky-accordion-header" import { StickyAccordionHeader } from "./sticky-accordion-header"
import { useDiffComponent } from "../context/diff" import { useDiffComponent } from "../context/diff"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
@@ -305,7 +305,6 @@ export const SessionReview = (props: SessionReviewProps) => {
<For each={props.diffs}> <For each={props.diffs}>
{(diff) => { {(diff) => {
let wrapper: HTMLDivElement | undefined let wrapper: HTMLDivElement | undefined
let textarea: HTMLTextAreaElement | undefined
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
const commentedLines = createMemo(() => comments().map((c) => c.selection)) const commentedLines = createMemo(() => comments().map((c) => c.selection))
@@ -396,7 +395,6 @@ export const SessionReview = (props: SessionReviewProps) => {
if (!range) return if (!range) return
setDraft("") setDraft("")
scheduleAnchors() scheduleAnchors()
requestAnimationFrame(() => textarea?.focus())
}) })
createEffect(() => { createEffect(() => {
@@ -565,7 +563,7 @@ export const SessionReview = (props: SessionReviewProps) => {
<For each={comments()}> <For each={comments()}>
{(comment) => ( {(comment) => (
<LineCommentAnchor <LineComment
id={comment.id} id={comment.id}
top={positions()[comment.id]} top={positions()[comment.id]}
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })} onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
@@ -578,83 +576,31 @@ export const SessionReview = (props: SessionReviewProps) => {
openComment(comment) openComment(comment)
}} }}
open={isCommentOpen(comment)} open={isCommentOpen(comment)}
> comment={comment.comment}
<div data-slot="session-review-comment-content"> selection={selectionLabel(comment.selection)}
<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>
)} )}
</For> </For>
<Show when={draftRange()}> <Show when={draftRange()}>
{(range) => ( {(range) => (
<Show when={draftTop() !== undefined}> <Show when={draftTop() !== undefined}>
<LineCommentAnchor <LineCommentEditor
top={draftTop()} top={draftTop()}
onClick={() => textarea?.focus()} value={draft()}
open={true} selection={selectionLabel(range())}
variant="editor" onInput={setDraft}
> onCancel={() => setCommenting(null)}
<div data-slot="session-review-comment-draft"> onSubmit={(comment) => {
<textarea props.onLineComment?.({
ref={textarea} file: diff.file,
data-slot="session-review-comment-textarea" selection: range(),
rows={3} comment,
placeholder="Add comment" preview: selectionPreview(diff, range()),
value={draft()} })
onInput={(e) => setDraft(e.currentTarget.value)} setCommenting(null)
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>
</Show> </Show>
)} )}
</Show> </Show>