diff options
| author | Adam <[email protected]> | 2026-01-22 19:57:51 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-22 22:12:12 -0600 |
| commit | 82f718b3cfd19cd79171d240851e29127c9d8077 (patch) | |
| tree | d3cc51b08038409e23dd75d660addff75b4a9860 /packages/app/src/components | |
| parent | 0eb523631d6b321960ecbc3893a74d3df086a5d7 (diff) | |
| download | opencode-82f718b3cfd19cd79171d240851e29127c9d8077.tar.gz opencode-82f718b3cfd19cd79171d240851e29127c9d8077.zip | |
wip(app): line selection
Diffstat (limited to 'packages/app/src/components')
| -rw-r--r-- | packages/app/src/components/prompt-input.tsx | 185 |
1 files changed, 106 insertions, 79 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 8dc64b428..f0ac47632 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -15,7 +15,7 @@ import { import { createStore, produce } from "solid-js/store" import { createFocusSignal } from "@solid-primitives/active-element" import { useLocal } from "@/context/local" -import { selectionFromLines, useFile, type FileSelection } from "@/context/file" +import { useFile, type FileSelection } from "@/context/file" import { ContentPart, DEFAULT_PROMPT, @@ -161,18 +161,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) - const activeFile = createMemo(() => { - const tab = tabs().active() - if (!tab) return - return files.pathFromTab(tab) - }) + const recent = createMemo(() => { + const all = tabs().all() + const active = tabs().active() + const order = active ? [active, ...all.filter((x) => x !== active)] : all + const seen = new Set<string>() + const paths: string[] = [] + + for (const tab of order) { + const path = files.pathFromTab(tab) + if (!path) continue + if (seen.has(path)) continue + seen.add(path) + paths.push(path) + } - const activeFileSelection = createMemo(() => { - const path = activeFile() - if (!path) return - const range = files.selectedLines(path) - if (!range) return - return selectionFromLines(range) + return paths }) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const status = createMemo( @@ -393,7 +397,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => { if (!isFocused()) setComposing(false) }) - type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string } + type AtOption = + | { type: "agent"; name: string; display: string } + | { type: "file"; path: string; display: string; recent?: boolean } const agentList = createMemo(() => sync.data.agent @@ -424,12 +430,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => { } = useFilteredList<AtOption>({ items: async (query) => { const agents = agentList() + const open = recent() + const seen = new Set(open) + const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true })) const paths = await files.searchFilesAndDirectories(query) - const fileOptions: AtOption[] = paths.map((path) => ({ type: "file", path, display: path })) - return [...agents, ...fileOptions] + const fileOptions: AtOption[] = paths + .filter((path) => !seen.has(path)) + .map((path) => ({ type: "file", path, display: path })) + return [...agents, ...pinned, ...fileOptions] }, key: atKey, filterKeys: ["display"], + groupBy: (item) => { + if (item.type === "agent") return "agent" + if (item.recent) return "recent" + return "file" + }, + sortGroupsBy: (a, b) => { + const rank = (category: string) => { + if (category === "agent") return 0 + if (category === "recent") return 1 + return 2 + } + return rank(a.category) - rank(b.category) + }, onSelect: handleAtSelect, }) @@ -1242,37 +1266,67 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const usedUrls = new Set(fileAttachmentParts.map((part) => part.url)) - const contextFileParts: Array<{ - id: string - type: "file" - mime: string - url: string - filename?: string - }> = [] - - const addContextFile = (path: string, selection?: FileSelection) => { - const absolute = toAbsolutePath(path) - const query = selection ? `?start=${selection.startLine}&end=${selection.endLine}` : "" + const context = prompt.context.items().slice() + + const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim()) + + const contextParts: Array< + | { + id: string + type: "text" + text: string + } + | { + id: string + type: "file" + mime: string + url: string + filename?: string + } + > = [] + + const commentNote = (path: string, selection: FileSelection | undefined, comment: string) => { + const start = selection ? Math.min(selection.startLine, selection.endLine) : undefined + const end = selection ? Math.max(selection.startLine, selection.endLine) : undefined + const range = + start === undefined || end === undefined + ? "this file" + : start === end + ? `line ${start}` + : `lines ${start} through ${end}` + + return `The user made the following comment regarding ${range} of ${path}: ${comment}` + } + + const addContextFile = (input: { path: string; selection?: FileSelection; comment?: string }) => { + const absolute = toAbsolutePath(input.path) + const query = input.selection ? `?start=${input.selection.startLine}&end=${input.selection.endLine}` : "" const url = `file://${absolute}${query}` - if (usedUrls.has(url)) return + + const comment = input.comment?.trim() + if (!comment && usedUrls.has(url)) return usedUrls.add(url) - contextFileParts.push({ + + if (comment) { + contextParts.push({ + id: Identifier.ascending("part"), + type: "text", + text: commentNote(input.path, input.selection, comment), + }) + } + + contextParts.push({ id: Identifier.ascending("part"), type: "file", mime: "text/plain", url, - filename: getFilename(path), + filename: getFilename(input.path), }) } - const activePath = activeFile() - if (activePath && prompt.context.activeTab()) { - addContextFile(activePath, activeFileSelection()) - } - - for (const item of prompt.context.items()) { + for (const item of context) { if (item.type !== "file") continue - addContextFile(item.path, item.selection) + addContextFile({ path: item.path, selection: item.selection, comment: item.comment }) } const imageAttachmentParts = images.map((attachment) => ({ @@ -1292,7 +1346,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const requestParts = [ textPart, ...fileAttachmentParts, - ...contextFileParts, + ...contextParts, ...agentAttachmentParts, ...imageAttachmentParts, ] @@ -1345,6 +1399,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => { ) } + for (const item of commentItems) { + prompt.context.remove(item.key) + } + clearInput() addOptimisticMessage() @@ -1363,6 +1421,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => { description: errorMessage(err), }) removeOptimisticMessage() + for (const item of commentItems) { + prompt.context.add({ + type: "file", + path: item.path, + selection: item.selection, + comment: item.comment, + commentID: item.commentID, + preview: item.preview, + }) + } restoreInput() }) } @@ -1487,49 +1555,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => { </div> </div> </Show> - <Show when={prompt.context.items().length > 0 || !!activeFile()}> + <Show when={prompt.context.items().length > 0}> <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="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> - </div> - )} - </Show> - <Show when={!prompt.context.activeTab() && !!activeFile()}> - <button - type="button" - 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" /> - <span>{language.t("prompt.context.includeActiveFile")}</span> - </button> - </Show> <For each={prompt.context.items()}> {(item) => { const preview = createMemo(() => selectionPreview(item.path, item.selection, item.preview)) |
