= (props) => {
itemIndex === selectedIndex() ? "dropdown-item-highlight" : ""
}`}
data-picker-selected={itemIndex === selectedIndex()}
- onClick={() => handleSelect({ type: "file", file })}
+ onClick={() => props.onSelect({ type: "file", file }, "click")}
>
a.source.type === "file")
+ .map((a) => a.source.path),
+ )
+
+ const pathAttachments = new Set(
+ attachments
+ .filter((a) => a.source.type === "text" && typeof a.display === "string" && a.display.startsWith("path:"))
+ .map((a) => (a.source as { value: string }).value),
+ )
+
+ let result = prompt
+
+ // Step 1: Handle root paths FIRST using unique placeholders
+ // Replace longer pattern first to avoid partial match issues
+ result = result.replace(/@(\.\/)/g, "___ROOT___")
+ result = result.replace(/@(\.)(?!\.)/g, "___ROOT_NOSLASH___")
+ // Note: The regex @(\.)(?!\.) means @. NOT followed by another .
+
+ // Step 2: Build set of non-root paths
+ const allPaths = new Set()
+ for (const p of fileAttachments) {
+ if (p && p !== "." && p !== "./") allPaths.add(p)
+ }
+ for (const p of pathAttachments) {
+ if (p && p !== "." && p !== "./") allPaths.add(p)
+ }
+
+ // Step 3: Replace @path with ./path for non-root paths
+ for (const path of allPaths) {
+ if (!path) continue
+ const withoutPrefix = path.startsWith("./") ? path.slice(2) : path
+ const withPrefix = path.startsWith("./") ? path : "./" + path
+ result = result.replace("@" + withoutPrefix, withPrefix)
+ result = result.replace("@" + withoutPrefix + "/", withPrefix + "/")
+ }
+
+ // Step 4: Convert placeholders back to ./
+ result = result.replace("___ROOT___", "./")
+ result = result.replace("___ROOT_NOSLASH___", "./")
+
+ // Step 5: Resolve [pasted #N] placeholders
+ if (!result.includes("[pasted #")) {
+ return result
+ }
+
if (!attachments || attachments.length === 0) {
- return prompt
+ return result
}
const lookup = new Map()
@@ -15,7 +62,7 @@ export function resolvePastedPlaceholders(prompt: string, attachments: Attachmen
const source = attachment?.source
if (!source || source.type !== "text") continue
const display = attachment?.display
- const value = source.value
+ const value = (source as { value?: string }).value
if (typeof display !== "string" || typeof value !== "string") continue
const match = display.match(/pasted #(\d+)/)
if (!match) continue
@@ -26,10 +73,10 @@ export function resolvePastedPlaceholders(prompt: string, attachments: Attachmen
}
if (lookup.size === 0) {
- return prompt
+ return result
}
- return prompt.replace(/\[pasted #(\d+)\]/g, (fullMatch) => {
+ return result.replace(/\[pasted #(\d+)\]/g, (fullMatch) => {
const replacement = lookup.get(fullMatch)
return typeof replacement === "string" ? replacement : fullMatch
})
diff --git a/packages/ui/src/stores/session-actions.ts b/packages/ui/src/stores/session-actions.ts
index 4771fae3..456f5456 100644
--- a/packages/ui/src/stores/session-actions.ts
+++ b/packages/ui/src/stores/session-actions.ts
@@ -140,8 +140,11 @@ async function sendMessage(
const display: string | undefined = att.display
const value: unknown = source.value
const isPastedPlaceholder = typeof display === "string" && /^pasted #\d+/.test(display)
+ const isPathPlaceholder = typeof display === "string" && /^path:/.test(display)
- if (isPastedPlaceholder || typeof value !== "string") {
+ // Skip path: attachments from being sent as separate parts (content is already in prompt)
+ // Skip pasted placeholders too (already resolved in prompt)
+ if (isPastedPlaceholder || isPathPlaceholder || typeof value !== "string") {
continue
}