Guide AI connection setup with private node credentials and explicit providers
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AIConnectionModal ref="connectionSetup" :show="showConnectionSetup" @close="showConnectionSetup = false" @configured="providerConfigured" />
|
||||
|
||||
<!-- Loading indicator while iframe loads. pointer-events:none on the
|
||||
wrapper (see <style>) so this never blocks clicks reaching the
|
||||
iframe underneath even while shown; the bounded timeout below
|
||||
@@ -124,6 +126,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ContextBroker } from '@/services/contextBroker'
|
||||
import ToolConfirmModal from '@/components/ToolConfirmModal.vue'
|
||||
import AIConnectionModal from '@/components/AIConnectionModal.vue'
|
||||
import { AI_PERMISSION_CATEGORIES } from '@/stores/aiPermissions'
|
||||
import { IS_DEMO } from '@/composables/useDemoIntro'
|
||||
|
||||
@@ -133,6 +136,13 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const aiuiFrame = ref<HTMLIFrameElement | null>(null)
|
||||
const aiuiConnected = ref(false)
|
||||
const connectionSetup = ref<InstanceType<typeof AIConnectionModal> | null>(null)
|
||||
const showConnectionSetup = ref(false)
|
||||
function providerConfigured(provider: 'claude' | 'openai' | 'routstr' | 'auto' | 'local', model?: string) {
|
||||
if (!aiuiFrame.value?.contentWindow || !aiuiUrl.value) return
|
||||
aiuiFrame.value.contentWindow.postMessage({ type: 'ai:provider-configured', provider, model }, new URL(aiuiUrl.value, window.location.origin).origin)
|
||||
}
|
||||
onMounted(async () => { if (!IS_DEMO && await connectionSetup.value?.checkNeeded()) showConnectionSetup.value = true })
|
||||
// Belt-and-suspenders backstop (2026-07-30 live-testing follow-up): the
|
||||
// loading overlay must never be able to wedge the UI permanently regardless
|
||||
// of AIUI/backend state — a broken handshake, a misconfigured origin, or the
|
||||
@@ -285,15 +295,21 @@ function openAISettings() {
|
||||
}
|
||||
|
||||
function onAiuiMessage(event: MessageEvent) {
|
||||
if (!aiuiUrl.value) return
|
||||
if (!aiuiUrl.value || event.source !== aiuiFrame.value?.contentWindow) return
|
||||
// Validate origin — only accept messages from AIUI
|
||||
try {
|
||||
const expected = new URL(aiuiUrl.value, window.location.origin).origin
|
||||
if (event.origin !== expected) return
|
||||
} catch { return }
|
||||
if (event.data?.type === 'ai:setup-request') {
|
||||
showConnectionSetup.value = true
|
||||
if (event.data.reason === 'funding') connectionSetup.value?.showRoutstr()
|
||||
return
|
||||
}
|
||||
// Listen for ready messages from AIUI iframe
|
||||
if (event.data?.type === 'ready') {
|
||||
aiuiConnected.value = true
|
||||
if (!IS_DEMO) void connectionSetup.value?.syncSelection()
|
||||
if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null }
|
||||
// A ⌘K ask that arrived before the handshake is waiting — send it now.
|
||||
flushAsk()
|
||||
@@ -362,6 +378,7 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
/* Teleported to body, so this is positioned against the viewport, not the
|
||||
chat panel. Sits above the iframe but below the confirm modal — a
|
||||
blocking decision must always win over a passive offer. */
|
||||
|
||||
@@ -11,6 +11,8 @@ import { KeepAlive, defineComponent, h, ref } from 'vue'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Chat from '../Chat.vue'
|
||||
|
||||
vi.mock('@/components/AIConnectionModal.vue', () => ({ default: { template: '<div />', methods: { checkNeeded: async () => false, syncSelection: async () => {} } } }))
|
||||
|
||||
const routerBackMock = vi.fn()
|
||||
const routerPushMock = vi.fn()
|
||||
const routerReplaceMock = vi.fn()
|
||||
@@ -99,6 +101,7 @@ describe('Chat / AIUI embed URL stability + D-14 defaults (02-07)', () => {
|
||||
Object.defineProperty(frame, 'contentWindow', { configurable: true, value: { postMessage: post } })
|
||||
|
||||
window.dispatchEvent(new MessageEvent('message', {
|
||||
source: (wrapper.find('iframe').element as HTMLIFrameElement).contentWindow,
|
||||
origin: 'http://localhost:5173',
|
||||
data: { type: 'ready' },
|
||||
}))
|
||||
@@ -125,6 +128,7 @@ describe('Chat / AIUI embed URL stability + D-14 defaults (02-07)', () => {
|
||||
Object.defineProperty(frame, 'contentWindow', { configurable: true, value: { postMessage: post } })
|
||||
|
||||
window.dispatchEvent(new MessageEvent('message', {
|
||||
source: (wrapper.find('iframe').element as HTMLIFrameElement).contentWindow,
|
||||
origin: 'http://localhost:5173',
|
||||
data: { type: 'ready' },
|
||||
}))
|
||||
@@ -165,6 +169,7 @@ describe('Chat / AIUI embed URL stability + D-14 defaults (02-07)', () => {
|
||||
const { wrapper } = mountChatInKeepAlive()
|
||||
|
||||
window.dispatchEvent(new MessageEvent('message', {
|
||||
source: (wrapper.find('iframe').element as HTMLIFrameElement).contentWindow,
|
||||
data: { type: 'ready' },
|
||||
origin: 'http://evil.example',
|
||||
}))
|
||||
@@ -177,10 +182,22 @@ describe('Chat / AIUI embed URL stability + D-14 defaults (02-07)', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('ignores ready and setup messages from a different same-origin frame', async () => {
|
||||
const { wrapper } = mountChatInKeepAlive()
|
||||
for (const type of ['ready', 'ai:setup-request']) window.dispatchEvent(new MessageEvent('message', {
|
||||
source: window, origin: 'http://localhost:5173', data: { type },
|
||||
}))
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.chat-loading').exists()).toBe(true)
|
||||
expect(wrapper.find('[title="chat.aiuiConnected"]').exists()).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('aiuiConnected survives a deactivate/reactivate cycle once set by a same-origin ready message', async () => {
|
||||
const { wrapper, show } = mountChatInKeepAlive()
|
||||
|
||||
window.dispatchEvent(new MessageEvent('message', {
|
||||
source: (wrapper.find('iframe').element as HTMLIFrameElement).contentWindow,
|
||||
data: { type: 'ready' },
|
||||
origin: 'http://localhost:5173',
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user