From 78940d5b7ee2f3e5020f87b400db1785b37a7d71 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 1 Jan 2026 08:48:35 -0600 Subject: wip(app): file context --- packages/app/src/components/dialog-select-file.tsx | 10 +- packages/app/src/components/prompt-input.tsx | 121 +++++++++++++++++++-- 2 files changed, 120 insertions(+), 11 deletions(-) (limited to 'packages/app/src/components') 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() { 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 = (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 = (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 = (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 = (props) => { } = useFilteredList({ 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 = (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 = (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 = (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 = (props) => { + 0 || !!activeFile()}> +
+ + {(path) => ( +
+ +
+ {getDirectory(path())} + {getFilename(path())} + active +
+ prompt.context.removeActive()} + /> +
+ )} +
+ + + + + {(item) => ( +
+ +
+ {getDirectory(item.path)} + {getFilename(item.path)} + + {(sel) => ( + + {sel().startLine === sel().endLine + ? `:${sel().startLine}` + : `:${sel().startLine}-${sel().endLine}`} + + )} + +
+ prompt.context.remove(item.key)} + /> +
+ )} +
+
+
0}>
-- cgit v1.2.3