feat(aiui): receive a Cmd+K handoff and prefill the composer

Completes the 'Talk to AIUI about it' path from the other side. archyBridge
gains a chat:prefill case behind its existing parent-origin validation, and
ChatInput prefills + focuses with the caret at the end.

Prefills rather than auto-sends: the operator sees and can amend the question
before it costs a model call, and a draft they had already started is never
clobbered by a background handoff. Auto-send is the natural seam for the
follow-up that actions things directly.

The bridge buffers a prefill that arrives before the composer mounts (collapsed
chat, mobile content tab) and replays it on registration, so a Cmd+K ask into a
cold frame is not silently dropped. onPrefill returns an unsubscribe so a
remounting composer cannot leak a stale handler.

Verified: vue-tsc clean; AIUI suite 332 passed. The 3 remaining failures
(seed-songs extraction x2, web-search system prompt) are pre-existing — I
confirmed by reverting 13-01's two AIUI files to their parent state and
reproducing the identical 3 failures without any phase-13 change present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 16:50:23 -04:00
co-authored by Claude Opus 5
parent 97b3707b5e
commit e1624dda08
2 changed files with 61 additions and 1 deletions
@@ -167,7 +167,8 @@
</template>
<script setup lang="ts">
import { ref, computed, nextTick, watch } from 'vue'
import { ref, computed, nextTick, watch, onMounted, onBeforeUnmount } from 'vue'
import { archyBridge } from '../../services/archyBridge'
import { useFederatedSearch, type SearchResult } from '@/composables/useFederatedSearch'
import SearchResults from '@/components/ui/SearchResults.vue'
import PromptPalette from './PromptPalette.vue'
@@ -212,6 +213,32 @@ const images = ref<ImageAttachment[]>([])
const paletteRef = ref<InstanceType<typeof PromptPalette> | null>(null)
// Archy's ⌘K search can hand us the text the operator typed there
// ("Talk to AIUI about it"). We prefill and focus rather than auto-sending:
// the operator gets to see and amend the question before it costs a model
// call. Anything already half-typed here wins — their own draft is not
// clobbered by a background handoff.
let releasePrefill: (() => void) | null = null
onMounted(() => {
releasePrefill = archyBridge.onPrefill((incoming: string) => {
if (text.value.trim()) return
text.value = incoming
nextTick(() => {
autoResize()
textareaRef.value?.focus()
// Caret to the end so they can keep typing straight away.
const el = textareaRef.value
if (el) el.setSelectionRange(el.value.length, el.value.length)
})
})
})
onBeforeUnmount(() => {
releasePrefill?.()
releasePrefill = null
})
// Prompt palette: opens when text starts with "/" and has no space yet
const isPaletteMode = computed(() => {
const t = text.value.trimStart()
@@ -101,6 +101,19 @@ function handleMessage(event: MessageEvent) {
}
break
}
// Archy's ⌘K search offers "Talk to AIUI about it"; this carries the text
// the operator typed there into our composer.
case 'chat:prefill': {
const text = typeof msg.text === 'string' ? msg.text : ''
if (!text) break
if (prefillCallback) prefillCallback(text)
// Buffer when nothing is listening yet: the composer may not be mounted
// at handshake time (collapsed chat, mobile content tab), and dropping
// the text would silently lose what the operator typed.
else bufferedPrefill = text
break
}
}
}
@@ -116,6 +129,9 @@ function handleMessage(event: MessageEvent) {
* dev server port). `document.referrer` is the standard, cross-origin-safe
* way an iframed document learns its embedding parent's URL.
*/
let prefillCallback: ((text: string) => void) | null = null
let bufferedPrefill = ''
function deriveParentOrigin(): string {
if (typeof document !== 'undefined' && document.referrer) {
try {
@@ -142,6 +158,23 @@ export const archyBridge = {
postToParent({ type: 'ready' })
},
/**
* Register the composer's prefill handler. Replays a prefill that arrived
* before the composer mounted, so a ⌘K ask is never dropped on a cold frame.
* Returns an unsubscribe so a remounting composer does not leak the old one.
*/
onPrefill(fn: (text: string) => void) {
prefillCallback = fn
if (bufferedPrefill) {
const text = bufferedPrefill
bufferedPrefill = ''
fn(text)
}
return () => {
if (prefillCallback === fn) prefillCallback = null
}
},
/** Clean up listeners */
destroy() {
window.removeEventListener('message', handleMessage)