From ee01f01271f1e8c04a0efeacad0c36a44fd18515 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 4 Jul 2025 12:16:55 -0400 Subject: [PATCH] file attachments --- packages/opencode/src/session/index.ts | 64 +++++++++++++++---- .../tui/internal/components/chat/editor.go | 2 +- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 5a2c1b5e..437ce09b 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -1,4 +1,4 @@ -import path from "path" +import path from "node:path" import { App } from "../app/app" import { Identifier } from "../id/id" import { Storage } from "../storage/storage" @@ -15,6 +15,7 @@ import { type UIMessage, type ProviderMetadata, wrapLanguageModel, + type Attachment, } from "ai" import { z, ZodSchema } from "zod" import { Decimal } from "decimal.js" @@ -187,7 +188,6 @@ export namespace Session { export async function unshare(id: string) { const share = await getShare(id) if (!share) return - console.log("share", share) await Storage.remove("session/share/" + id) await update(id, (draft) => { draft.share = undefined @@ -361,6 +361,36 @@ export namespace Session { if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id) const app = App.info() + input.parts = await Promise.all( + input.parts.map(async (part) => { + if (part.type === "file") { + const url = new URL(part.url) + switch (url.protocol) { + case "file:": + let content = await Bun.file( + path.join(app.path.cwd, url.pathname), + ).text() + const range = { + start: url.searchParams.get("start"), + end: url.searchParams.get("end"), + } + if (range.start != null) { + const lines = content.split("\n") + const start = parseInt(range.start) + const end = range.end ? parseInt(range.end) : lines.length + content = lines.slice(start, end).join("\n") + } + return { + type: "file", + url: "data:text/plain;base64," + btoa(content), + mediaType: "text/plain", + filename: part.filename, + } + } + } + return part + }), + ) if (msgs.length === 0 && !session.parentID) { generateText({ maxTokens: input.providerID === "google" ? 1024 : 20, @@ -376,7 +406,7 @@ export namespace Session { { role: "user", content: "", - parts: toParts(input.parts), + ...toParts(input.parts), }, ]), ], @@ -1028,7 +1058,7 @@ function toUIMessage(msg: Message.Info): UIMessage { id: msg.id, role: "assistant", content: "", - parts: toParts(msg.parts), + ...toParts(msg.parts), } } @@ -1037,35 +1067,41 @@ function toUIMessage(msg: Message.Info): UIMessage { id: msg.id, role: "user", content: "", - parts: toParts(msg.parts), + ...toParts(msg.parts), } } throw new Error("not implemented") } -function toParts(parts: Message.MessagePart[]): UIMessage["parts"] { - const result: UIMessage["parts"] = [] +function toParts(parts: Message.MessagePart[]) { + const result: { + parts: UIMessage["parts"] + experimental_attachments: Attachment[] + } = { + parts: [], + experimental_attachments: [], + } for (const part of parts) { switch (part.type) { case "text": - result.push({ type: "text", text: part.text }) + result.parts.push({ type: "text", text: part.text }) break case "file": - result.push({ - type: "file", - data: part.url, - mimeType: part.mediaType, + result.experimental_attachments.push({ + url: part.url, + contentType: part.mediaType, + name: part.filename, }) break case "tool-invocation": - result.push({ + result.parts.push({ type: "tool-invocation", toolInvocation: part.toolInvocation, }) break case "step-start": - result.push({ + result.parts.push({ type: "step-start", }) break diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 427fcc3c..99925e16 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -108,7 +108,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { attachment := &textarea.Attachment{ ID: uuid.NewString(), Display: "@" + fileName, - URL: fmt.Sprintf("file://%s", filePath), + URL: fmt.Sprintf("file://./%s", filePath), Filename: fileName, MediaType: mediaType, }