From 566027ee204069f2f2da4ce8fff1b0a4dae9efaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Manuel=20B=C3=A9cares?= Date: Thu, 15 Oct 2026 02:24:29 +0200 Subject: [PATCH 1/5] feat(chat): @ mentions for files and subagents, shell-mode hint Typing @ offers the project's files and folders (fuzzy search on the server, /find/file or the v2 /api/fs/find) and the subagents; picked mentions are sent as opencode file and agent parts. ! at the start shows opencode's shell mode. User messages render file and agent parts as chips instead of the synthetic text opencode adds. On phones the options row hides while the keyboard is open. Co-Authored-By: Claude Opus 5.5 --- app/components/chat/MessageItem.vue | 44 +++++-- app/components/chat/PromptBox.vue | 158 ++++++++++++++++++++++- app/composables/useMentions.ts | 172 ++++++++++++++++++++++++++ app/composables/useOpencodeApi.ts | 4 + app/pages/p/[dir]/session/[id].vue | 10 +- server/utils/compat/v2/handlers.ts | 14 +++ server/utils/compat/v2/mappers.ts | 10 ++ tests/e2e/autocomplete.spec.ts | 81 ++++++++++++ tests/unit/compat-v2-handlers.test.ts | 17 +++ tests/unit/mentions.test.ts | 70 +++++++++++ 10 files changed, 566 insertions(+), 14 deletions(-) create mode 100644 app/composables/useMentions.ts create mode 100644 tests/e2e/autocomplete.spec.ts create mode 100644 tests/unit/mentions.test.ts diff --git a/app/components/chat/MessageItem.vue b/app/components/chat/MessageItem.vue index 25fbb57..d38072b 100644 --- a/app/components/chat/MessageItem.vue +++ b/app/components/chat/MessageItem.vue @@ -9,7 +9,7 @@ const speech = useSpeech() const messageText = computed(() => (props.message.parts || []) - .filter((p) => p.type === 'text') + .filter((p) => p.type === 'text' && !(p as { synthetic?: boolean }).synthetic) .map((p) => (p as { text?: string }).text || '') .join('\n\n') .trim() @@ -61,6 +61,28 @@ const visibleParts = computed(() => }) ) +// user messages: the text the user wrote (opencode adds synthetic parts with +// the contents of @-mentioned files and agent hand-offs: those stay hidden) +const userTextParts = computed(() => visibleParts.value.filter( + (p) => p.type === 'text' && !(p as { synthetic?: boolean }).synthetic +)) + +interface Chip { id: string; kind: 'file' | 'folder' | 'agent' | 'attachment'; label: string; icon: string; title: string } +const userChips = computed(() => visibleParts.value.flatMap((p): Chip[] => { + const part = p as Record + if (part.type === 'agent') { + return [{ id: p.id, kind: 'agent', label: `@${part.name}`, icon: 'i-lucide-bot', title: `Handed to the ${part.name} agent` }] + } + if (part.type !== 'file') return [] + const url = String(part.url || '') + if (url.startsWith('file:')) { + const folder = part.mime === 'application/x-directory' + const path = part.source?.path || part.filename || decodeURIComponent(url.replace(/^file:\/\//, '')) + return [{ id: p.id, kind: folder ? 'folder' : 'file', label: `@${path}`, icon: folder ? 'i-lucide-folder' : 'i-lucide-file', title: decodeURIComponent(url.replace(/^file:\/\//, '')) }] + } + return [{ id: p.id, kind: 'attachment', label: part.filename || 'attachment', icon: 'i-lucide-paperclip', title: part.mime || '' }] +})) + const showReasoning = ref(false) function tokenTotal(tokens?: TokenUsage) { @@ -91,13 +113,21 @@ const errorMessage = computed(() => { @click="emit('fork')" /> - -
+
Save to - - + + defined in the {{ info.definedIn }} config: saving here adds an override @@ -740,7 +740,7 @@ const title = computed(() => creating.value ? 'Add MCP server' : props.name || '