diff options
| author | Adam <[email protected]> | 2026-01-01 08:48:35 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-01 21:03:03 -0600 |
| commit | 78940d5b7ee2f3e5020f87b400db1785b37a7d71 (patch) | |
| tree | 5784212a5a219f9c648b2e3afc6c952ea1a2da46 /packages/app/src/components | |
| parent | b84a1f714bf0b81efdf89a0dd6e35fa2b3e8692a (diff) | |
| download | opencode-78940d5b7ee2f3e5020f87b400db1785b37a7d71.tar.gz opencode-78940d5b7ee2f3e5020f87b400db1785b37a7d71.zip | |
wip(app): file context
Diffstat (limited to 'packages/app/src/components')
| -rw-r--r-- | packages/app/src/components/dialog-select-file.tsx | 10 | ||||
| -rw-r--r-- | packages/app/src/components/prompt-input.tsx | 121 |
2 files changed, 120 insertions, 11 deletions
diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index b27afdc8b..8e68a3eb8 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -6,11 +6,11 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path" import { useParams } from "@solidjs/router" import { createMemo } from "solid-js" import { useLayout } from "@/context/layout" -import { useLocal } from "@/context/local" +import { useFile } from "@/context/file" export function DialogSelectFile() { const layout = useLayout() - const local = useLocal() + const file = useFile() const dialog = useDialog() const params = useParams() const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) @@ -20,11 +20,13 @@ export function DialogSelectFile() { <List search={{ placeholder: "Search files", autofocus: true }} emptyMessage="No files found" - items={local.file.searchFiles} + items={file.searchFiles} key={(x) => x} onSelect={(path) => { if (path) { - tabs().open("file://" + path) + const value = file.tab(path) + tabs().open(value) + file.load(path) } dialog.close() }} diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 855eb31e1..967b17606 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -3,6 +3,7 @@ import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Mat import { createStore, produce } from "solid-js/store" import { createFocusSignal } from "@solid-primitives/active-element" import { useLocal } from "@/context/local" +import { useFile, type FileSelection } from "@/context/file" import { ContentPart, DEFAULT_PROMPT, @@ -83,6 +84,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const sdk = useSDK() const sync = useSync() const local = useLocal() + const files = useFile() const prompt = usePrompt() const layout = useLayout() const params = useParams() @@ -126,6 +128,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) + const activeFile = createMemo(() => { + const tab = tabs().active() + if (!tab) return + return files.pathFromTab(tab) + }) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const status = createMemo( () => @@ -303,10 +310,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => { event.preventDefault() setStore("dragging", false) - const files = event.dataTransfer?.files - if (!files) return + const dropped = event.dataTransfer?.files + if (!dropped) return - for (const file of Array.from(files)) { + for (const file of Array.from(dropped)) { if (ACCEPTED_FILE_TYPES.includes(file.type)) { await addImageAttachment(file) } @@ -360,8 +367,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => { } = useFilteredList<AtOption>({ items: async (query) => { const agents = agentList() - const files = await local.file.searchFilesAndDirectories(query) - const fileOptions: AtOption[] = files.map((path) => ({ type: "file", path, display: path })) + const paths = await files.searchFilesAndDirectories(query) + const fileOptions: AtOption[] = paths.map((path) => ({ type: "file", path, display: path })) return [...agents, ...fileOptions] }, key: atKey, @@ -1205,6 +1212,41 @@ 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 url = `file://${absolute}${query}` + if (usedUrls.has(url)) return + usedUrls.add(url) + contextFileParts.push({ + id: Identifier.ascending("part"), + type: "file", + mime: "text/plain", + url, + filename: getFilename(path), + }) + } + + const activePath = activeFile() + if (activePath && prompt.context.activeTab()) { + addContextFile(activePath) + } + + for (const item of prompt.context.items()) { + if (item.type !== "file") continue + addContextFile(item.path, item.selection) + } + const imageAttachmentParts = store.imageAttachments.map((attachment) => ({ id: Identifier.ascending("part"), type: "file" as const, @@ -1214,7 +1256,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => { })) const isShellMode = store.mode === "shell" - tabs().setActive(undefined) editorRef.innerHTML = "" prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0) setStore("imageAttachments", []) @@ -1274,7 +1315,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => { type: "text" as const, text, } - const requestParts = [textPart, ...fileAttachmentParts, ...agentAttachmentParts, ...imageAttachmentParts] + const requestParts = [ + textPart, + ...fileAttachmentParts, + ...contextFileParts, + ...agentAttachmentParts, + ...imageAttachmentParts, + ] const optimisticParts = requestParts.map((part) => ({ ...part, sessionID: existing.id, @@ -1413,6 +1460,66 @@ export const PromptInput: Component<PromptInputProps> = (props) => { </div> </div> </Show> + <Show when={prompt.context.items().length > 0 || !!activeFile()}> + <div class="flex flex-wrap items-center gap-2 px-3 pt-3"> + <Show when={prompt.context.activeTab() ? activeFile() : undefined}> + {(path) => ( + <div class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base max-w-full"> + <FileIcon node={{ path: path(), type: "file" }} class="shrink-0 size-4" /> + <div class="flex items-center text-12-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> + <span class="text-text-weak whitespace-nowrap ml-1">active</span> + </div> + <IconButton + type="button" + icon="close" + variant="ghost" + class="h-6 w-6" + onClick={() => prompt.context.removeActive()} + /> + </div> + )} + </Show> + <Show when={!prompt.context.activeTab() && !!activeFile()}> + <button + type="button" + class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base text-12-regular text-text-weak hover:bg-surface-raised-base-hover" + onClick={() => prompt.context.addActive()} + > + <Icon name="plus-small" size="small" /> + <span>Include active file</span> + </button> + </Show> + <For each={prompt.context.items()}> + {(item) => ( + <div class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base max-w-full"> + <FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" /> + <div class="flex items-center text-12-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-6 w-6" + onClick={() => prompt.context.remove(item.key)} + /> + </div> + )} + </For> + </div> + </Show> <Show when={store.imageAttachments.length > 0}> <div class="flex flex-wrap gap-2 px-3 pt-3"> <For each={store.imageAttachments}> |
