From d15340a4b8d372f99fcea42b43f5e4339b4a401d Mon Sep 17 00:00:00 2001 From: "codenomadbot[bot]" <261069733+codenomadbot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:49:24 +0000 Subject: [PATCH] fix(ui): unwrap pasted placeholders in slash commands (#235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Fix slash command execution so `[pasted #N]` placeholders are resolved before calling `session.command`, matching normal prompt send behavior. ## Why When pasting long text into a slash command (e.g. `/some-command [pasted #1]`), the UI previously bypassed `resolvePastedPlaceholders(...)` for known slash commands and sent the literal placeholder text as command arguments. ## Changes - Resolve pasted placeholders (and other prompt placeholders handled by `resolvePastedPlaceholders`) in slash-command arguments before `executeCustomCommand(...)`. - Remove *consumed* pasted-text attachments (those referenced by placeholders in the slash-command args) so they don’t linger for the next prompt. Fixes #234. ## Notes - I attempted `npm run typecheck --workspace @codenomad/ui` locally but the workspace dependencies aren’t installed in this bot environment, so it fails with missing-module errors. CI should validate with a full install. -- Yours, [CodeNomadBot](https://github.com/NeuralNomadsAI/CodeNomad) Co-authored-by: Shantur Rathore --- packages/ui/src/components/prompt-input.tsx | 43 ++++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index 63c80fa1..646d67f8 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -4,6 +4,7 @@ import UnifiedPicker from "./unified-picker" import ExpandButton from "./expand-button" import { clearAttachments, removeAttachment } from "../stores/attachments" import { resolvePastedPlaceholders } from "../lib/prompt-placeholders" +import { createPastedPlaceholderRegex, pastedDisplayCounterRegex } from "./prompt-input/attachmentPlaceholders" import Kbd from "./kbd" import { getActiveInstance } from "../stores/instances" import { agents, executeCustomCommand } from "../stores/sessions" @@ -13,12 +14,41 @@ import { useI18n } from "../lib/i18n" import { getLogger } from "../lib/logger" import { preferences } from "../stores/preferences" import type { ExpandState, PromptInputApi, PromptInputProps, PromptInsertMode, PromptMode } from "./prompt-input/types" +import type { Attachment } from "../types/attachment" import { usePromptState } from "./prompt-input/usePromptState" import { usePromptAttachments } from "./prompt-input/usePromptAttachments" import { usePromptPicker } from "./prompt-input/usePromptPicker" import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown" const log = getLogger("actions") +function getConsumedPastedTextAttachmentIds(text: string, attachments: Attachment[]): string[] { + if (!text || attachments.length === 0) return [] + + const usedCounters = new Set() + for (const match of text.matchAll(createPastedPlaceholderRegex())) { + const counter = match?.[1] + if (counter) usedCounters.add(counter) + } + + if (usedCounters.size === 0) return [] + + const consumed = new Set() + + for (const attachment of attachments) { + if (!attachment?.id) continue + if (attachment?.source?.type !== "text") continue + const display = attachment.display + if (typeof display !== "string") continue + const match = display.match(pastedDisplayCounterRegex) + if (!match?.[1]) continue + if (usedCounters.has(match[1])) { + consumed.add(attachment.id) + } + } + + return Array.from(consumed) +} + export default function PromptInput(props: PromptInputProps) { const { t } = useI18n() const [, setIsFocused] = createSignal(false) @@ -246,7 +276,12 @@ export default function PromptInput(props: PromptInputProps) { commandName.length > 0 && getCommands(props.instanceId).some((cmd) => cmd.name === commandName) - const resolvedPrompt = isKnownSlashCommand ? text : resolvePastedPlaceholders(text, currentAttachments) + const resolvedCommandArgs = isKnownSlashCommand ? resolvePastedPlaceholders(commandArgs, currentAttachments) : "" + const resolvedPrompt = isKnownSlashCommand + ? resolvedCommandArgs + ? `${commandToken} ${resolvedCommandArgs}` + : commandToken + : resolvePastedPlaceholders(text, currentAttachments) const historyEntry = resolvedPrompt const refreshHistory = () => recordHistoryEntry(historyEntry) @@ -262,6 +297,10 @@ export default function PromptInput(props: PromptInputProps) { syncAttachmentCounters("") setIgnoredAtPositions(new Set()) } else { + const consumedIds = getConsumedPastedTextAttachmentIds(commandArgs, currentAttachments) + for (const attachmentId of consumedIds) { + removeAttachment(props.instanceId, props.sessionId, attachmentId) + } syncAttachmentCounters("") setIgnoredAtPositions(new Set()) } @@ -281,7 +320,7 @@ export default function PromptInput(props: PromptInputProps) { await props.onSend(resolvedPrompt, []) } } else if (isKnownSlashCommand) { - await executeCustomCommand(props.instanceId, props.sessionId, commandName, commandArgs) + await executeCustomCommand(props.instanceId, props.sessionId, commandName, resolvedCommandArgs) } else { await props.onSend(resolvedPrompt, currentAttachments) }