Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"
import { decodeFileUriForDisplay, decodeFileUrisInText } from "@opencode-ai/util/path"
import { fileURLToPath } from "url"

export type PromptProps = {
sessionID?: string
Expand Down Expand Up @@ -926,27 +928,36 @@ export function Prompt(props: PromptProps) {
command.trigger("prompt.paste")
return
}
const decodedPastedContent = decodeFileUrisInText(pastedContent)

// trim ' from the beginning and end of the pasted content. just
// ' and nothing else
const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
const filepath = decodedPastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
const localPath = (() => {
if (!filepath.startsWith("file://")) return filepath
try {
return fileURLToPath(filepath)
} catch {
return decodeFileUriForDisplay(filepath).replace(/^file:\/\//, "")
}
})()
const isUrl = /^(https?):\/\//.test(filepath)
if (!isUrl) {
try {
const mime = Filesystem.mimeType(filepath)
const filename = path.basename(filepath)
const mime = Filesystem.mimeType(localPath)
const filename = path.basename(localPath)
// Handle SVG as raw text content, not as base64 image
if (mime === "image/svg+xml") {
event.preventDefault()
const content = await Filesystem.readText(filepath).catch(() => {})
const content = await Filesystem.readText(localPath).catch(() => {})
if (content) {
pasteText(content, `[SVG: ${filename ?? "image"}]`)
return
}
}
if (mime.startsWith("image/")) {
event.preventDefault()
const content = await Filesystem.readArrayBuffer(filepath)
const content = await Filesystem.readArrayBuffer(localPath)
.then((buffer) => Buffer.from(buffer).toString("base64"))
.catch(() => {})
if (content) {
Expand All @@ -961,13 +972,19 @@ export function Prompt(props: PromptProps) {
} catch {}
}

const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
const lineCount = (decodedPastedContent.match(/\n/g)?.length ?? 0) + 1
if (
(lineCount >= 3 || pastedContent.length > 150) &&
(lineCount >= 3 || decodedPastedContent.length > 150) &&
!sync.data.config.experimental?.disable_paste_summary
) {
event.preventDefault()
pasteText(pastedContent, `[Pasted ~${lineCount} lines]`)
pasteText(decodedPastedContent, `[Pasted ~${lineCount} lines]`)
return
}

if (decodedPastedContent !== pastedContent) {
event.preventDefault()
input.insertText(decodedPastedContent)
return
}

Expand Down
9 changes: 7 additions & 2 deletions packages/opencode/src/cli/cmd/tui/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { usePromptRef } from "../context/prompt"
import { Installation } from "@/installation"
import { useKV } from "../context/kv"
import { useCommandDialog } from "../component/dialog-command"
import { decodeFileUrisInText } from "@opencode-ai/util/path"

// TODO: what is the best way to do this?
let once = false
Expand Down Expand Up @@ -79,10 +80,14 @@ export function Home() {
onMount(() => {
if (once) return
if (route.initialPrompt) {
prompt.set(route.initialPrompt)
const next = {
...route.initialPrompt,
input: decodeFileUrisInText(route.initialPrompt.input),
}
prompt.set(next)
once = true
} else if (args.prompt) {
prompt.set({ input: args.prompt, parts: [] })
prompt.set({ input: decodeFileUrisInText(args.prompt), parts: [] })
once = true
prompt.submit()
}
Expand Down
12 changes: 10 additions & 2 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import { DialogExportOptions } from "../../ui/dialog-export-options"
import { formatTranscript } from "../../util/transcript"
import { UI } from "@/cli/ui.ts"
import { useTuiConfig } from "../../context/tui-config"
import { decodeFileUrisInText } from "@opencode-ai/util/path"

addDefaultParsers(parsers.parsers)

Expand Down Expand Up @@ -160,6 +161,13 @@ export function Session() {
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)

const normalizeInitialPrompt = (promptInfo: PromptInfo) => {
return {
...promptInfo,
input: decodeFileUrisInText(promptInfo.input),
}
}

const wide = createMemo(() => dimensions().width > 120)
const sidebarVisible = createMemo(() => {
if (session()?.parentID) return false
Expand Down Expand Up @@ -204,7 +212,7 @@ export function Session() {
// Handle initial prompt from fork
createEffect(() => {
if (route.initialPrompt && prompt) {
prompt.set(route.initialPrompt)
prompt.set(normalizeInitialPrompt(route.initialPrompt))
}
})

Expand Down Expand Up @@ -1161,7 +1169,7 @@ export function Session() {
promptRef.set(r)
// Apply initial prompt when prompt component mounts (e.g., from fork)
if (route.initialPrompt) {
r.set(route.initialPrompt)
r.set(normalizeInitialPrompt(route.initialPrompt))
}
}}
disabled={permissions().length > 0 || questions().length > 0}
Expand Down
36 changes: 36 additions & 0 deletions packages/util/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,39 @@ export function truncateMiddle(text: string, maxLength: number = 20) {
const end = Math.floor(available / 2)
return text.slice(0, start) + "…" + text.slice(-end)
}

const FILE_URI_PATTERN = /file:\/\/[^\s<>()`"']+/g
const PERCENT_RUN_PATTERN = /(?:%[0-9A-Fa-f]{2})+/g

function decodePercentRunIfUnicode(encoded: string) {
try {
const decoded = decodeURIComponent(encoded)
return /[^\x00-\x7F]/.test(decoded) ? decoded : encoded
} catch {
return encoded
}
}

export function decodeFileUriForDisplay(uri: string) {
if (!uri.startsWith("file://")) return uri

const queryIndex = uri.indexOf("?")
const hashIndex = uri.indexOf("#")
const boundary =
queryIndex === -1
? hashIndex === -1
? uri.length
: hashIndex
: hashIndex === -1
? queryIndex
: Math.min(queryIndex, hashIndex)

const base = uri.slice(0, boundary)
const suffix = uri.slice(boundary)
const path = base.slice("file://".length).replace(PERCENT_RUN_PATTERN, decodePercentRunIfUnicode)
return `file://${path}${suffix}`
}

export function decodeFileUrisInText(text: string) {
return text.replace(FILE_URI_PATTERN, decodeFileUriForDisplay)
}
Loading