wip(app): line selection

This commit is contained in:
Adam
2026-01-21 05:27:52 -06:00
parent 640d1f1ecc
commit 0ce0cacb28
7 changed files with 627 additions and 57 deletions

View File

@@ -164,6 +164,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return files.pathFromTab(tab)
})
const selectionPreview = (path: string, selection?: FileSelection, preview?: string) => {
if (preview) return preview
if (!selection) return undefined
const content = files.get(path)?.content?.content
if (!content) return undefined
const start = Math.max(1, Math.min(selection.startLine, selection.endLine))
const end = Math.max(selection.startLine, selection.endLine)
const lines = content.split("\n").slice(start - 1, end)
if (lines.length === 0) return undefined
return lines.slice(0, 2).join("\n")
}
const activeFileSelection = createMemo(() => {
const path = activeFile()
if (!path) return
@@ -171,6 +183,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (!range) return
return selectionFromLines(range)
})
const activeSelectionPreview = createMemo(() => {
const path = activeFile()
if (!path) return
return selectionPreview(path, activeFileSelection())
})
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const status = createMemo(
() =>
@@ -1485,40 +1502,49 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
</Show>
<Show when={prompt.context.items().length > 0 || !!activeFile()}>
<div class="flex flex-wrap items-center gap-1.5 px-3 pt-3">
<div class="flex flex-nowrap items-start gap-1.5 px-3 pt-3 overflow-x-auto no-scrollbar">
<Show when={prompt.context.activeTab() ? activeFile() : undefined}>
{(path) => (
<div class="flex items-center gap-1.5 px-1.5 py-0.5 rounded-md bg-surface-base border border-border-base max-w-full">
<FileIcon node={{ path: path(), type: "file" }} class="shrink-0 size-3.5" />
<div class="flex items-center text-11-regular min-w-0">
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
<Show when={activeFileSelection()}>
{(sel) => (
<span class="text-text-weak whitespace-nowrap ml-1">
{sel().startLine === sel().endLine
? `:${sel().startLine}`
: `:${sel().startLine}-${sel().endLine}`}
</span>
)}
</Show>
<span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
<div class="shrink-0 flex flex-col gap-1 rounded-md bg-surface-base border border-border-base px-2 py-1 max-w-[320px]">
<div class="flex items-center gap-1.5">
<FileIcon node={{ path: path(), type: "file" }} class="shrink-0 size-3.5" />
<div class="flex items-center text-11-regular min-w-0">
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
<Show when={activeFileSelection()}>
{(sel) => (
<span class="text-text-weak whitespace-nowrap ml-1">
{sel().startLine === sel().endLine
? `:${sel().startLine}`
: `:${sel().startLine}-${sel().endLine}`}
</span>
)}
</Show>
<span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
</div>
<IconButton
type="button"
icon="close"
variant="ghost"
class="h-5 w-5"
onClick={() => prompt.context.removeActive()}
aria-label={language.t("prompt.context.removeActiveFile")}
/>
</div>
<IconButton
type="button"
icon="close"
variant="ghost"
class="h-5 w-5"
onClick={() => prompt.context.removeActive()}
aria-label={language.t("prompt.context.removeActiveFile")}
/>
<Show when={activeSelectionPreview()}>
{(preview) => (
<pre class="text-10-regular text-text-weak font-mono whitespace-pre-wrap leading-4">
{preview()}
</pre>
)}
</Show>
</div>
)}
</Show>
<Show when={!prompt.context.activeTab() && !!activeFile()}>
<button
type="button"
class="flex items-center gap-1.5 px-1.5 py-0.5 rounded-md bg-surface-base border border-border-base text-11-regular text-text-weak hover:bg-surface-raised-base-hover"
class="shrink-0 flex items-center gap-1.5 px-1.5 py-0.5 rounded-md bg-surface-base border border-border-base text-11-regular text-text-weak hover:bg-surface-raised-base-hover"
onClick={() => prompt.context.addActive()}
>
<Icon name="plus-small" size="small" />
@@ -1526,32 +1552,47 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</button>
</Show>
<For each={prompt.context.items()}>
{(item) => (
<div class="flex items-center gap-1.5 px-1.5 py-0.5 rounded-md bg-surface-base border border-border-base max-w-full">
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
<div class="flex items-center text-11-regular min-w-0">
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(item.path)}</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path)}</span>
<Show when={item.selection}>
{(sel) => (
<span class="text-text-weak whitespace-nowrap ml-1">
{sel().startLine === sel().endLine
? `:${sel().startLine}`
: `:${sel().startLine}-${sel().endLine}`}
</span>
{(item) => {
const preview = createMemo(() => selectionPreview(item.path, item.selection, item.preview))
return (
<div class="shrink-0 flex flex-col gap-1 rounded-md bg-surface-base border border-border-base px-2 py-1 max-w-[320px]">
<div class="flex items-center gap-1.5">
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
<div class="flex items-center text-11-regular min-w-0">
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(item.path)}</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path)}</span>
<Show when={item.selection}>
{(sel) => (
<span class="text-text-weak whitespace-nowrap ml-1">
{sel().startLine === sel().endLine
? `:${sel().startLine}`
: `:${sel().startLine}-${sel().endLine}`}
</span>
)}
</Show>
</div>
<IconButton
type="button"
icon="close"
variant="ghost"
class="h-5 w-5"
onClick={() => prompt.context.remove(item.key)}
aria-label={language.t("prompt.context.removeFile")}
/>
</div>
<Show when={item.comment}>
{(comment) => <div class="text-11-regular text-text-strong">{comment()}</div>}
</Show>
<Show when={preview()}>
{(content) => (
<pre class="text-10-regular text-text-weak font-mono whitespace-pre-wrap leading-4">
{content()}
</pre>
)}
</Show>
</div>
<IconButton
type="button"
icon="close"
variant="ghost"
class="h-5 w-5"
onClick={() => prompt.context.remove(item.key)}
aria-label={language.t("prompt.context.removeFile")}
/>
</div>
)}
)
}}
</For>
</div>
</Show>

