feat(app): delegate chat to Archy's node-side assistant loop when embedded (Archy phase 13, D-01/D-17)
Adds archyBridge.sendChat(text) built on the existing postToParent + origin-validated listener pattern (same request-id correlation as requestContext), with a 180s timeout matching the node's ASSISTANT_HTTP_TIMEOUT. Adds useAI.ts's streamViaArchy, which branches all three existing send sites on the same __AIUI_EMBEDDED__ signal useArchy.ts already reads: embedded mode delegates the model call, the tool-calling loop and the model key to the node; standalone mode is untouched and keeps using streamClaude/streamOpenRouter with AIUI's own dev proxy (D-17). CLAUDE_PATH/OPENROUTER_PATH are not removed — 13-02 changes what those paths resolve to on a node, 13-09 retires them. Verified: vitest run 332/335 passing (3 pre-existing failures confirmed via a scratch worktree at the prior HEAD, unrelated to this change — seed extraction count assertions and a web-search-integration body.webSearch assertion); vue-tsc --noEmit clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import type { ImageAttachment } from '@aiui/core/types/message'
|
||||
import { usePersonaStore } from '@/stores/personas'
|
||||
import { useMemoryStore } from '@/stores/memory'
|
||||
import { useArchy } from '@/composables/useArchy'
|
||||
import { archyBridge } from '@/services/archyBridge'
|
||||
import { useCodeContext } from '@/composables/useCodeContext'
|
||||
import { apiFetch } from '@/utils/api-fetch'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
@@ -374,6 +375,33 @@ async function streamOpenRouter(
|
||||
}, onError, signal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Embedded-mode chat delegation (D-01/D-17): when AIUI is running inside
|
||||
* Archy, the model call, the tool-calling loop, and the model key all live
|
||||
* node-side. This sends only the latest user turn over the existing
|
||||
* origin-checked postMessage bridge (`archyBridge.sendChat`) and emits the
|
||||
* node's final answer as a single token — there is no streaming across the
|
||||
* bridge in this tracer, only a one-shot `chat:request`/`chat:response`
|
||||
* round trip.
|
||||
*/
|
||||
async function streamViaArchy(
|
||||
messages: ChatMessage[],
|
||||
onToken: (text: string) => void,
|
||||
onError: (err: string) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
const lastUser = [...messages].reverse().find((m) => m.role === 'user')
|
||||
const text = lastUser?.content ?? ''
|
||||
try {
|
||||
const result = await archyBridge.sendChat(text)
|
||||
if (signal?.aborted) return
|
||||
onToken(result.text)
|
||||
} catch (err) {
|
||||
if (signal?.aborted) return
|
||||
onError(err instanceof Error ? err.message : 'Archy chat request failed')
|
||||
}
|
||||
}
|
||||
|
||||
async function readSSE(
|
||||
res: Response,
|
||||
onData: (data: string) => void,
|
||||
@@ -560,7 +588,11 @@ export async function streamWithModel(
|
||||
activeModel.value = model
|
||||
|
||||
try {
|
||||
if (provider === 'claude') {
|
||||
if (useArchy().isEmbedded.value) {
|
||||
// D-17: embedded mode delegates the loop, the tools and the key to
|
||||
// Archy — provider/model selection here doesn't apply node-side.
|
||||
await streamViaArchy(history, onToken, onError, signal)
|
||||
} else if (provider === 'claude') {
|
||||
await streamClaude(history, onToken, onError, 'You are a helpful assistant.', false, signal)
|
||||
} else if (provider === 'openrouter') {
|
||||
await streamOpenRouter(history, onToken, onError, 'You are a helpful assistant.', signal)
|
||||
@@ -643,7 +675,11 @@ export function useAI() {
|
||||
const genParams = getConversationParams(chatStore)
|
||||
|
||||
try {
|
||||
if (provider === 'claude') {
|
||||
if (useArchy().isEmbedded.value) {
|
||||
// D-17: embedded mode delegates the loop, the tools and the key to
|
||||
// Archy — provider/model selection here doesn't apply node-side.
|
||||
await streamViaArchy(history, onToken, onError, signal)
|
||||
} else if (provider === 'claude') {
|
||||
await streamClaude(history, onToken, onError, systemPrompt, proxyWebSearch, signal, genParams)
|
||||
} else if (provider === 'openrouter') {
|
||||
await streamOpenRouter(history, onToken, onError, systemPrompt, signal)
|
||||
@@ -755,7 +791,11 @@ export function useAI() {
|
||||
const genParams = getConversationParams(chatStore)
|
||||
|
||||
try {
|
||||
if (provider === 'claude') {
|
||||
if (useArchy().isEmbedded.value) {
|
||||
// D-17: embedded mode delegates the loop, the tools and the key to
|
||||
// Archy — provider/model selection here doesn't apply node-side.
|
||||
await streamViaArchy(history, onToken, onError, signal)
|
||||
} else if (provider === 'claude') {
|
||||
await streamClaude(history, onToken, onError, systemPrompt, chatStore.webSearchEnabled, signal, genParams)
|
||||
} else if (provider === 'openrouter') {
|
||||
await streamOpenRouter(history, onToken, onError, systemPrompt, signal)
|
||||
|
||||
@@ -74,6 +74,19 @@ function handleMessage(event: MessageEvent) {
|
||||
break
|
||||
}
|
||||
|
||||
case 'chat:response': {
|
||||
const pending = pendingRequests.get(msg.id)
|
||||
if (pending) {
|
||||
pendingRequests.delete(msg.id)
|
||||
if (msg.success) {
|
||||
pending.resolve({ text: msg.text ?? '' })
|
||||
} else {
|
||||
pending.reject(new Error(msg.error || 'Chat request failed'))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'permissions:update': {
|
||||
currentPermissions = msg.categories || []
|
||||
for (const cb of permissionsCallbacks) cb(currentPermissions)
|
||||
@@ -189,6 +202,32 @@ export const archyBridge = {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a chat turn to Archy's node-side assistant loop (D-01: the model
|
||||
* key and the tool-calling loop live on the node, never in this bundle).
|
||||
* Returns the assistant's final text answer for this turn.
|
||||
*/
|
||||
sendChat(text: string): Promise<{ text: string }> {
|
||||
const id = generateId()
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRequests.set(id, { resolve: resolve as (v: unknown) => void, reject })
|
||||
postToParent({
|
||||
type: 'chat:request',
|
||||
id,
|
||||
text,
|
||||
})
|
||||
|
||||
// Matches the node's ASSISTANT_HTTP_TIMEOUT (180s) — the assistant
|
||||
// loop's tool-calling round trip can legitimately take that long.
|
||||
setTimeout(() => {
|
||||
if (pendingRequests.has(id)) {
|
||||
pendingRequests.delete(id)
|
||||
reject(new Error('Chat request timed out'))
|
||||
}
|
||||
}, 180000)
|
||||
})
|
||||
},
|
||||
|
||||
/** Request Archy's theme info */
|
||||
requestTheme() {
|
||||
postToParent({ type: 'theme:request' })
|
||||
|
||||
Reference in New Issue
Block a user