From 0eb523631d6b321960ecbc3893a74d3df086a5d7 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:10:51 -0600 Subject: [PATCH] wip(app): line selection --- packages/app/src/components/prompt-input.tsx | 1 + packages/app/src/pages/session.tsx | 299 +++++++++++--- packages/ui/src/components/session-review.css | 19 +- packages/ui/src/components/session-review.tsx | 373 ++++++++---------- 4 files changed, 427 insertions(+), 265 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 10af351cb..8dc64b428 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1568,6 +1568,7 @@ export const PromptInput: Component = (props) => { class="h-5 w-5" onClick={(e) => { e.stopPropagation() + if (item.commentID) comments.remove(item.path, item.commentID) prompt.context.remove(item.key) }} aria-label={language.t("prompt.context.removeFile")} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index dea9c3d44..96de3f117 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -39,6 +39,7 @@ import { useTerminal, type LocalPTY } from "@/context/terminal" import { useLayout } from "@/context/layout" import { Terminal } from "@/components/terminal" import { checksum, base64Encode, base64Decode } from "@opencode-ai/util/encode" +import { getFilename } from "@opencode-ai/util/path" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectFile } from "@/components/dialog-select-file" import { DialogSelectModel } from "@/components/dialog-select-model" @@ -1866,6 +1867,258 @@ export default function Page() { return `L${sel.startLine}-${sel.endLine}` }) + let wrap: HTMLDivElement | undefined + let textarea: HTMLTextAreaElement | undefined + + const fileComments = createMemo(() => { + const p = path() + if (!p) return [] + return comments.list(p) + }) + + const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection)) + + const [openedComment, setOpenedComment] = createSignal(null) + const [commenting, setCommenting] = createSignal(null) + const [draft, setDraft] = createSignal("") + const [positions, setPositions] = createSignal>({}) + const [draftTop, setDraftTop] = createSignal(undefined) + + const commentLabel = (range: SelectedLineRange) => { + const start = Math.min(range.start, range.end) + const end = Math.max(range.start, range.end) + if (start === end) return `line ${start}` + return `lines ${start}-${end}` + } + + const getRoot = () => { + const el = wrap + if (!el) return + + const host = el.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return + + const root = host.shadowRoot + if (!root) return + + return root + } + + const findMarker = (root: ShadowRoot, range: SelectedLineRange) => { + const line = Math.max(range.start, range.end) + const node = root.querySelector(`[data-line="${line}"]`) + if (!(node instanceof HTMLElement)) return + return node + } + + const markerTop = (wrapper: HTMLElement, marker: HTMLElement) => { + const wrapperRect = wrapper.getBoundingClientRect() + const rect = marker.getBoundingClientRect() + return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2) + } + + const updateComments = () => { + const el = wrap + const root = getRoot() + if (!el || !root) { + setPositions({}) + setDraftTop(undefined) + return + } + + const next: Record = {} + for (const comment of fileComments()) { + const marker = findMarker(root, comment.selection) + if (!marker) continue + next[comment.id] = markerTop(el, marker) + } + + setPositions(next) + + const range = commenting() + if (!range) { + setDraftTop(undefined) + return + } + + const marker = findMarker(root, range) + if (!marker) { + setDraftTop(undefined) + return + } + + setDraftTop(markerTop(el, marker)) + } + + const scheduleComments = () => { + requestAnimationFrame(updateComments) + } + + createEffect(() => { + fileComments() + scheduleComments() + }) + + createEffect(() => { + commenting() + scheduleComments() + }) + + createEffect(() => { + const range = commenting() + if (!range) return + setDraft("") + requestAnimationFrame(() => textarea?.focus()) + }) + + const renderCode = (source: string, wrapperClass: string) => ( +
{ + wrap = el + scheduleComments() + }} + class={`relative overflow-hidden ${wrapperClass}`} + > + { + requestAnimationFrame(restoreScroll) + requestAnimationFrame(updateSelectionPopover) + requestAnimationFrame(scheduleComments) + }} + onLineSelected={(range: SelectedLineRange | null) => { + const p = path() + if (!p) return + file.setSelectedLines(p, range) + if (!range) setCommenting(null) + }} + onLineSelectionEnd={(range: SelectedLineRange | null) => { + if (!range) { + setCommenting(null) + return + } + + setOpenedComment(null) + setCommenting(range) + }} + overflow="scroll" + class="select-text" + /> + + {(comment) => ( +
+ + +
+
+
+ {getFilename(comment.file)}:{commentLabel(comment.selection)} +
+
+ {comment.comment} +
+
+
+
+
+ )} +
+ + {(range) => ( + +
+ +
+
+
+ Commenting on {getFilename(path() ?? "")}:{commentLabel(range())} +
+