From f346992924e0932722c47c2557f480d6c38624fe Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 4 Mar 2026 05:15:15 +0000 Subject: [PATCH] feat(chat): slash command palette, action button containers, app detection fix - Slash commands (/code, /nostr, /design, /search) now appear in the prompt palette with descriptions, auto-send on select - Chat message hover actions wrapped in a proper glass container with backdrop blur, divider between actions and feedback thumbs - Palette has 8px side margins, no scroll limit - Fix app extraction: queries mentioning known app names (e.g. "start9") now surface the Apps tab even without explicit app/nostr query patterns Co-Authored-By: Claude Opus 4.6 --- .../app/src/components/chat/ChatInput.vue | 18 ++- .../app/src/components/chat/ChatMessage.vue | 104 +++++++++--------- .../app/src/components/chat/ChatWindow.vue | 12 +- .../app/src/components/chat/PromptPalette.vue | 104 ++++++++++++++---- .../app/src/composables/contentExtraction.ts | 11 +- 5 files changed, 171 insertions(+), 78 deletions(-) diff --git a/packages/app/src/components/chat/ChatInput.vue b/packages/app/src/components/chat/ChatInput.vue index ee6db914..2f70c85c 100644 --- a/packages/app/src/components/chat/ChatInput.vue +++ b/packages/app/src/components/chat/ChatInput.vue @@ -199,10 +199,10 @@ const images = ref([]) const paletteRef = ref | null>(null) -// Prompt palette: opens when text starts with "/" (but not "/search " or "/code" etc.) +// Prompt palette: opens when text starts with "/" and has no space yet const isPaletteMode = computed(() => { const t = text.value.trimStart() - return t === '/' || (t.startsWith('/') && !t.includes(' ') && !t.startsWith('/search') && !t.startsWith('/code') && !t.startsWith('/nostr') && !t.startsWith('/exit')) + return t === '/' || (t.startsWith('/') && !t.includes(' ')) }) const paletteQuery = computed(() => { @@ -211,6 +211,20 @@ const paletteQuery = computed(() => { }) function handlePaletteSelect(templateText: string) { + // Slash commands — send immediately (except /search which needs query input) + if (templateText.startsWith('/') && !templateText.includes('{{')) { + text.value = templateText + if (templateText.trimEnd() === '/search') { + // /search needs a query — set text and let user type + nextTick(() => { + autoResize() + textareaRef.value?.focus() + }) + return + } + nextTick(() => send()) + return + } text.value = templateText nextTick(() => { autoResize() diff --git a/packages/app/src/components/chat/ChatMessage.vue b/packages/app/src/components/chat/ChatMessage.vue index bca39049..fb3d54b2 100644 --- a/packages/app/src/components/chat/ChatMessage.vue +++ b/packages/app/src/components/chat/ChatMessage.vue @@ -9,59 +9,61 @@
- - - - - - - +
+ + + + +
+ + +
-
+
- +
-
-

Templates

+ +
+
+

Commands

+
+
+
+ {{ cmd.slash }} +

{{ cmd.title }}

+
+

{{ cmd.preview }}

+
-
-

{{ t.title }}

-

{{ t.preview }}

+ + +
+
+

Templates

+
+
+

{{ t.title }}

+

{{ t.preview }}

+
-
-

No matching templates

+ +
+

No matching commands

@@ -62,6 +87,20 @@ import { ref, computed, watch } from 'vue' import { usePromptTemplateStore, extractVariables, type PromptTemplate } from '@/stores/promptTemplates' +interface PaletteCommand { + id: string + slash: string + title: string + preview: string +} + +const BUILT_IN_COMMANDS: PaletteCommand[] = [ + { id: 'cmd-code', slash: '/code', title: 'Code', preview: 'Open project browser and code editor' }, + { id: 'cmd-nostr', slash: '/nostr', title: 'Nostr', preview: 'Browse the Nostr network feed' }, + { id: 'cmd-design', slash: '/design', title: 'Design System', preview: 'Open the design system viewer' }, + { id: 'cmd-search', slash: '/search ', title: 'Search', preview: 'Search your content library' }, +] + const props = defineProps<{ query: string isOpen: boolean @@ -77,7 +116,17 @@ const highlightIndex = ref(0) const selectedTemplate = ref(null) const variableValues = ref>({}) -const filteredTemplates = computed(() => { +const filteredCommands = computed(() => { + const q = props.query.toLowerCase() + if (!q) return BUILT_IN_COMMANDS + return BUILT_IN_COMMANDS.filter(c => + c.slash.toLowerCase().includes('/' + q) || + c.title.toLowerCase().includes(q) || + c.preview.toLowerCase().includes(q) + ) +}) + +const filteredUserTemplates = computed(() => { const q = props.query.toLowerCase() if (!q) return templateStore.sortedTemplates return templateStore.sortedTemplates.filter(t => @@ -85,6 +134,8 @@ const filteredTemplates = computed(() => { ) }) +const allItemsCount = computed(() => filteredCommands.value.length + filteredUserTemplates.value.length) + const variables = computed(() => { if (!selectedTemplate.value) return [] return extractVariables(selectedTemplate.value.content) @@ -103,10 +154,14 @@ watch(() => props.isOpen, (open) => { } }) +function selectCommand(cmd: PaletteCommand) { + // Commands like /code, /nostr send the slash text directly + emit('select', cmd.slash) +} + function selectTemplate(t: PromptTemplate) { const vars = extractVariables(t.content) if (vars.length === 0) { - // No variables — insert directly emit('select', t.content) return } @@ -135,12 +190,17 @@ function navigateUp() { } function navigateDown() { - if (highlightIndex.value < filteredTemplates.value.length - 1) highlightIndex.value++ + if (highlightIndex.value < allItemsCount.value - 1) highlightIndex.value++ } function selectHighlighted() { - const t = filteredTemplates.value[highlightIndex.value] - if (t) selectTemplate(t) + const idx = highlightIndex.value + if (idx < filteredCommands.value.length) { + selectCommand(filteredCommands.value[idx]) + } else { + const t = filteredUserTemplates.value[idx - filteredCommands.value.length] + if (t) selectTemplate(t) + } } defineExpose({ diff --git a/packages/app/src/composables/contentExtraction.ts b/packages/app/src/composables/contentExtraction.ts index 6b4f71a4..98ca978d 100644 --- a/packages/app/src/composables/contentExtraction.ts +++ b/packages/app/src/composables/contentExtraction.ts @@ -1218,8 +1218,15 @@ export function extractApps(text: string, userQuery: string): AppEntry[] { } } - // Surface apps if: app/nostr query with 1+, or 2+ apps detected in any context - const isAppContext = isAppQuery(userQuery) || isNostrQuery(userQuery) + // Check if query itself mentions a known app name + const queryLower = userQuery.toLowerCase() + const queryMatchesApp = APP_DATABASE.some(app => + [app.name.toLowerCase(), ...app.keywords.map(k => k.toLowerCase())] + .some(kw => kw.length >= 3 && queryLower.includes(kw)) + ) + + // Surface apps if: app/nostr/known-app query with 1+, or 2+ apps detected in any context + const isAppContext = isAppQuery(userQuery) || isNostrQuery(userQuery) || queryMatchesApp if (isAppContext && matched.length >= 1) return matched if (matched.length >= 2) return matched return []