From eb35c4d66a2dbe0a189694b2bf6ccd93701ee85b Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Sat, 8 Aug 2026 01:34:44 +0800 Subject: [PATCH] refactor(*): cleaner provider design, simplified chat store --- .../pages/devtools/performance-playground.vue | 13 +- apps/stage-pocket/src/pages/index.vue | 8 +- apps/stage-tamagotchi/src/renderer/App.vue | 76 +- .../renderer/components/InteractiveArea.vue | 51 +- .../src/renderer/layouts/settings.vue | 12 +- apps/stage-tamagotchi/src/renderer/main.ts | 4 + .../src/renderer/pages/index.vue | 11 +- .../renderer/pages/settings/data/index.vue | 7 +- .../pages/settings/system/developer.vue | 21 +- .../src/renderer/pages/spotlight.vue | 21 +- .../stores/chat-sync-lifecycle.test.ts | 89 - .../renderer/stores/chat-sync-lifecycle.ts | 51 - .../src/renderer/stores/chat-sync.test.ts | 691 ------ .../src/renderer/stores/chat-sync.ts | 748 ------- .../src/renderer/stores/plugin-tools.ts | 77 - .../renderer/stores/tools/built-in.test.ts | 51 + .../src/renderer/stores/tools/built-in.ts | 52 + .../src/renderer/stores/tools/index.ts | 7 + .../{mcp-tools.test.ts => tools/mcp.test.ts} | 28 +- .../stores/{mcp-tools.ts => tools/mcp.ts} | 40 +- .../plugins.test.ts} | 46 +- .../src/renderer/stores/tools/plugins.ts | 79 + .../src/renderer/window-route.test.ts | 13 + .../src/renderer/window-route.ts | 10 + apps/stage-web/src/main.ts | 4 + .../pages/devtools/performance-playground.vue | 13 +- apps/stage-web/src/pages/index.vue | 8 +- packages/core-agent/src/index.ts | 1 + .../runtime/chat-orchestrator-runtime.test.ts | 20 + .../src/runtime/chat-orchestrator-runtime.ts | 7 +- packages/core-agent/src/types/chat.ts | 13 +- .../components/Layouts/InteractiveArea.vue | 4 +- .../Layouts/MobileInteractiveArea.vue | 12 +- .../src/components/Widgets/ChatArea.vue | 13 +- .../composables/use-transcriptions.test.ts | 4 +- .../src/composables/use-transcriptions.ts | 4 +- .../stage-layouts/src/layouts/settings.vue | 12 +- .../src/pages/devtools/context-flow/index.vue | 84 +- .../components/CardCreationDialog.vue | 10 +- .../pages/settings/modules/consciousness.vue | 9 +- .../src/pages/settings/modules/hearing.vue | 8 +- .../src/pages/settings/modules/speech.vue | 10 +- .../src/pages/settings/modules/vision.vue | 9 +- .../settings/providers/chat/[providerId].vue | 8 +- .../providers/chat/amazon-bedrock.vue | 6 +- .../providers/chat/azure-ai-foundry.vue | 6 +- .../providers/chat/cloudflare-workers-ai.vue | 6 +- .../settings/providers/chat/lm-studio.vue | 6 +- .../providers/chat/official-provider.vue | 7 +- .../pages/settings/providers/chat/ollama.vue | 12 +- .../src/pages/settings/providers/index.vue | 10 +- .../speech/alibaba-cloud-model-studio.vue | 26 +- .../providers/speech/comet-api-speech.vue | 10 +- .../providers/speech/deepgram-tts.vue | 15 +- .../settings/providers/speech/elevenlabs.vue | 34 +- .../speech/google-gemini-audio-speech.vue | 10 +- .../providers/speech/index-tts-vllm.vue | 10 +- .../providers/speech/kokoro-local.vue | 34 +- .../providers/speech/microsoft-speech.vue | 10 +- .../providers/speech/mimo-audio-speech.vue | 10 +- .../official-provider-speech-streaming.vue | 16 +- .../speech/official-provider-speech.vue | 7 +- .../providers/speech/openai-audio-speech.vue | 14 +- .../speech/openai-compatible-audio-speech.vue | 10 +- .../speech/openrouter-audio-speech.vue | 10 +- .../providers/speech/player2-speech.vue | 15 +- .../settings/providers/speech/volcengine.vue | 22 +- .../aliyun-nls-transcription.vue | 8 +- .../transcription/browser-web-speech-api.vue | 16 +- .../transcription/comet-api-transcription.vue | 10 +- .../mimo-audio-transcription.vue | 10 +- .../official-provider-transcription.vue | 4 +- .../openai-audio-transcription.vue | 12 +- .../openai-compatible-audio-transcription.vue | 19 +- .../providers/vision/[providerId].vue | 8 +- .../providers/vision/amazon-bedrock.vue | 6 +- .../providers/vision/azure-ai-foundry.vue | 6 +- .../vision/cloudflare-workers-ai.vue | 6 +- .../settings/providers/vision/lm-studio.vue | 6 +- .../providers/vision/official-provider.vue | 7 +- .../settings/providers/vision/ollama.vue | 12 +- .../src/pages/v2/settings/providers.vue | 18 +- .../providers/edit/[providerId]/index.vue | 67 +- packages/stage-ui/package.json | 2 + .../src/components/menu/icon-status-item.vue | 6 +- .../dialogs/onboarding/onboarding.vue | 12 +- .../step-provider-configuration.vue | 19 +- .../onboarding/step-provider-selection.vue | 2 +- .../scenarios/providers/speech-playground.vue | 2 +- .../providers/speech-provider-settings.vue | 27 +- .../transcription-provider-settings.vue | 23 +- .../stage-ui/src/components/scenes/Stage.vue | 13 +- .../use-auth-provider-sync.test.ts | 6 +- .../src/composables/use-auth-provider-sync.ts | 8 +- .../src/composables/use-data-maintenance.ts | 8 +- .../src/composables/use-inference-preload.ts | 8 +- .../composables/use-provider-validation.ts | 27 +- .../vision/use-vision-inference.test.ts | 4 +- .../vision/use-vision-inference.ts | 4 +- packages/stage-ui/src/libs/pinia/index.ts | 1 + .../stage-ui/src/libs/pinia/setup-synced.ts | 60 + .../{source-metadata.ts => attributes.ts} | 50 +- packages/stage-ui/src/libs/providers/index.ts | 3 +- .../src/libs/providers/metadata.test.ts | 75 + .../stage-ui/src/libs/providers/metadata.ts | 116 + .../providers/providers/official/index.ts | 2 +- packages/stage-ui/src/libs/providers/types.ts | 15 + .../inference-service-providers.test.ts | 104 - .../src/models/inference-service-providers.ts | 122 - .../inference-service-providers.test.ts | 11 +- .../services/inference-service-providers.ts | 74 +- .../character/orchestrator/index.test.ts | 6 +- .../stores/character/orchestrator/store.ts | 4 +- .../stage-ui/src/stores/chat.contract.test.ts | 97 +- packages/stage-ui/src/stores/chat.ts | 214 +- .../stage-ui/src/stores/chat/maintenance.ts | 13 +- .../src/stores/chat/session-store.test.ts | 31 + .../stage-ui/src/stores/chat/session-store.ts | 42 +- .../stage-ui/src/stores/chat/stream-store.ts | 6 + packages/stage-ui/src/stores/index.ts | 3 +- .../stage-ui/src/stores/llm-tool-resolver.ts | 4 +- .../stage-ui/src/stores/llm-tools.test.ts | 183 +- packages/stage-ui/src/stores/llm-tools.ts | 142 +- packages/stage-ui/src/stores/llm.test.ts | 80 +- .../stage-ui/src/stores/markdown-stress.ts | 10 +- .../stores/mods/api/channel-server.test.ts | 29 +- .../src/stores/mods/api/channel-server.ts | 53 +- .../context-bridge-performance-call.test.ts | 6 +- .../context-bridge.contract.browser.test.ts | 51 +- .../src/stores/mods/api/context-bridge.ts | 58 +- .../stores/mods/api/context-channel.test.ts | 38 + .../src/stores/mods/api/context-channel.ts | 64 + .../src/stores/modules/artistry-autonomous.ts | 4 +- .../src/stores/modules/consciousness.test.ts | 40 +- .../src/stores/modules/consciousness.ts | 10 +- .../stage-ui/src/stores/modules/hearing.ts | 21 +- .../src/stores/modules/speech.test.ts | 22 +- .../stage-ui/src/stores/modules/speech.ts | 16 +- .../src/stores/modules/vision/store.ts | 17 +- packages/stage-ui/src/stores/onboarding.ts | 8 +- .../src/stores/provider-catalog.test.ts | 156 -- .../stage-ui/src/stores/provider-catalog.ts | 198 -- packages/stage-ui/src/stores/providers.ts | 914 -------- .../src/stores/providers/config.test.ts | 120 + .../stage-ui/src/stores/providers/config.ts | 261 +++ .../src/stores/providers/converters.test.ts | 108 - .../src/stores/providers/converters.ts | 298 --- .../providers/google-gemini-speech.test.ts | 372 ---- .../stores/providers/google-gemini-speech.ts | 228 -- .../openai-compatible-builder.test.ts | 36 - .../providers/openai-compatible-builder.ts | 297 --- .../providers/openrouter/audio-speech.ts | 239 -- .../src/stores/providers/provider.test.ts | 67 + .../stage-ui/src/stores/providers/provider.ts | 1016 +++++++++ packages/stage-ui/src/types/chat.ts | 1 + pnpm-lock.yaml | 1955 +++++++++++++++-- pnpm-workspace.yaml | 1 + 157 files changed, 5343 insertions(+), 5980 deletions(-) delete mode 100644 apps/stage-tamagotchi/src/renderer/stores/chat-sync-lifecycle.test.ts delete mode 100644 apps/stage-tamagotchi/src/renderer/stores/chat-sync-lifecycle.ts delete mode 100644 apps/stage-tamagotchi/src/renderer/stores/chat-sync.test.ts delete mode 100644 apps/stage-tamagotchi/src/renderer/stores/chat-sync.ts delete mode 100644 apps/stage-tamagotchi/src/renderer/stores/plugin-tools.ts create mode 100644 apps/stage-tamagotchi/src/renderer/stores/tools/built-in.test.ts create mode 100644 apps/stage-tamagotchi/src/renderer/stores/tools/built-in.ts create mode 100644 apps/stage-tamagotchi/src/renderer/stores/tools/index.ts rename apps/stage-tamagotchi/src/renderer/stores/{mcp-tools.test.ts => tools/mcp.test.ts} (73%) rename apps/stage-tamagotchi/src/renderer/stores/{mcp-tools.ts => tools/mcp.ts} (52%) rename apps/stage-tamagotchi/src/renderer/stores/{plugin-tools.test.ts => tools/plugins.test.ts} (77%) create mode 100644 apps/stage-tamagotchi/src/renderer/stores/tools/plugins.ts create mode 100644 apps/stage-tamagotchi/src/renderer/window-route.test.ts create mode 100644 apps/stage-tamagotchi/src/renderer/window-route.ts create mode 100644 packages/stage-ui/src/libs/pinia/index.ts create mode 100644 packages/stage-ui/src/libs/pinia/setup-synced.ts rename packages/stage-ui/src/libs/providers/{source-metadata.ts => attributes.ts} (71%) create mode 100644 packages/stage-ui/src/libs/providers/metadata.test.ts create mode 100644 packages/stage-ui/src/libs/providers/metadata.ts delete mode 100644 packages/stage-ui/src/models/inference-service-providers.test.ts delete mode 100644 packages/stage-ui/src/models/inference-service-providers.ts create mode 100644 packages/stage-ui/src/stores/mods/api/context-channel.test.ts create mode 100644 packages/stage-ui/src/stores/mods/api/context-channel.ts delete mode 100644 packages/stage-ui/src/stores/provider-catalog.test.ts delete mode 100644 packages/stage-ui/src/stores/provider-catalog.ts delete mode 100644 packages/stage-ui/src/stores/providers.ts create mode 100644 packages/stage-ui/src/stores/providers/config.test.ts create mode 100644 packages/stage-ui/src/stores/providers/config.ts delete mode 100644 packages/stage-ui/src/stores/providers/converters.test.ts delete mode 100644 packages/stage-ui/src/stores/providers/converters.ts delete mode 100644 packages/stage-ui/src/stores/providers/google-gemini-speech.test.ts delete mode 100644 packages/stage-ui/src/stores/providers/google-gemini-speech.ts delete mode 100644 packages/stage-ui/src/stores/providers/openai-compatible-builder.test.ts delete mode 100644 packages/stage-ui/src/stores/providers/openai-compatible-builder.ts delete mode 100644 packages/stage-ui/src/stores/providers/openrouter/audio-speech.ts create mode 100644 packages/stage-ui/src/stores/providers/provider.test.ts create mode 100644 packages/stage-ui/src/stores/providers/provider.ts diff --git a/apps/stage-pocket/src/pages/devtools/performance-playground.vue b/apps/stage-pocket/src/pages/devtools/performance-playground.vue index 52e20ee34..685d2dd1e 100644 --- a/apps/stage-pocket/src/pages/devtools/performance-playground.vue +++ b/apps/stage-pocket/src/pages/devtools/performance-playground.vue @@ -9,12 +9,13 @@ import { useDelayMessageQueue, useEmotionsMessageQueue } from '@proj-airi/stage- import { llmInferenceEndToken } from '@proj-airi/stage-ui/constants' import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionThinkMotionName } from '@proj-airi/stage-ui/constants/emotions' import { useAudioContext, useSpeakingStore } from '@proj-airi/stage-ui/stores/audio' -import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat' +import { useChatStore } from '@proj-airi/stage-ui/stores/chat' import { useChatMaintenanceStore } from '@proj-airi/stage-ui/stores/chat/maintenance' import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store' import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness' import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech' -import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' +import { useProviderConfigStore } from '@proj-airi/stage-ui/stores/providers/config' +import { useProviderStore } from '@proj-airi/stage-ui/stores/providers/provider' import { useSettings } from '@proj-airi/stage-ui/stores/settings' import { createQueue } from '@proj-airi/stream-kit' import { generateSpeech } from '@xsai/generate-speech' @@ -43,7 +44,9 @@ onMounted(async () => { setupAnalyser() }) -const providersStore = useProvidersStore() +const providersStore = useProviderStore() + +const providerStore = useProviderConfigStore() const speechStore = useSpeechStore() const { activeSpeechProvider, activeSpeechVoice, activeSpeechModel, ssmlEnabled, pitch } = storeToRefs(speechStore) const consciousnessStore = useConsciousnessStore() @@ -77,7 +80,7 @@ const { mouthOpenSize } = storeToRefs(useSpeakingStore()) const nowSpeaking = ref(false) const logLines = ref([]) const chatInput = ref('') -const chatOrchestrator = useChatOrchestratorStore() +const chatOrchestrator = useChatStore() const chatSession = useChatSessionStore() const chatMaintenance = useChatMaintenanceStore() const chatMessages = computed(() => { @@ -156,7 +159,7 @@ const speechPipeline = createSpeechPipeline({ if (!request.text && !request.special) return null - const providerConfig = providersStore.getProviderConfig(activeSpeechProvider.value) + const providerConfig = providerStore.getProviderConfig(activeSpeechProvider.value) const input = ssmlEnabled.value ? speechStore.generateSSML(request.text, activeSpeechVoice.value, { ...providerConfig, pitch: pitch.value }) : request.text diff --git a/apps/stage-pocket/src/pages/index.vue b/apps/stage-pocket/src/pages/index.vue index 963ac98ed..264d54cdd 100644 --- a/apps/stage-pocket/src/pages/index.vue +++ b/apps/stage-pocket/src/pages/index.vue @@ -14,10 +14,10 @@ import { IS_DEV } from '@proj-airi/stage-shared' import { ViewControlSlider, WidgetStage } from '@proj-airi/stage-ui/components/scenes' import { useAudioRecorder } from '@proj-airi/stage-ui/composables/audio/audio-recorder' import { useVAD } from '@proj-airi/stage-ui/stores/ai/models/vad' -import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat' +import { useChatStore } from '@proj-airi/stage-ui/stores/chat' import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness' import { useHearingSpeechInputPipeline } from '@proj-airi/stage-ui/stores/modules/hearing' -import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' +import { useProviderStore } from '@proj-airi/stage-ui/stores/providers/provider' import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings' import { breakpointsTailwind, useBreakpoints, useMouse } from '@vueuse/core' import { storeToRefs } from 'pinia' @@ -50,10 +50,10 @@ const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream) const hearingPipeline = useHearingSpeechInputPipeline() const { transcribeForRecording, transcribeForMediaStream, stopStreamingTranscription } = hearingPipeline const { supportsStreamInput } = storeToRefs(hearingPipeline) -const providersStore = useProvidersStore() +const providersStore = useProviderStore() const consciousnessStore = useConsciousnessStore() const { activeProvider: activeChatProvider, activeModel: activeChatModel } = storeToRefs(consciousnessStore) -const chatStore = useChatOrchestratorStore() +const chatStore = useChatStore() const shouldUseStreamInput = computed(() => supportsStreamInput.value && !!stream.value) diff --git a/apps/stage-tamagotchi/src/renderer/App.vue b/apps/stage-tamagotchi/src/renderer/App.vue index 71fa7b64b..0d3d6de46 100644 --- a/apps/stage-tamagotchi/src/renderer/App.vue +++ b/apps/stage-tamagotchi/src/renderer/App.vue @@ -6,8 +6,10 @@ import { artistrySyncConfig } from '@proj-airi/stage-shared' import { ToasterRoot } from '@proj-airi/stage-ui/components' import { useInferencePreload } from '@proj-airi/stage-ui/composables' import { useAuthProviderSync } from '@proj-airi/stage-ui/composables/use-auth-provider-sync' +import { usePiniaSynced } from '@proj-airi/stage-ui/libs/pinia' import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics' import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character' +import { useChatStore } from '@proj-airi/stage-ui/stores/chat' import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store' import { usePluginHostInspectorStore } from '@proj-airi/stage-ui/stores/devtools/plugin-host-debug' import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models' @@ -53,11 +55,14 @@ import { electronPluginToolsChanged } from '../shared/eventa/plugin/tools' import { initializeElectronAuthCallbackBridge } from './bridges/electron-auth-callback' import { initializeStageThreeRuntimeTraceBridge } from './bridges/stage-three-runtime-trace' import { useLanguage } from './composables/use-language' -import { createChatSyncWindowLifecycle, resolveInitialChatSyncRoutePath } from './stores/chat-sync-lifecycle' -import { useTamagotchiMcpToolsStore } from './stores/mcp-tools' -import { useTamagotchiPluginToolsStore } from './stores/plugin-tools' import { useServerChannelSettingsStore } from './stores/settings/server-channel' import { useStageWindowLifecycleStore } from './stores/stage-window-lifecycle' +import { + useTamagotchiBuiltinToolsStore, + useTamagotchiMcpToolsStore, + useTamagotchiPluginToolsStore, +} from './stores/tools' +import { resolveInitialWindowRoutePath } from './window-route' const { isDark: dark } = useTheme() const settingsStore = useSettings() @@ -68,14 +73,44 @@ const chatSessionStore = useChatSessionStore() const context = useElectronEventaContext() const getMainLocale = useElectronEventaInvoke(i18nGetLocale) const setLocale = useElectronEventaInvoke(i18nSetLocale) -const initialWindowRoutePath = resolveInitialChatSyncRoutePath(route.path) -const chatSyncLifecycle = createChatSyncWindowLifecycle(route.path) +const initialWindowRoutePath = resolveInitialWindowRoutePath(route.path) +useChatStore() +const builtinToolsStore = useTamagotchiBuiltinToolsStore() +const mcpToolsStore = useTamagotchiMcpToolsStore() +const pluginToolsStore = useTamagotchiPluginToolsStore() +const syncedPinia = usePiniaSynced() const isSpotlightWindowRoute = initialWindowRoutePath === '/spotlight' const isSettingsWindowRoute = initialWindowRoutePath === '/settings' || initialWindowRoutePath.startsWith('/settings/') const isEditorWindowRoute = initialWindowRoutePath === '/editor' -if (!isSpotlightWindowRoute) - useAuthProviderSync() +// Every renderer participates in leader election. Keep provider state ready in +// auxiliary windows so a newly elected leader can execute chat actions after +// the previous window closes. +useAuthProviderSync() + +async function refreshPluginRuntimeTools() { + try { + await pluginToolsStore.refresh() + } + catch (error) { + console.warn('[App] Failed to refresh plugin runtime tools:', error) + } +} + +// Every renderer creates the runtime tool stores because every renderer can +// become the leader. Only the leader discovers tools and keeps executors. +const stopToolLeadershipListener = syncedPinia.onLeadershipChange((isLeader) => { + if (!isLeader) + return + + void builtinToolsStore.refresh().catch((error) => { + console.warn('[App] Failed to refresh built-in runtime tools:', error) + }) + void mcpToolsStore.refresh().catch((error) => { + console.warn('[App] Failed to refresh MCP runtime tools:', error) + }) + void refreshPluginRuntimeTools() +}) function createFullStageRuntime() { const contextBridgeStore = useContextBridgeStore() @@ -87,8 +122,6 @@ function createFullStageRuntime() { const analyticsStore = useSharedAnalyticsStore() const inferencePreload = useInferencePreload() const pluginHostInspectorStore = usePluginHostInspectorStore() - const mcpToolsStore = useTamagotchiMcpToolsStore() - const pluginToolsStore = useTamagotchiPluginToolsStore() const stageWindowLifecycleStore = useStageWindowLifecycleStore() const settingsAudioDeviceStore = useSettingsAudioDevice() const artistryStore = useArtistryStore() @@ -119,15 +152,6 @@ function createFullStageRuntime() { settingsStore.restoreBuiltInStageModelRenderer() } - async function refreshPluginRuntimeTools() { - try { - await pluginToolsStore.refresh() - } - catch (error) { - console.warn('[App] Failed to refresh plugin runtime tools:', error) - } - } - usePerfTracerBridgeStore() initializeStageThreeRuntimeTraceBridge() initializeElectronAuthCallbackBridge() @@ -164,13 +188,6 @@ function createFullStageRuntime() { inspect: () => inspectPluginHost(), }) - // NOTICE: Runtime tool stores must register during setup so renderer consumers can see them - // before `onMounted()` finishes the rest of the startup flow. - void mcpToolsStore.refresh().catch((error) => { - console.warn('[App] Failed to refresh MCP runtime tools:', error) - }) - void refreshPluginRuntimeTools() - watch([activeProvider, artistryGlobals, activeModel, defaultPromptPrefix, providerOptions], () => { if (activeProvider.value) { void syncArtistryConfig({ @@ -248,8 +265,6 @@ function createFullStageRuntime() { dispose() { if (!isAuxiliaryChatRoute) contextBridgeStore.dispose() - mcpToolsStore.dispose() - pluginToolsStore.dispose() }, } } @@ -277,8 +292,6 @@ if (isSettingsWindowRoute) { } onMounted(async () => { - chatSyncLifecycle.initialize() - // NOTICE: Issue #1658 // When Electron restarts, renderer localStorage may not be flushed to disk. // The store's onMounted hook falls back to navigator.language, which triggers @@ -292,10 +305,6 @@ onMounted(async () => { await fullStageRuntime?.initialize() }) -onUnmounted(() => { - chatSyncLifecycle.dispose() -}) - watch(themeColorsHue, () => { document.documentElement.style.setProperty('--chromatic-hue', themeColorsHue.value.toString()) }, { immediate: true }) @@ -305,6 +314,7 @@ watch(themeColorsHueDynamic, () => { }, { immediate: true }) onUnmounted(() => { + stopToolLeadershipListener?.() fullStageRuntime?.dispose() }) diff --git a/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue b/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue index 4ddb9bb92..fdf618593 100644 --- a/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue +++ b/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue @@ -2,12 +2,11 @@ import type { ChatToolCallRendererRegistry } from '@proj-airi/stage-ui/components' import type { ChatHistoryItem } from '@proj-airi/stage-ui/types/chat' -import { errorMessageFrom } from '@moeru/std' import { useStopSpeakingButton } from '@proj-airi/stage-layouts/composables/useStopSpeakingButton' import { ChatHistory, JournalPreviewModal } from '@proj-airi/stage-ui/components' import { useAnalytics } from '@proj-airi/stage-ui/composables/use-analytics' import { useBackgroundStore } from '@proj-airi/stage-ui/stores/background' -import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat' +import { useChatStore } from '@proj-airi/stage-ui/stores/chat' import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store' import { useChatStreamStore } from '@proj-airi/stage-ui/stores/chat/stream-store' import { useJournalPreviewStore } from '@proj-airi/stage-ui/stores/journal-preview' @@ -22,24 +21,23 @@ import { useRouter } from 'vue-router' import JournalToolCallBlock from './chat-tool-renderers/journal-tool-call-block.vue' -import { useChatSyncStore } from '../stores/chat-sync' +import { artistryToolReferences, widgetToolReferences } from '../stores/tools' const router = useRouter() const messageInput = ref('') const lastEnterTime = ref(0) const attachments = ref<{ type: 'image', data: string, mimeType: string, url: string }[]>([]) -const chatOrchestrator = useChatOrchestratorStore() +const chatStore = useChatStore() const chatSession = useChatSessionStore() const chatStream = useChatStreamStore() -const chatSyncStore = useChatSyncStore() const backgroundStore = useBackgroundStore() const journalPreviewStore = useJournalPreviewStore() const airiCardStore = useAiriCardStore() const { messages } = storeToRefs(chatSession) const { streamingMessage } = storeToRefs(chatStream) -const { sending } = storeToRefs(chatOrchestrator) +const { sending } = storeToRefs(chatStore) const { activeCard, activeCardId } = storeToRefs(airiCardStore) const { t } = useI18n() const { openImagePreview } = journalPreviewStore @@ -48,7 +46,6 @@ const DOUBLE_ENTER_INTERVAL_MS = 300 const TRAILING_NEWLINES_REGEX = /[\r\n]+$/ const SEND_MODES = ['enter', 'ctrl-enter', 'double-enter'] as const type SendMode = (typeof SEND_MODES)[number] -type ToolCallRerunToolset = 'widgets' | 'artistry' const sendMode = useLocalStorage('ui/chat/settings/send-mode', 'enter') const toolCallRenderers = { image_journal: JournalToolCallBlock, @@ -95,25 +92,19 @@ async function handleSend() { attachments.value = [] try { - await chatSyncStore.requestIngest({ + await chatStore.send({ + sessionId: chatSession.activeSessionId, text: textToSend, attachments: attachmentsToSend, - toolset: 'artistry', + tools: artistryToolReferences, }) attachmentsToSend.forEach(att => URL.revokeObjectURL(att.url)) } - catch (error) { + catch { // restore on failure messageInput.value = textToSend attachments.value = attachmentsToSend - chatSession.setSessionMessages(chatSession.activeSessionId, [ - ...messages.value, - { - role: 'error', - content: errorMessageFrom(error) ?? 'Failed to send message', - }, - ]) } } @@ -207,7 +198,10 @@ const assistantLabel = computed(() => activeCard.value?.name?.trim() || undefine async function handleDeleteMessage(index: number) { const message = messages.value[index] - await chatSyncStore.requestDeleteMessage({ index }) + await chatSession.deleteMessage({ + sessionId: chatSession.activeSessionId, + index, + }) trackChatMessageDeleted({ source: 'history', message_role: message?.role ?? 'unknown', @@ -219,34 +213,21 @@ onMounted(() => { }) async function handleRetryMessage(index: number) { - await chatSyncStore.requestRetry({ + await chatStore.retry({ sessionId: chatSession.activeSessionId, index, + tools: widgetToolReferences, }) trackChatMessageRetried({ source: 'history', }) } -function resolveToolCallRerunToolset(toolName: string): ToolCallRerunToolset | undefined { - // TODO: Stop hardcoding tool names to app-local toolsets. Tool registration - // should expose the owning runtime/toolset id so reruns can reuse the exact - // source that created the original tool call. - if (toolName === 'image_journal' || toolName === 'text_journal') - return 'artistry' - - if (toolName === 'stage_widgets' || toolName === 'get_weather') - return 'widgets' - - return undefined -} - async function handleToolCallRerun(payload: { message: ChatHistoryItem, index: number, key: string | number, toolCallId: string, toolName: string, args: string }) { - await chatSyncStore.requestToolCallRerun({ + await chatStore.rerunToolCall({ sessionId: chatSession.activeSessionId, messageId: payload.message.id, index: payload.index, - toolset: resolveToolCallRerunToolset(payload.toolName), toolCallId: payload.toolCallId, toolName: payload.toolName, args: payload.args, @@ -255,7 +236,7 @@ async function handleToolCallRerun(payload: { message: ChatHistoryItem, index: n async function handleCleanupMessages() { const messageCount = messages.value.filter(message => message.role !== 'system').length - await chatSyncStore.requestCleanup() + await chatStore.cleanup(chatSession.activeSessionId) trackChatMessagesCleared({ source: 'chat_controls', message_count: messageCount, diff --git a/apps/stage-tamagotchi/src/renderer/layouts/settings.vue b/apps/stage-tamagotchi/src/renderer/layouts/settings.vue index adced4847..97fbdd7f7 100644 --- a/apps/stage-tamagotchi/src/renderer/layouts/settings.vue +++ b/apps/stage-tamagotchi/src/renderer/layouts/settings.vue @@ -1,7 +1,7 @@ diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue index 7d46f6d01..185928748 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue @@ -6,7 +6,7 @@ import { computed } from 'vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' -import { electronOpenDevtoolsWindow, electronOpenMainDevtools } from '../../../../shared/eventa' +import { electronOpenDevtoolsWindow, electronOpenEditor, electronOpenMainDevtools } from '../../../../shared/eventa' const { t } = useI18n() const settings = useSettings() @@ -101,12 +101,13 @@ const menu = computed(() => [ const openDevTools = useElectronEventaInvoke(electronOpenMainDevtools) const openDevtoolsWindow = useElectronEventaInvoke(electronOpenDevtoolsWindow) +const openEditor = useElectronEventaInvoke(electronOpenEditor)