fix(ui): avoid stripping non-path @mentions
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { createSignal, type Accessor, type Setter } from "solid-js"
|
||||
import type { Command as SDKCommand } from "@opencode-ai/sdk/v2"
|
||||
import type { Agent } from "../../types/session"
|
||||
import { createAgentAttachment, createFileAttachment, createTextAttachment } from "../../types/attachment"
|
||||
import { createAgentAttachment, createFileAttachment } from "../../types/attachment"
|
||||
import { addAttachment, getAttachments, removeAttachment } from "../../stores/attachments"
|
||||
import type { PickerMode } from "./types"
|
||||
import type { PickerSelectAction } from "../unified-picker"
|
||||
@@ -213,18 +213,6 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
|
||||
return trimmed.length > 0 ? trimmed : "."
|
||||
})()
|
||||
|
||||
const addPathOnlyAttachment = (value: string) => {
|
||||
const display = `path: ${value}`
|
||||
const filename = value
|
||||
const existing = getAttachments(options.instanceId(), options.sessionId())
|
||||
const alreadyAttached = existing.some(
|
||||
(att) => att.source.type === "text" && att.source.value === value && att.display === display,
|
||||
)
|
||||
if (!alreadyAttached) {
|
||||
addAttachment(options.instanceId(), options.sessionId(), createTextAttachment(value, display, filename))
|
||||
}
|
||||
}
|
||||
|
||||
if (isFolder) {
|
||||
if (action === "tab") {
|
||||
// TAB on directory: autocomplete directory name and show its contents.
|
||||
|
||||
@@ -5,21 +5,87 @@ export function resolvePastedPlaceholders(prompt: string, attachments: Attachmen
|
||||
return prompt
|
||||
}
|
||||
|
||||
// First, strip @ from file/directory paths that don't have file attachments
|
||||
// This handles SHIFT+ENTER case where @path should become path
|
||||
const fileAttachments = new Set(
|
||||
// First, strip `@` from path-like mentions that do NOT have a backing file attachment.
|
||||
// This is intended for SHIFT+ENTER selection where we keep `@path` in the textarea for
|
||||
// easy deletion, but send `path` to the API.
|
||||
//
|
||||
// IMPORTANT: avoid rewriting plain `@mentions` or email addresses.
|
||||
const fileAttachmentPaths = new Set(
|
||||
attachments
|
||||
.filter((a) => a.source.type === "file" && "path" in a.source)
|
||||
.map((a) => (a.source as { path: string }).path),
|
||||
.filter((a) => a.source.type === "file")
|
||||
.map((a) => a.source.path),
|
||||
)
|
||||
|
||||
let result = prompt.replace(/@([^\s@]+)/g, (match, path) => {
|
||||
// If this path has a file attachment, keep the @ (attachment is sent separately)
|
||||
if (fileAttachments.has(path) || fileAttachments.has(path.replace(/\/$/, ""))) {
|
||||
return match
|
||||
const isPathLike = (value: string) => {
|
||||
if (!value) return false
|
||||
if (value.includes("/") || value.includes("\\")) return true
|
||||
if (value.startsWith("./") || value.startsWith("../")) return true
|
||||
if (value.startsWith("~")) return true
|
||||
if (value.endsWith("/")) return true
|
||||
|
||||
// Root-level files (no `/`) still commonly have an extension.
|
||||
const ext = value.split(".").pop()?.toLowerCase()
|
||||
if (!ext || ext === value.toLowerCase()) return false
|
||||
|
||||
// Keep this list intentionally small and code-focused to avoid matching domains like `example.com`.
|
||||
const allowedExts = new Set([
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"mjs",
|
||||
"cjs",
|
||||
"json",
|
||||
"md",
|
||||
"txt",
|
||||
"yml",
|
||||
"yaml",
|
||||
"toml",
|
||||
"css",
|
||||
"html",
|
||||
"htm",
|
||||
"svg",
|
||||
"png",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
"pdf",
|
||||
"rs",
|
||||
"go",
|
||||
"py",
|
||||
"java",
|
||||
"kt",
|
||||
"swift",
|
||||
"sh",
|
||||
"bash",
|
||||
"zsh",
|
||||
"sql",
|
||||
"lock",
|
||||
])
|
||||
return allowedExts.has(ext)
|
||||
}
|
||||
|
||||
const stripTrailingPunctuation = (value: string) => {
|
||||
const match = value.match(/^(.*?)([)\]}.,!?:;]+)?$/)
|
||||
if (!match) return { core: value, trailing: "" }
|
||||
return { core: match[1] ?? value, trailing: match[2] ?? "" }
|
||||
}
|
||||
|
||||
let result = prompt.replace(/(^|[\s([{"'`])@([^\s@]+)/g, (full, prefix, rawToken) => {
|
||||
const { core, trailing } = stripTrailingPunctuation(String(rawToken))
|
||||
if (!core) return full
|
||||
|
||||
// If this path has a file attachment, keep the `@` (attachment is sent separately).
|
||||
if (fileAttachmentPaths.has(core) || fileAttachmentPaths.has(core.replace(/\/$/, ""))) {
|
||||
return `${prefix}@${core}${trailing}`
|
||||
}
|
||||
// Otherwise (SHIFT+ENTER case), strip the @
|
||||
return path
|
||||
|
||||
// Only strip for path-like tokens; leave plain `@mentions` intact.
|
||||
if (!isPathLike(core)) {
|
||||
return `${prefix}@${core}${trailing}`
|
||||
}
|
||||
|
||||
return `${prefix}${core}${trailing}`
|
||||
})
|
||||
|
||||
// Then, resolve [pasted #N] placeholders
|
||||
|
||||
Reference in New Issue
Block a user