View File

@@ -4,6 +4,7 @@ import { batch, createMemo, createRoot, onCleanup } from "solid-js"
import { useParams } from "@solidjs/router"
import type { FileSelection } from "@/context/file"
import { Persist, persisted } from "@/utils/persist"
import { checksum } from "@opencode-ai/util/encode"
interface PartBase {
content: string
@@ -41,6 +42,8 @@ export type FileContextItem = {
type: "file"
path: string
selection?: FileSelection
comment?: string
preview?: string
}
export type ContextItem = FileContextItem
@@ -135,7 +138,11 @@ function createPromptSession(dir: string, id: string | undefined) {
if (item.type !== "file") return item.type
const start = item.selection?.startLine
const end = item.selection?.endLine
return `${item.type}:${item.path}:${start}:${end}`
const key = `${item.type}:${item.path}:${start}:${end}`
const comment = item.comment?.trim()
if (!comment) return key
const digest = checksum(comment) ?? comment
return `${key}:c=${digest.slice(0, 8)}`
}
return {

View File

@@ -81,6 +81,7 @@ interface SessionReviewTabProps {
diffStyle: DiffStyle
onDiffStyleChange?: (style: DiffStyle) => void
onViewFile?: (file: string) => void
onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void
classes?: {
root?: string
header?: string
@@ -166,6 +167,7 @@ function SessionReviewTab(props: SessionReviewTabProps) {
onDiffStyleChange={props.onDiffStyleChange}
onViewFile={props.onViewFile}
readFile={readFile}
onLineComment={props.onLineComment}
/>
)
}
@@ -488,8 +490,36 @@ export default function Page() {
setStore("expanded", id, status().type !== "idle")
})
const selectionPreview = (path: string, selection: FileSelection) => {
const content = file.get(path)?.content?.content
if (!content) return undefined
const start = Math.max(1, Math.min(selection.startLine, selection.endLine))
const end = Math.max(selection.startLine, selection.endLine)
const lines = content.split("\n").slice(start - 1, end)
if (lines.length === 0) return undefined
return lines.slice(0, 2).join("\n")
}
const addSelectionToContext = (path: string, selection: FileSelection) => {
prompt.context.add({ type: "file", path, selection })
const preview = selectionPreview(path, selection)
prompt.context.add({ type: "file", path, selection, preview })
}
const addCommentToContext = (input: {
file: string
selection: SelectedLineRange
comment: string
preview?: string
}) => {
const selection = selectionFromLines(input.selection)
const preview = input.preview ?? selectionPreview(input.file, selection)
prompt.context.add({
type: "file",
path: input.file,
selection,
comment: input.comment,
preview,
})
}
command.register(() => [
@@ -1402,6 +1432,7 @@ export default function Page() {
diffs={diffs}
view={view}
diffStyle="unified"
onLineComment={addCommentToContext}
onViewFile={(path) => {
const value = file.tab(path)
tabs().open(value)
@@ -1717,6 +1748,7 @@ export default function Page() {
view={view}
diffStyle={layout.review.diffStyle()}
onDiffStyleChange={layout.review.setDiffStyle}
onLineComment={addCommentToContext}
onViewFile={(path) => {
const value = file.tab(path)
tabs().open(value)