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 { createSignal, type Accessor, type Setter } from "solid-js"
|
||||||
import type { Command as SDKCommand } from "@opencode-ai/sdk/v2"
|
import type { Command as SDKCommand } from "@opencode-ai/sdk/v2"
|
||||||
import type { Agent } from "../../types/session"
|
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 { addAttachment, getAttachments, removeAttachment } from "../../stores/attachments"
|
||||||
import type { PickerMode } from "./types"
|
import type { PickerMode } from "./types"
|
||||||
import type { PickerSelectAction } from "../unified-picker"
|
import type { PickerSelectAction } from "../unified-picker"
|
||||||
@@ -213,18 +213,6 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
|
|||||||
return trimmed.length > 0 ? trimmed : "."
|
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 (isFolder) {
|
||||||
if (action === "tab") {
|
if (action === "tab") {
|
||||||
// TAB on directory: autocomplete directory name and show its contents.
|
// TAB on directory: autocomplete directory name and show its contents.
|
||||||
|
|||||||
@@ -5,21 +5,87 @@ export function resolvePastedPlaceholders(prompt: string, attachments: Attachmen
|
|||||||
return prompt
|
return prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, strip @ from file/directory paths that don't have file attachments
|
// First, strip `@` from path-like mentions that do NOT have a backing file attachment.
|
||||||
// This handles SHIFT+ENTER case where @path should become path
|
// This is intended for SHIFT+ENTER selection where we keep `@path` in the textarea for
|
||||||
const fileAttachments = new Set(
|
// easy deletion, but send `path` to the API.
|
||||||
|
//
|
||||||
|
// IMPORTANT: avoid rewriting plain `@mentions` or email addresses.
|
||||||
|
const fileAttachmentPaths = new Set(
|
||||||
attachments
|
attachments
|
||||||
.filter((a) => a.source.type === "file" && "path" in a.source)
|
.filter((a) => a.source.type === "file")
|
||||||
.map((a) => (a.source as { path: string }).path),
|
.map((a) => a.source.path),
|
||||||
)
|
)
|
||||||
|
|
||||||
let result = prompt.replace(/@([^\s@]+)/g, (match, path) => {
|
const isPathLike = (value: string) => {
|
||||||
// If this path has a file attachment, keep the @ (attachment is sent separately)
|
if (!value) return false
|
||||||
if (fileAttachments.has(path) || fileAttachments.has(path.replace(/\/$/, ""))) {
|
if (value.includes("/") || value.includes("\\")) return true
|
||||||
return match
|
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
|
// Then, resolve [pasted #N] placeholders
|
||||||
|
|||||||
Reference in New Issue
Block a user