feat(spotlight): offer 'Talk to AIUI about it' for the typed query

Cmd/Ctrl+K search could only match text against known screens; anything it
did not recognise dead-ended at 'No results'. That text is now handed to the
assistant instead: a blue accented row (chat-bubble + sparkle) appears while
there is a query, always last in the keyboard order, so Cmd+K -> type -> Enter
reaches AIUI without the mouse. On a zero-match query it is the only option.

The prompt travels by postMessage, NOT as an iframe URL param. Chat.vue's
aiuiUrl is deliberately free of reactive dependencies so the iframe src stays
byte-identical and AIUI survives a tab switch (see the D14_FLAGS comment);
threading the question through the URL would reload AIUI and discard the
conversation on every ask — the opposite of the intent. Two regression tests
pin this: the src is byte-identical across an ask, and ask/askedAt are
stripped afterwards so a refresh cannot silently re-ask.

The ask is queued and flushed on AIUI's 'ready' handshake, because arriving
from Cmd+K on a cold Chat tab means the iframe has not connected yet.

AIUI-side receiver lands separately; until then this posts a message AIUI
ignores, which is inert rather than broken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 16:45:33 -04:00
co-authored by Claude Opus 5
parent 3015a8acf3
commit 97b3707b5e
3 changed files with 156 additions and 5 deletions
+46 -2
View File
@@ -63,8 +63,8 @@
</template>
<script setup lang="ts">
import { ref, computed, onActivated, onBeforeUnmount, onDeactivated, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ref, computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ContextBroker } from '@/services/contextBroker'
import { IS_DEMO } from '@/composables/useDemoIntro'
@@ -72,6 +72,7 @@ import { IS_DEMO } from '@/composables/useDemoIntro'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const aiuiFrame = ref<HTMLIFrameElement | null>(null)
const aiuiConnected = ref(false)
// Belt-and-suspenders backstop (2026-07-30 live-testing follow-up): the
@@ -109,6 +110,47 @@ const aiuiUrl = computed(() => {
return ''
})
// ⌘K → "Talk to AIUI about it" hands the typed text over as `?ask=`.
//
// It is delivered by postMessage, NOT by adding a query param to `aiuiUrl`.
// That is deliberate: the comment on D14_FLAGS above explains that aiuiUrl must
// have no reactive dependencies so the iframe `src` stays byte-identical and
// AIUI survives a tab switch. Threading `ask` through the URL would rebuild the
// src on every question and reload AIUI, discarding the conversation — the
// exact opposite of what this feature is for.
//
// The ask is queued rather than sent directly, because the common case is
// arriving from ⌘K on a cold Chat tab where the iframe has not handshaked yet.
// `ready` flushes it.
const pendingAsk = ref('')
function flushAsk() {
const text = pendingAsk.value
if (!text || !aiuiConnected.value) return
const frame = aiuiFrame.value
if (!frame?.contentWindow || !aiuiUrl.value) return
let targetOrigin: string
try {
targetOrigin = new URL(aiuiUrl.value, window.location.origin).origin
} catch { return }
frame.contentWindow.postMessage({ type: 'chat:prefill', text }, targetOrigin)
pendingAsk.value = ''
// Drop ask/askedAt from the URL so a refresh or a back-nav does not re-ask.
const { ask: _a, askedAt: _t, ...rest } = route.query
router.replace({ path: route.path, query: rest })
}
watch(
() => route.query.askedAt,
() => {
const ask = route.query.ask
if (!ask) return
pendingAsk.value = String(ask)
flushAsk()
},
{ immediate: true },
)
function closeChat() {
if (window.history.length > 1) {
router.back()
@@ -128,6 +170,8 @@ function onAiuiMessage(event: MessageEvent) {
if (event.data?.type === 'ready') {
aiuiConnected.value = true
if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null }
// A ⌘K ask that arrived before the handshake is waiting — send it now.
flushAsk()
}
}