fix(stage-ui,stage-tamagotchi,stage-web): infinite write config causing forever loading
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { OnboardingDialog, OnboardingStepAnalyticsNotice, ToasterRoot } from '@proj-airi/stage-ui/components'
|
||||
import { useAuthProviderSync } from '@proj-airi/stage-ui/composables/use-auth-provider-sync'
|
||||
import { initializeAnalytics, isAnalyticsAvailableInBuild } from '@proj-airi/stage-ui/libs/analytics'
|
||||
import { usePiniaSynced } from '@proj-airi/stage-ui/libs/pinia'
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding'
|
||||
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { useSettingsStageModel } from '@proj-airi/stage-ui/stores/settings/stage-model'
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { StageTransitionGroup } from '@proj-airi/ui-transitions'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@@ -22,9 +26,8 @@ import OnboardingPermissionsStep from './components/onboarding/step-permissions.
|
||||
|
||||
import { getHostWebSocketConnector } from './modules/websocket-bridge'
|
||||
|
||||
useAuthProviderSync()
|
||||
|
||||
const contextBridgeStore = useContextBridgeStore()
|
||||
const authStore = useAuthStore()
|
||||
const i18n = useI18n()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const settingsStore = useSettings()
|
||||
@@ -36,7 +39,11 @@ const settingsAudioDeviceStore = useSettingsAudioDevice()
|
||||
const { showingSetup } = storeToRefs(onboardingStore)
|
||||
const { isDark } = useTheme()
|
||||
const cardStore = useAiriCardStore()
|
||||
const syncedPinia = usePiniaSynced()
|
||||
useArtistryStore()
|
||||
useConsciousnessStore()
|
||||
useSpeechStore()
|
||||
useSettingsStageModel()
|
||||
useVisionStore()
|
||||
|
||||
const primaryColor = computed(() => {
|
||||
return isDark.value
|
||||
@@ -75,8 +82,8 @@ watch(settings.themeColorsHueDynamic, () => {
|
||||
// Initialize first-time setup check when app mounts
|
||||
onMounted(async () => {
|
||||
initializeAnalytics()
|
||||
await authStore.initialize()
|
||||
await displayModelsStore.initialize()
|
||||
cardStore.startRuntime(syncedPinia)
|
||||
await cardStore.initialize()
|
||||
|
||||
if (onboardingStore.needsOnboarding) {
|
||||
@@ -96,7 +103,6 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cardStore.disposeRuntime()
|
||||
contextBridgeStore.dispose()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { ArtistrySyncPayload } from '@proj-airi/stage-shared'
|
||||
|
||||
import { defineInvokeHandler } from '@moeru/eventa'
|
||||
import { useElectronEventaContext, useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { themeColorFromValue, useThemeColor } from '@proj-airi/stage-layouts/composables/theme-color'
|
||||
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 { initializeAnalytics } from '@proj-airi/stage-ui/libs/analytics'
|
||||
import { usePiniaSynced } from '@proj-airi/stage-ui/libs/pinia'
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
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'
|
||||
@@ -17,10 +19,15 @@ import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/c
|
||||
import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
import { usePerfTracerBridgeStore } from '@proj-airi/stage-ui/stores/perf-tracer-bridge'
|
||||
import { listProvidersForPluginHost, shouldPublishPluginHostCapabilities } from '@proj-airi/stage-ui/stores/plugin-host-capabilities'
|
||||
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { useSettingsStageModel } from '@proj-airi/stage-ui/stores/settings/stage-model'
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { isEqual } from 'es-toolkit'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
@@ -84,8 +91,6 @@ chatSessionStore.setCloudSyncOwnership(syncedPinia.isLeader())
|
||||
const isSpotlightWindow = initialRoutePath === '/spotlight'
|
||||
const isSettingsWindow = initialRoutePath === '/settings' || initialRoutePath.startsWith('/settings/')
|
||||
|
||||
useAuthProviderSync()
|
||||
|
||||
async function refreshPluginRuntimeTools() {
|
||||
try {
|
||||
await pluginToolsStore.refresh()
|
||||
@@ -112,6 +117,7 @@ const stopLeadershipListener = syncedPinia.onLeadershipChange((isLeader) => {
|
||||
})
|
||||
|
||||
function createFullStageRuntime() {
|
||||
const authStore = useAuthStore()
|
||||
const contextBridgeStore = useContextBridgeStore()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const serverChannelSettingsStore = useServerChannelSettingsStore()
|
||||
@@ -123,6 +129,10 @@ function createFullStageRuntime() {
|
||||
const stageWindowLifecycleStore = useStageWindowLifecycleStore()
|
||||
const settingsAudioDeviceStore = useSettingsAudioDevice()
|
||||
const artistryStore = useArtistryStore()
|
||||
useConsciousnessStore()
|
||||
useSpeechStore()
|
||||
useSettingsStageModel()
|
||||
useVisionStore()
|
||||
const { activeProvider, artistryGlobals, activeModel, defaultPromptPrefix, providerOptions } = storeToRefs(artistryStore)
|
||||
const getServerChannelConfig = useElectronEventaInvoke(electronGetServerChannelConfig)
|
||||
const listPlugins = useElectronEventaInvoke(electronPluginList)
|
||||
@@ -186,16 +196,25 @@ function createFullStageRuntime() {
|
||||
inspect: () => inspectPluginHost(),
|
||||
})
|
||||
|
||||
let lastSyncedArtistryConfig: ArtistrySyncPayload | undefined
|
||||
watch([activeProvider, artistryGlobals, activeModel, defaultPromptPrefix, providerOptions], () => {
|
||||
if (activeProvider.value) {
|
||||
void syncArtistryConfig({
|
||||
provider: activeProvider.value as string,
|
||||
globals: JSON.parse(JSON.stringify(artistryGlobals.value)),
|
||||
model: activeModel.value,
|
||||
promptPrefix: defaultPromptPrefix.value,
|
||||
options: providerOptions.value,
|
||||
})
|
||||
}
|
||||
if (!activeProvider.value)
|
||||
return
|
||||
|
||||
const config = JSON.parse(JSON.stringify({
|
||||
provider: activeProvider.value,
|
||||
globals: artistryGlobals.value,
|
||||
model: activeModel.value,
|
||||
promptPrefix: defaultPromptPrefix.value,
|
||||
options: providerOptions.value,
|
||||
})) as ArtistrySyncPayload
|
||||
if (isEqual(config, lastSyncedArtistryConfig))
|
||||
return
|
||||
|
||||
// Pinia synchronization applies cloned snapshots in every renderer. Keep
|
||||
// this IPC bridge edge-triggered so equal snapshots do not repeat IO.
|
||||
lastSyncedArtistryConfig = config
|
||||
void syncArtistryConfig(config)
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
context.value.on(electronGodotStageStatusChanged, (event) => {
|
||||
@@ -213,8 +232,8 @@ function createFullStageRuntime() {
|
||||
return {
|
||||
async initialize() {
|
||||
initializeAnalytics()
|
||||
await authStore.initialize()
|
||||
await displayModelsStore.initialize()
|
||||
cardStore.startRuntime(syncedPinia)
|
||||
await cardStore.initialize()
|
||||
|
||||
await displayModelsStore.loadDisplayModelsFromIndexedDB()
|
||||
@@ -260,7 +279,6 @@ function createFullStageRuntime() {
|
||||
inferencePreload.triggerPreload()
|
||||
},
|
||||
dispose() {
|
||||
cardStore.disposeRuntime()
|
||||
contextBridgeStore.dispose()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ const synced = setupSynced({
|
||||
leadership: resolveRendererWindowContext().leadership,
|
||||
})
|
||||
pinia.use(synced.pinia)
|
||||
pinia.use(piniaPluginTracing)
|
||||
if (import.meta.env.DEV)
|
||||
pinia.use(piniaPluginTracing)
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
|
||||
@@ -133,7 +133,7 @@ async function executeCreateImageJournalEntry(params: { prompt?: string, title?:
|
||||
if (!extension.airi.modules)
|
||||
extension.airi.modules = {}
|
||||
extension.airi.modules.activeBackgroundId = entryId
|
||||
cardStore.updateCard(cardId, { ...card, extensions: extension })
|
||||
await cardStore.updateCard(cardId, { ...card, extensions: extension })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ async function executeSetAsBackground(params: { query?: string }) {
|
||||
if (!extension.airi.modules)
|
||||
extension.airi.modules = {}
|
||||
extension.airi.modules.activeBackgroundId = entry.id
|
||||
cardStore.updateCard(cardId, { ...card, extensions: extension })
|
||||
await cardStore.updateCard(cardId, { ...card, extensions: extension })
|
||||
}
|
||||
}
|
||||
return `Background set to "${entry.title}".`
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { OnboardingDialog, OnboardingStepAnalyticsNotice, 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 { initializeAnalytics, isAnalyticsAvailableInBuild } from '@proj-airi/stage-ui/libs/analytics'
|
||||
import { usePiniaSynced } from '@proj-airi/stage-ui/libs/pinia'
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character'
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding'
|
||||
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { useSettingsStageModel } from '@proj-airi/stage-ui/stores/settings/stage-model'
|
||||
import { ErrorBoundary, useTheme } from '@proj-airi/ui'
|
||||
import { StageTransitionGroup } from '@proj-airi/ui-transitions'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@@ -25,9 +30,9 @@ import PerformanceOverlay from './components/Devtools/PerformanceOverlay.vue'
|
||||
import { usePWAStore } from './stores/pwa'
|
||||
|
||||
usePWAStore()
|
||||
useAuthProviderSync()
|
||||
|
||||
const contextBridgeStore = useContextBridgeStore()
|
||||
const authStore = useAuthStore()
|
||||
const i18n = useI18n()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const settingsStore = useSettings()
|
||||
@@ -43,6 +48,11 @@ const settingsAudioDeviceStore = useSettingsAudioDevice()
|
||||
const { showingSetup } = storeToRefs(onboardingStore)
|
||||
const { isDark } = useTheme()
|
||||
const cardStore = useAiriCardStore()
|
||||
useArtistryStore()
|
||||
useConsciousnessStore()
|
||||
useSpeechStore()
|
||||
useSettingsStageModel()
|
||||
useVisionStore()
|
||||
const inferencePreload = useInferencePreload()
|
||||
|
||||
const primaryColor = computed(() => {
|
||||
@@ -88,8 +98,8 @@ watch(settings.themeColorsHueDynamic, () => {
|
||||
// Initialize first-time setup check when app mounts
|
||||
onMounted(async () => {
|
||||
initializeAnalytics()
|
||||
await authStore.initialize()
|
||||
await displayModelsStore.initialize()
|
||||
cardStore.startRuntime(syncedPinia)
|
||||
await cardStore.initialize()
|
||||
|
||||
if (onboardingStore.needsOnboarding) {
|
||||
@@ -111,7 +121,6 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
stopLeadershipListener()
|
||||
cardStore.disposeRuntime()
|
||||
contextBridgeStore.dispose()
|
||||
})
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ configureAnalyticsAdapter(async (options) => {
|
||||
const pinia = createPinia()
|
||||
const synced = setupSynced()
|
||||
pinia.use(synced.pinia)
|
||||
pinia.use(piniaPluginTracing)
|
||||
if (import.meta.env.DEV)
|
||||
pinia.use(piniaPluginTracing)
|
||||
|
||||
// TODO: vite-plugin-vue-layouts is long deprecated, replace with another layout solution
|
||||
const routeRecords = setupLayouts(routes as RouteRecordRaw[])
|
||||
|
||||
@@ -302,7 +302,7 @@ watch(() => props.modelValue, (isOpen) => {
|
||||
const showError = ref<boolean>(false)
|
||||
const errorMessage = ref<string>('')
|
||||
|
||||
function saveCard(card: Card, activate: boolean): boolean {
|
||||
async function saveCard(card: Card, activate: boolean): Promise<boolean> {
|
||||
const draftResult = safeParseAiriCardDraft(toRaw(card), selectedArtistryConfigStr.value)
|
||||
if (!draftResult.success) {
|
||||
showError.value = true
|
||||
@@ -342,7 +342,7 @@ function saveCard(card: Card, activate: boolean): boolean {
|
||||
let savedCardId: string
|
||||
if (isEditMode.value && props.cardId) {
|
||||
// Edit mode: update existing card
|
||||
if (!cardStore.updateCard(props.cardId, cardWithModules)) {
|
||||
if (!await cardStore.updateCard(props.cardId, cardWithModules)) {
|
||||
showError.value = true
|
||||
errorMessage.value = t('settings.pages.card.card_not_found')
|
||||
return false
|
||||
@@ -351,11 +351,11 @@ function saveCard(card: Card, activate: boolean): boolean {
|
||||
trackCardEdited({ card_id: props.cardId })
|
||||
}
|
||||
else {
|
||||
savedCardId = cardStore.addCard(cardWithModules, 'scratch')
|
||||
savedCardId = await cardStore.addCard(cardWithModules, 'scratch')
|
||||
}
|
||||
|
||||
if (activate)
|
||||
cardStore.activeCardId = savedCardId
|
||||
await cardStore.activateCard(savedCardId)
|
||||
|
||||
modelValue.value = false // Close this
|
||||
return true
|
||||
|
||||
@@ -115,8 +115,8 @@ const isActivating = ref(false)
|
||||
|
||||
function handleActivate() {
|
||||
isActivating.value = true
|
||||
setTimeout(() => {
|
||||
activeCardId.value = props.cardId
|
||||
setTimeout(async () => {
|
||||
await cardStore.activateCard(props.cardId)
|
||||
isActivating.value = false
|
||||
}, 300)
|
||||
}
|
||||
@@ -149,9 +149,9 @@ function highlightTagToHtml(text: string) {
|
||||
// Delete confirmation
|
||||
const showDeleteConfirm = ref(false)
|
||||
|
||||
function handleDeleteConfirm() {
|
||||
async function handleDeleteConfirm() {
|
||||
if (selectedCard.value) {
|
||||
removeCard(props.cardId)
|
||||
await removeCard(props.cardId)
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
showDeleteConfirm.value = false
|
||||
@@ -180,7 +180,7 @@ const activeBackgroundId = computed({
|
||||
|
||||
extension.airi.modules.activeBackgroundId = val
|
||||
|
||||
cardStore.updateCard(props.cardId, {
|
||||
await cardStore.updateCard(props.cardId, {
|
||||
...selectedCard.value,
|
||||
extensions: extension,
|
||||
})
|
||||
|
||||
@@ -59,7 +59,7 @@ watch(inputFiles, async (newFiles) => {
|
||||
return
|
||||
|
||||
try {
|
||||
addCard(await importAiriCardPackage({ file, displayModelsStore }), 'import')
|
||||
await addCard(await importAiriCardPackage({ file, displayModelsStore }), 'import')
|
||||
toast(t('settings.pages.card.imported'))
|
||||
}
|
||||
catch (error) {
|
||||
@@ -113,9 +113,9 @@ const sortedFilteredCards = computed<CardItem[]>(() => {
|
||||
const showDeleteConfirm = ref(false)
|
||||
const cardToDelete = ref<string | null>(null)
|
||||
|
||||
function handleDeleteConfirm() {
|
||||
async function handleDeleteConfirm() {
|
||||
if (cardToDelete.value) {
|
||||
removeCard(cardToDelete.value)
|
||||
await removeCard(cardToDelete.value)
|
||||
cardToDelete.value = null
|
||||
showDeleteConfirm.value = false
|
||||
}
|
||||
@@ -154,7 +154,7 @@ function handleCardCreationDialog() {
|
||||
|
||||
// Card activation
|
||||
function activateCard(id: string) {
|
||||
activeCardId.value = id
|
||||
void cardStore.activateCard(id)
|
||||
}
|
||||
|
||||
watch(activeCardId, (cardId, previousCardId) => {
|
||||
|
||||
@@ -60,7 +60,7 @@ watch(activeProvider, async (provider, oldProvider) => {
|
||||
}, { immediate: true })
|
||||
|
||||
watch([activeProvider, activeModel], ([provider, model]) => {
|
||||
airiCardStore.updateActiveCardConsciousness({ provider, model })
|
||||
void airiCardStore.updateActiveCardConsciousness({ provider, model })
|
||||
})
|
||||
|
||||
function updateCustomModelName(value: string) {
|
||||
|
||||
@@ -405,7 +405,7 @@ watch(activeSpeechModel, async (model) => {
|
||||
})
|
||||
|
||||
watch([activeSpeechProvider, activeSpeechModel, activeSpeechVoiceId], ([provider, model, voiceId]) => {
|
||||
airiCardStore.updateActiveCardSpeech({ provider, model, voice_id: voiceId })
|
||||
void airiCardStore.updateActiveCardSpeech({ provider, model, voice_id: voiceId })
|
||||
})
|
||||
|
||||
// Function to generate speech
|
||||
|
||||
@@ -53,7 +53,7 @@ watch(activeProvider, async (provider, oldProvider) => {
|
||||
}, { immediate: true })
|
||||
|
||||
watch([activeProvider, activeModel], ([provider, model]) => {
|
||||
airiCardStore.updateActiveCardVision({ provider, model })
|
||||
void airiCardStore.updateActiveCardVision({ provider, model })
|
||||
})
|
||||
|
||||
function updateCustomModelName(value: string) {
|
||||
|
||||
@@ -29,7 +29,7 @@ const activeBackgroundId = computed({
|
||||
|
||||
extension.airi.modules.activeBackgroundId = val
|
||||
|
||||
cardStore.updateCard(cardStore.activeCardId, {
|
||||
void cardStore.updateCard(cardStore.activeCardId, {
|
||||
...cardStore.activeCard,
|
||||
extensions: extension,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { nextTick, watch } from 'vue'
|
||||
|
||||
import { useLocalStorageManualReset } from '.'
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('useLocalStorageManualReset', () => {
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// A synchronized store replaced a Map with a structured clone. Persistence
|
||||
// serialized that value, then the storage ref reflected another Map clone
|
||||
// into the store. Pinia saw the reflection as a new direct mutation and
|
||||
// published another domain snapshot.
|
||||
//
|
||||
// We fixed this by making `listenToStorageChanges: false` one-way: state is
|
||||
// still persisted, but the storage ref cannot write back into live state.
|
||||
it('does not reflect persisted values when storage listening is disabled', async () => {
|
||||
const state = useLocalStorageManualReset('cards', new Map<string, string>(), {
|
||||
listenToStorageChanges: false,
|
||||
})
|
||||
let changes = 0
|
||||
const stop = watch(state, () => {
|
||||
changes += 1
|
||||
}, { flush: 'sync' })
|
||||
|
||||
state.value = new Map([['card-1', 'ReLU']])
|
||||
await nextTick()
|
||||
|
||||
expect(changes).toBe(1)
|
||||
expect(state.value).toEqual(new Map([['card-1', 'ReLU']]))
|
||||
stop()
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import type { ManualResetRefReturn, UseStorageOptions } from '@vueuse/core'
|
||||
import type { MaybeRefOrGetter, WatchOptions } from 'vue'
|
||||
|
||||
import { refManualReset, useLocalStorage } from '@vueuse/core'
|
||||
import { unref, watch } from 'vue'
|
||||
import { toRaw, unref, watch } from 'vue'
|
||||
|
||||
export function useLocalStorageManualReset<T>(
|
||||
key: MaybeRefOrGetter<string>,
|
||||
@@ -14,11 +14,20 @@ export function useLocalStorageManualReset<T>(
|
||||
const state = refManualReset<T>(localStorageState)
|
||||
|
||||
const { resume, pause } = watch(state, newValue => localStorageState.value = newValue, options)
|
||||
watch(localStorageState, (newValue) => {
|
||||
pause()
|
||||
state.value = newValue
|
||||
resume()
|
||||
}, options)
|
||||
if (options?.listenToStorageChanges !== false) {
|
||||
watch(localStorageState, (newValue) => {
|
||||
// Writing state to useStorage updates this ref with the same value. A
|
||||
// manual ref triggers even when assigned the same reference, so reflecting
|
||||
// that write would publish a second Pinia mutation. Only storage-originated
|
||||
// values need to cross this boundary.
|
||||
if (toRaw(newValue) === toRaw(state.value))
|
||||
return
|
||||
|
||||
pause()
|
||||
state.value = newValue
|
||||
resume()
|
||||
}, options)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ onClickOutside(containerRef, () => {
|
||||
cancelCreate()
|
||||
})
|
||||
|
||||
function handleSelection(value: ProfileSelectValue) {
|
||||
async function handleSelection(value: ProfileSelectValue) {
|
||||
if (value === CREATE_PROFILE_ACTION) {
|
||||
selectedProfile.value = activeCardId.value
|
||||
void showCreateInput()
|
||||
@@ -105,7 +105,7 @@ function handleSelection(value: ProfileSelectValue) {
|
||||
return
|
||||
}
|
||||
|
||||
activeCardId.value = value
|
||||
await cardStore.activateCard(value)
|
||||
}
|
||||
|
||||
async function showCreateInput() {
|
||||
@@ -117,19 +117,19 @@ async function showCreateInput() {
|
||||
nameInputRef.value?.select()
|
||||
}
|
||||
|
||||
function confirmCreate() {
|
||||
async function confirmCreate() {
|
||||
const current = activeCard.value
|
||||
const name = newProfileName.value.trim()
|
||||
if (!current || !name || isDuplicateName.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const newId = cardStore.addCard({
|
||||
const newId = await cardStore.addCard({
|
||||
...structuredClone(toRaw(current)),
|
||||
name,
|
||||
}, 'duplicate')
|
||||
|
||||
activeCardId.value = newId
|
||||
await cardStore.activateCard(newId)
|
||||
cancelCreate()
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ const effectiveRenderer = computed(() => resolveModelSettingsPanelRenderer({
|
||||
|
||||
async function handleModelPick(selectedModel: DisplayModel | undefined) {
|
||||
stageModelSelected.value = selectedModel?.id ?? ''
|
||||
airiCardStore.updateActiveCardDisplayModel(selectedModel?.id)
|
||||
await airiCardStore.updateActiveCardDisplayModel(selectedModel?.id)
|
||||
await settingsStore.updateStageModel()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { OFFICIAL_TRANSCRIPTION_PROVIDER_ID } from '../libs/providers'
|
||||
import { useAuthProviderSync } from './use-auth-provider-sync'
|
||||
|
||||
const syncState = vi.hoisted(() => ({
|
||||
isLeader: false,
|
||||
authenticatedHook: undefined as (() => Promise<void>) | undefined,
|
||||
logoutHook: undefined as (() => void) | undefined,
|
||||
activeProvider: '',
|
||||
activeModel: '',
|
||||
activeVisionProvider: '',
|
||||
activeVisionModel: '',
|
||||
activeSpeechProvider: 'speech-noop',
|
||||
activeSpeechModel: '',
|
||||
activeSpeechVoiceId: '',
|
||||
activeTranscriptionProvider: '',
|
||||
activeTranscriptionModel: '',
|
||||
}))
|
||||
|
||||
const syncMocks = vi.hoisted(() => ({
|
||||
initializeAuth: vi.fn(async () => {}),
|
||||
leadershipHook: undefined as ((isLeader: boolean) => void) | undefined,
|
||||
disposeAuthenticatedHook: vi.fn(),
|
||||
disposeLogoutHook: vi.fn(),
|
||||
forceProviderConfigured: vi.fn(),
|
||||
setProviderUnconfigured: vi.fn(),
|
||||
setProviderAvailabilityOverride: vi.fn(),
|
||||
fetchModelsForProvider: vi.fn(async (_providerId: string): Promise<unknown[]> => []),
|
||||
loadConsciousnessModels: vi.fn(async () => []),
|
||||
loadVisionModels: vi.fn(async () => []),
|
||||
trackOfficialProviderSelected: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../libs/auth', () => ({
|
||||
initializeAuth: syncMocks.initializeAuth,
|
||||
}))
|
||||
|
||||
vi.mock('../libs/pinia', () => ({
|
||||
usePiniaSynced: () => ({
|
||||
isLeader: () => syncState.isLeader,
|
||||
onLeadershipChange: (hook: (isLeader: boolean) => void) => {
|
||||
syncMocks.leadershipHook = hook
|
||||
return vi.fn()
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../libs/providers', () => ({
|
||||
getStreamingTtsAvailable: () => true,
|
||||
OFFICIAL_TRANSCRIPTION_PROVIDER_ID: 'official-provider-transcription',
|
||||
}))
|
||||
|
||||
vi.mock('../stores/auth', () => ({
|
||||
useAuthStore: () => ({
|
||||
onAuthenticated: (hook: () => Promise<void>) => {
|
||||
syncState.authenticatedHook = hook
|
||||
return () => {
|
||||
syncState.authenticatedHook = undefined
|
||||
syncMocks.disposeAuthenticatedHook()
|
||||
}
|
||||
},
|
||||
onLogout: (hook: () => void) => {
|
||||
syncState.logoutHook = hook
|
||||
return () => {
|
||||
syncState.logoutHook = undefined
|
||||
syncMocks.disposeLogoutHook()
|
||||
}
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../stores/providers/provider', () => ({
|
||||
useProviderStore: () => ({
|
||||
findProviderDefinition: () => ({}),
|
||||
forceProviderConfigured: syncMocks.forceProviderConfigured,
|
||||
setProviderUnconfigured: syncMocks.setProviderUnconfigured,
|
||||
setProviderAvailabilityOverride: syncMocks.setProviderAvailabilityOverride,
|
||||
fetchModelsForProvider: syncMocks.fetchModelsForProvider,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../stores/modules/consciousness', () => ({
|
||||
useConsciousnessStore: () => ({
|
||||
get activeProvider() { return syncState.activeProvider },
|
||||
set activeProvider(value: string) { syncState.activeProvider = value },
|
||||
get activeModel() { return syncState.activeModel },
|
||||
set activeModel(value: string) { syncState.activeModel = value },
|
||||
loadModelsForProvider: syncMocks.loadConsciousnessModels,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../stores/modules/vision', () => ({
|
||||
useVisionStore: () => ({
|
||||
get activeProvider() { return syncState.activeVisionProvider },
|
||||
set activeProvider(value: string) { syncState.activeVisionProvider = value },
|
||||
get activeModel() { return syncState.activeVisionModel },
|
||||
set activeModel(value: string) { syncState.activeVisionModel = value },
|
||||
loadModelsForProvider: syncMocks.loadVisionModels,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../stores/modules/speech', () => ({
|
||||
useSpeechStore: () => ({
|
||||
get activeSpeechProvider() { return syncState.activeSpeechProvider },
|
||||
set activeSpeechProvider(value: string) { syncState.activeSpeechProvider = value },
|
||||
get activeSpeechModel() { return syncState.activeSpeechModel },
|
||||
set activeSpeechModel(value: string) { syncState.activeSpeechModel = value },
|
||||
get activeSpeechVoiceId() { return syncState.activeSpeechVoiceId },
|
||||
set activeSpeechVoiceId(value: string) { syncState.activeSpeechVoiceId = value },
|
||||
ensureStreamingDefaultModel: vi.fn(),
|
||||
loadVoicesForProvider: vi.fn(async () => []),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../stores/modules/hearing', () => ({
|
||||
useHearingStore: () => ({
|
||||
get activeTranscriptionProvider() { return syncState.activeTranscriptionProvider },
|
||||
set activeTranscriptionProvider(value: string) { syncState.activeTranscriptionProvider = value },
|
||||
get activeTranscriptionModel() { return syncState.activeTranscriptionModel },
|
||||
set activeTranscriptionModel(value: string) { syncState.activeTranscriptionModel = value },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('./use-analytics', () => ({
|
||||
useAnalytics: () => ({
|
||||
trackOfficialProviderSelected: syncMocks.trackOfficialProviderSelected,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useAuthProviderSync', () => {
|
||||
beforeEach(() => {
|
||||
syncState.isLeader = false
|
||||
syncState.authenticatedHook = undefined
|
||||
syncState.logoutHook = undefined
|
||||
syncMocks.leadershipHook = undefined
|
||||
syncState.activeProvider = ''
|
||||
syncState.activeModel = ''
|
||||
syncState.activeVisionProvider = ''
|
||||
syncState.activeVisionModel = ''
|
||||
syncState.activeSpeechProvider = 'speech-noop'
|
||||
syncState.activeSpeechModel = ''
|
||||
syncState.activeSpeechVoiceId = ''
|
||||
syncState.activeTranscriptionProvider = ''
|
||||
syncState.activeTranscriptionModel = ''
|
||||
vi.clearAllMocks()
|
||||
syncMocks.fetchModelsForProvider.mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('starts auth initialization when this renderer becomes the leader', async () => {
|
||||
useAuthProviderSync()
|
||||
expect(syncMocks.initializeAuth).not.toHaveBeenCalled()
|
||||
|
||||
syncState.isLeader = true
|
||||
syncMocks.leadershipHook?.(true)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(syncMocks.initializeAuth).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not activate providers in a follower renderer', () => {
|
||||
useAuthProviderSync()
|
||||
|
||||
expect(syncState.authenticatedHook).toBeUndefined()
|
||||
expect(syncMocks.initializeAuth).not.toHaveBeenCalled()
|
||||
expect(syncMocks.forceProviderConfigured).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// A renderer kept its auth hooks after it lost leadership. Those hooks
|
||||
// could mutate provider state while the new leader handled the same auth
|
||||
// transition.
|
||||
//
|
||||
// https://github.com/moeru-ai/airi/pull/2304
|
||||
it('removes auth hooks on demotion and restores them after reacquiring leadership', () => {
|
||||
syncState.isLeader = true
|
||||
useAuthProviderSync()
|
||||
|
||||
expect(syncState.authenticatedHook).toBeDefined()
|
||||
expect(syncState.logoutHook).toBeDefined()
|
||||
|
||||
syncState.isLeader = false
|
||||
syncMocks.leadershipHook?.(false)
|
||||
|
||||
expect(syncMocks.disposeAuthenticatedHook).toHaveBeenCalledTimes(1)
|
||||
expect(syncMocks.disposeLogoutHook).toHaveBeenCalledTimes(1)
|
||||
expect(syncState.authenticatedHook).toBeUndefined()
|
||||
expect(syncState.logoutHook).toBeUndefined()
|
||||
|
||||
syncState.isLeader = true
|
||||
syncMocks.leadershipHook?.(true)
|
||||
|
||||
expect(syncMocks.initializeAuth).toHaveBeenCalledTimes(2)
|
||||
expect(syncState.authenticatedHook).toBeDefined()
|
||||
expect(syncState.logoutHook).toBeDefined()
|
||||
})
|
||||
|
||||
it('activates every official provider after direct sign-in when no custom provider is selected', async () => {
|
||||
syncState.isLeader = true
|
||||
useAuthProviderSync()
|
||||
|
||||
await syncState.authenticatedHook?.()
|
||||
|
||||
expect(syncMocks.forceProviderConfigured).toHaveBeenCalledWith('official-provider')
|
||||
expect(syncMocks.forceProviderConfigured).toHaveBeenCalledWith('vision-official-provider')
|
||||
expect(syncMocks.forceProviderConfigured).toHaveBeenCalledWith('official-provider-speech')
|
||||
expect(syncMocks.forceProviderConfigured).toHaveBeenCalledWith(OFFICIAL_TRANSCRIPTION_PROVIDER_ID)
|
||||
expect(syncState.activeProvider).toBe('official-provider')
|
||||
expect(syncState.activeModel).toBe('auto')
|
||||
expect(syncState.activeSpeechProvider).toBe('official-provider-speech')
|
||||
expect(syncState.activeTranscriptionProvider).toBe(OFFICIAL_TRANSCRIPTION_PROVIDER_ID)
|
||||
})
|
||||
|
||||
it('retries provider activation when the first authenticated sync fails', async () => {
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The auth hook marked the session synchronized before model and streaming
|
||||
// provider bootstrap completed. A transient failure therefore made every
|
||||
// later authentication notification return early for the whole session.
|
||||
syncState.isLeader = true
|
||||
syncMocks.fetchModelsForProvider.mockImplementation(async (providerId: string) => {
|
||||
if (providerId === 'official-provider-speech-streaming')
|
||||
throw new Error('temporary catalog failure')
|
||||
return []
|
||||
})
|
||||
useAuthProviderSync()
|
||||
|
||||
await expect(syncState.authenticatedHook?.()).rejects.toThrow('temporary catalog failure')
|
||||
syncMocks.fetchModelsForProvider.mockResolvedValue([])
|
||||
await expect(syncState.authenticatedHook?.()).resolves.toBeUndefined()
|
||||
|
||||
expect(syncMocks.forceProviderConfigured).toHaveBeenCalledTimes(9)
|
||||
expect(syncMocks.forceProviderConfigured.mock.calls.filter(([providerId]) => providerId === 'official-provider')).toHaveLength(2)
|
||||
expect(syncMocks.forceProviderConfigured.mock.calls.filter(([providerId]) => providerId === 'official-provider-speech')).toHaveLength(2)
|
||||
expect(syncMocks.forceProviderConfigured.mock.calls.filter(([providerId]) => providerId === OFFICIAL_TRANSCRIPTION_PROVIDER_ID)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
@@ -1,282 +0,0 @@
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { initializeAuth } from '../libs/auth'
|
||||
import { usePiniaSynced } from '../libs/pinia'
|
||||
import { getStreamingTtsAvailable, OFFICIAL_TRANSCRIPTION_PROVIDER_ID } from '../libs/providers'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useConsciousnessStore } from '../stores/modules/consciousness'
|
||||
import { useHearingStore } from '../stores/modules/hearing'
|
||||
import { useSpeechStore } from '../stores/modules/speech'
|
||||
import { useVisionStore } from '../stores/modules/vision'
|
||||
import { useProviderStore } from '../stores/providers/provider'
|
||||
import { useAnalytics } from './use-analytics'
|
||||
|
||||
/**
|
||||
* Provider IDs to auto-activate on sign-in.
|
||||
* Edit this list to enable/disable official providers.
|
||||
*/
|
||||
const AUTH_ACTIVATED_PROVIDERS: Array<{ id: string, module: 'consciousness' | 'speech' | 'hearing' | 'vision' }> = [
|
||||
{ id: 'official-provider', module: 'consciousness' },
|
||||
{ id: 'vision-official-provider', module: 'vision' },
|
||||
{ id: 'official-provider-speech', module: 'speech' },
|
||||
{ id: OFFICIAL_TRANSCRIPTION_PROVIDER_ID, module: 'hearing' },
|
||||
]
|
||||
|
||||
// The streaming TTS provider is NOT in the static list above because its
|
||||
// visibility is operator-controlled: `UNSPEECH_UPSTREAM.streaming` may be
|
||||
// unconfigured server-side. It's bootstrapped separately (see
|
||||
// `syncStreamingSpeechProvider`) — probed on sign-in, then force-configured
|
||||
// only when the server reports it available, mirroring how the HTTP TTS
|
||||
// provider uses `forceProviderConfigured` but gating it on a server signal.
|
||||
const STREAMING_SPEECH_PROVIDER_ID = 'official-provider-speech-streaming'
|
||||
|
||||
/**
|
||||
* Glue layer: uses auth lifecycle hooks to activate/deactivate
|
||||
* official providers. Providers themselves know nothing about auth.
|
||||
*
|
||||
* Call once from each app renderer root so direct sign-in routes and
|
||||
* auxiliary windows do not depend on the transient Stage scene lifecycle.
|
||||
*/
|
||||
export function useAuthProviderSync() {
|
||||
const syncedPinia = usePiniaSynced()
|
||||
let leaderSyncInitialized = false
|
||||
let disposeAuthenticatedProviderSync: (() => void) | undefined
|
||||
|
||||
function initializeLeaderSync() {
|
||||
if (!syncedPinia.isLeader() || leaderSyncInitialized)
|
||||
return
|
||||
|
||||
leaderSyncInitialized = true
|
||||
void initializeAuth()
|
||||
disposeAuthenticatedProviderSync = setupAuthenticatedProviderSync()
|
||||
}
|
||||
|
||||
function disposeLeaderSync() {
|
||||
if (!leaderSyncInitialized)
|
||||
return
|
||||
|
||||
disposeAuthenticatedProviderSync?.()
|
||||
disposeAuthenticatedProviderSync = undefined
|
||||
leaderSyncInitialized = false
|
||||
}
|
||||
|
||||
syncedPinia.onLeadershipChange((isLeader) => {
|
||||
if (isLeader)
|
||||
initializeLeaderSync()
|
||||
else
|
||||
disposeLeaderSync()
|
||||
})
|
||||
|
||||
initializeLeaderSync()
|
||||
}
|
||||
|
||||
function setupAuthenticatedProviderSync() {
|
||||
const authStore = useAuthStore()
|
||||
const providersStore = useProviderStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const visionStore = useVisionStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const hearingStore = useHearingStore()
|
||||
const { trackOfficialProviderSelected } = useAnalytics()
|
||||
|
||||
// Track the completed and in-flight work separately. Authentication can be
|
||||
// announced more than once while a catalog request is still pending; those
|
||||
// notifications share one task, while a failed task remains retryable.
|
||||
let hasSynced = false
|
||||
let authGeneration = 0
|
||||
let syncInFlight: Promise<void> | undefined
|
||||
|
||||
const stopAuthenticatedHook = authStore.onAuthenticated(async () => {
|
||||
if (hasSynced)
|
||||
return
|
||||
|
||||
if (syncInFlight)
|
||||
return syncInFlight
|
||||
|
||||
const generation = authGeneration
|
||||
const task = syncAuthenticatedProviders(generation)
|
||||
syncInFlight = task
|
||||
|
||||
try {
|
||||
await task
|
||||
if (generation === authGeneration)
|
||||
hasSynced = true
|
||||
}
|
||||
finally {
|
||||
if (syncInFlight === task)
|
||||
syncInFlight = undefined
|
||||
}
|
||||
})
|
||||
|
||||
async function syncAuthenticatedProviders(generation: number) {
|
||||
if (generation !== authGeneration)
|
||||
return
|
||||
|
||||
const toActivate = AUTH_ACTIVATED_PROVIDERS.filter(
|
||||
p => providersStore.findProviderDefinition(p.id) != null,
|
||||
)
|
||||
|
||||
for (const { id } of toActivate) {
|
||||
providersStore.forceProviderConfigured(id)
|
||||
}
|
||||
|
||||
// Only set official provider as active when the user hasn't configured
|
||||
// any provider for that module yet.
|
||||
for (const { id, module } of toActivate) {
|
||||
switch (module) {
|
||||
case 'consciousness':
|
||||
if (!consciousnessStore.activeProvider) {
|
||||
consciousnessStore.activeProvider = id
|
||||
consciousnessStore.activeModel = 'auto'
|
||||
trackOfficialProviderSelected({
|
||||
provider_id: id,
|
||||
provider_mode: 'official',
|
||||
source: 'default_auto',
|
||||
auto_selected: true,
|
||||
model_id: 'auto',
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'vision':
|
||||
if (!visionStore.activeProvider) {
|
||||
visionStore.activeProvider = id
|
||||
visionStore.activeModel = 'auto'
|
||||
}
|
||||
break
|
||||
case 'speech':
|
||||
if (!speechStore.activeSpeechProvider || speechStore.activeSpeechProvider === 'speech-noop') {
|
||||
speechStore.activeSpeechProvider = id
|
||||
speechStore.activeSpeechModel = ''
|
||||
}
|
||||
break
|
||||
case 'hearing':
|
||||
if (!hearingStore.activeTranscriptionProvider) {
|
||||
hearingStore.activeTranscriptionProvider = id
|
||||
hearingStore.activeTranscriptionModel = 'auto'
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
try {
|
||||
await Promise.all(
|
||||
toActivate.map(({ id, module }) =>
|
||||
module === 'consciousness'
|
||||
? consciousnessStore.loadModelsForProvider(id)
|
||||
: module === 'vision'
|
||||
? visionStore.loadModelsForProvider(id)
|
||||
: providersStore.fetchModelsForProvider(id),
|
||||
),
|
||||
)
|
||||
}
|
||||
catch (err) {
|
||||
console.error('error loading models for official providers', err)
|
||||
}
|
||||
|
||||
// Logout may happen while provider catalogs are loading. The logout hook
|
||||
// owns cleanup, so stale work must not re-enable streaming TTS afterward.
|
||||
if (generation !== authGeneration)
|
||||
return
|
||||
|
||||
await syncStreamingSpeechProvider()
|
||||
}
|
||||
|
||||
// Bootstrap the streaming TTS provider from the server's availability signal.
|
||||
// Probing populates `getStreamingTtsAvailable()` (and the default model /
|
||||
// voices) via the provider's listModels(). The availability override drives
|
||||
// the provider's presence in the available/configured lists (and thus the
|
||||
// settings card + picker); force-configure makes it selectable. It is never
|
||||
// set as the active speech provider — the HTTP TTS provider stays default.
|
||||
async function syncStreamingSpeechProvider() {
|
||||
if (providersStore.findProviderDefinition(STREAMING_SPEECH_PROVIDER_ID) == null)
|
||||
return
|
||||
|
||||
await providersStore.fetchModelsForProvider(STREAMING_SPEECH_PROVIDER_ID)
|
||||
|
||||
const available = getStreamingTtsAvailable()
|
||||
providersStore.setProviderAvailabilityOverride(STREAMING_SPEECH_PROVIDER_ID, available)
|
||||
|
||||
if (available) {
|
||||
providersStore.forceProviderConfigured(STREAMING_SPEECH_PROVIDER_ID)
|
||||
// The speech-module watcher skips voice loading for streaming until it's
|
||||
// confirmed configured (avoids a pre-probe request on reload), so when
|
||||
// streaming is the persisted active provider, load its voices now that
|
||||
// it's confirmed available.
|
||||
if (speechStore.activeSpeechProvider === STREAMING_SPEECH_PROVIDER_ID) {
|
||||
speechStore.ensureStreamingDefaultModel()
|
||||
await speechStore.loadVoicesForProvider(STREAMING_SPEECH_PROVIDER_ID, speechStore.activeSpeechModel || undefined)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
providersStore.setProviderUnconfigured(STREAMING_SPEECH_PROVIDER_ID)
|
||||
// `setProviderUnconfigured` blanks `validatedCredentialHash`, which makes
|
||||
// the speech-module reset watcher skip its own clear. So when the server
|
||||
// now reports streaming unavailable on an authenticated reload (no logout
|
||||
// event fires), clear a stale active streaming selection here.
|
||||
clearActiveStreamingSelection()
|
||||
}
|
||||
|
||||
function clearActiveStreamingSelection() {
|
||||
if (speechStore.activeSpeechProvider !== STREAMING_SPEECH_PROVIDER_ID)
|
||||
return
|
||||
speechStore.activeSpeechProvider = ''
|
||||
speechStore.activeSpeechModel = ''
|
||||
speechStore.activeSpeechVoiceId = ''
|
||||
}
|
||||
|
||||
const stopLogoutHook = authStore.onLogout(() => {
|
||||
authGeneration++
|
||||
hasSynced = false
|
||||
|
||||
for (const { id } of AUTH_ACTIVATED_PROVIDERS) {
|
||||
providersStore.setProviderUnconfigured(id)
|
||||
}
|
||||
|
||||
// Streaming TTS is bootstrapped outside AUTH_ACTIVATED_PROVIDERS, so reset
|
||||
// it explicitly. `setProviderUnconfigured` blanks `validatedCredentialHash`,
|
||||
// which makes the speech-module watcher skip its own reset (it guards
|
||||
// against racing initial validation), so clear the active selection here
|
||||
// too when streaming was the active provider.
|
||||
clearActiveStreamingSelection()
|
||||
providersStore.setProviderUnconfigured(STREAMING_SPEECH_PROVIDER_ID)
|
||||
providersStore.setProviderAvailabilityOverride(STREAMING_SPEECH_PROVIDER_ID, false)
|
||||
|
||||
// Reset active provider/model if they belong to an auth-activated provider
|
||||
for (const { id, module } of AUTH_ACTIVATED_PROVIDERS) {
|
||||
switch (module) {
|
||||
case 'consciousness':
|
||||
if (consciousnessStore.activeProvider === id) {
|
||||
consciousnessStore.activeProvider = ''
|
||||
consciousnessStore.activeModel = ''
|
||||
}
|
||||
break
|
||||
case 'vision':
|
||||
if (visionStore.activeProvider === id) {
|
||||
visionStore.activeProvider = ''
|
||||
visionStore.activeModel = ''
|
||||
}
|
||||
break
|
||||
case 'speech':
|
||||
if (speechStore.activeSpeechProvider === id) {
|
||||
speechStore.activeSpeechProvider = ''
|
||||
speechStore.activeSpeechModel = ''
|
||||
}
|
||||
break
|
||||
case 'hearing':
|
||||
if (hearingStore.activeTranscriptionProvider === id) {
|
||||
hearingStore.activeTranscriptionProvider = ''
|
||||
hearingStore.activeTranscriptionModel = ''
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
authGeneration++
|
||||
stopAuthenticatedHook()
|
||||
stopLogoutHook()
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,105 @@ import { errorMessageFrom } from '@moeru/std'
|
||||
import { piniaActionTracingChannelName } from '@proj-airi/stage-shared/types/pinia-action-event'
|
||||
import { nanoid } from 'nanoid/non-secure'
|
||||
|
||||
const piniaActionChannel = new BroadcastChannel(piniaActionTracingChannelName)
|
||||
const rateTraceStorageKey = 'airi:debug:pinia-tracing'
|
||||
const rateTraceIntervalMs = 5_000
|
||||
const rateTraceLimit = 10
|
||||
|
||||
let piniaActionChannel: BroadcastChannel | undefined
|
||||
|
||||
interface RateTraceWindow {
|
||||
actionFailures: number
|
||||
actions: Map<string, number>
|
||||
mutations: Map<string, number>
|
||||
mutationTypes: Map<string, number>
|
||||
startedAt: number
|
||||
}
|
||||
|
||||
let rateTraceTimer: ReturnType<typeof setInterval> | undefined
|
||||
let rateTraceWindow: RateTraceWindow | undefined
|
||||
|
||||
function createRateTraceWindow(): RateTraceWindow {
|
||||
return {
|
||||
actionFailures: 0,
|
||||
actions: new Map(),
|
||||
mutations: new Map(),
|
||||
mutationTypes: new Map(),
|
||||
startedAt: performance.now(),
|
||||
}
|
||||
}
|
||||
|
||||
function incrementRateTraceCount(counts: Map<string, number>, key: string): void {
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1)
|
||||
}
|
||||
|
||||
function totalRateTraceCount(counts: Map<string, number>): number {
|
||||
let total = 0
|
||||
for (const count of counts.values())
|
||||
total += count
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
function topRateTraceCounts(counts: Map<string, number>): Array<{ count: number, name: string }> {
|
||||
return [...counts.entries()]
|
||||
.sort((left, right) => right[1] - left[1])
|
||||
.slice(0, rateTraceLimit)
|
||||
.map(([name, count]) => ({ count, name }))
|
||||
}
|
||||
|
||||
function reportRateTraceWindow(): void {
|
||||
const window = rateTraceWindow
|
||||
if (!window)
|
||||
return
|
||||
|
||||
const actionCount = totalRateTraceCount(window.actions)
|
||||
const mutationCount = totalRateTraceCount(window.mutations)
|
||||
if (actionCount === 0 && mutationCount === 0)
|
||||
return
|
||||
|
||||
const elapsedMs = performance.now() - window.startedAt
|
||||
const summary = {
|
||||
actionCount,
|
||||
actionFailures: window.actionFailures,
|
||||
actionsPerSecond: Number((actionCount * 1_000 / elapsedMs).toFixed(1)),
|
||||
elapsedMs: Math.round(elapsedMs),
|
||||
mutationCount,
|
||||
mutationsPerSecond: Number((mutationCount * 1_000 / elapsedMs).toFixed(1)),
|
||||
mutationTypes: topRateTraceCounts(window.mutationTypes),
|
||||
sourceUrl: location.href,
|
||||
topActions: topRateTraceCounts(window.actions),
|
||||
topMutations: topRateTraceCounts(window.mutations),
|
||||
}
|
||||
console.info(`[DEBUG-pinia-rate] ${JSON.stringify(summary)}`)
|
||||
window.actionFailures = 0
|
||||
window.actions.clear()
|
||||
window.mutations.clear()
|
||||
window.mutationTypes.clear()
|
||||
window.startedAt = performance.now()
|
||||
}
|
||||
|
||||
function startRateTracing(): RateTraceWindow | undefined {
|
||||
if (!import.meta.env.DEV || typeof window === 'undefined' || window.localStorage.getItem(rateTraceStorageKey) !== 'true')
|
||||
return
|
||||
|
||||
rateTraceWindow ??= createRateTraceWindow()
|
||||
if (!rateTraceTimer) {
|
||||
rateTraceTimer = setInterval(reportRateTraceWindow, rateTraceIntervalMs)
|
||||
window.addEventListener('pagehide', () => {
|
||||
clearInterval(rateTraceTimer)
|
||||
rateTraceTimer = undefined
|
||||
}, { once: true })
|
||||
}
|
||||
|
||||
return rateTraceWindow
|
||||
}
|
||||
|
||||
function emitActionEvent(
|
||||
event: Omit<PiniaActionEvent, 'status' | 'timestamp'>,
|
||||
status: PiniaActionEventStatus,
|
||||
error?: unknown,
|
||||
): void {
|
||||
piniaActionChannel ??= new BroadcastChannel(piniaActionTracingChannelName)
|
||||
piniaActionChannel.postMessage({
|
||||
...event,
|
||||
status,
|
||||
@@ -24,9 +116,23 @@ function emitActionEvent(
|
||||
* Traces Pinia action lifecycle events through a broadcast channel.
|
||||
*
|
||||
* The plugin never retains action arguments, results, or state snapshots.
|
||||
* In development, set `airi:debug:pinia-tracing` to `true` in local storage
|
||||
* before a reload to print one action and mutation rate summary every five seconds.
|
||||
*/
|
||||
export const piniaPluginTracing: PiniaPlugin = ({ store }) => {
|
||||
const tracedWindow = startRateTracing()
|
||||
|
||||
if (tracedWindow) {
|
||||
store.$subscribe((mutation) => {
|
||||
incrementRateTraceCount(tracedWindow.mutations, mutation.storeId)
|
||||
incrementRateTraceCount(tracedWindow.mutationTypes, mutation.type)
|
||||
}, { detached: true, flush: 'sync' })
|
||||
}
|
||||
|
||||
store.$onAction(({ name, after, onError }) => {
|
||||
if (tracedWindow)
|
||||
incrementRateTraceCount(tracedWindow.actions, `${store.$id}.${name}`)
|
||||
|
||||
const event = {
|
||||
invocationId: nanoid(),
|
||||
storeId: store.$id,
|
||||
@@ -36,6 +142,10 @@ export const piniaPluginTracing: PiniaPlugin = ({ store }) => {
|
||||
|
||||
emitActionEvent(event, 'started')
|
||||
after(() => emitActionEvent(event, 'completed'))
|
||||
onError(error => emitActionEvent(event, 'failed', error))
|
||||
onError((error) => {
|
||||
if (tracedWindow)
|
||||
tracedWindow.actionFailures += 1
|
||||
emitActionEvent(event, 'failed', error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Session, User } from 'better-auth'
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { triggerSignIn } from '../libs/auth'
|
||||
@@ -23,6 +23,34 @@ vi.mock('../libs/auth-oidc', () => ({
|
||||
refreshAccessToken: vi.fn(),
|
||||
}))
|
||||
|
||||
class MemoryStorage implements Storage {
|
||||
readonly values = new Map<string, string>()
|
||||
|
||||
get length() {
|
||||
return this.values.size
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.values.clear()
|
||||
}
|
||||
|
||||
getItem(key: string) {
|
||||
return this.values.get(key) ?? null
|
||||
}
|
||||
|
||||
key(index: number) {
|
||||
return [...this.values.keys()][index] ?? null
|
||||
}
|
||||
|
||||
removeItem(key: string) {
|
||||
this.values.delete(key)
|
||||
}
|
||||
|
||||
setItem(key: string, value: string) {
|
||||
this.values.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
const user: User = {
|
||||
id: 'user-1',
|
||||
name: 'AIRI User',
|
||||
@@ -42,7 +70,11 @@ const session: Session = {
|
||||
}
|
||||
|
||||
describe('auth store sign-in requests', () => {
|
||||
let storage: MemoryStorage
|
||||
|
||||
beforeEach(() => {
|
||||
storage = new MemoryStorage()
|
||||
vi.stubGlobal('localStorage', storage)
|
||||
setActivePinia(createPinia())
|
||||
vi.mocked(triggerSignIn).mockReset()
|
||||
vi.mocked(triggerSignIn).mockResolvedValue()
|
||||
@@ -50,6 +82,10 @@ describe('auth store sign-in requests', () => {
|
||||
vi.mocked(requestAuthSession).mockResolvedValue({ user, session })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('allows sign-in to be requested again after an external flow is canceled', async () => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
@@ -94,7 +130,43 @@ describe('auth store sign-in requests', () => {
|
||||
expect(authStore.idToken).toBe('new-id-token')
|
||||
expect(authStore.user).toEqual(user)
|
||||
expect(authStore.session).toEqual(session)
|
||||
expect(storage.getItem('auth/v1/token')).toBe('new-access-token')
|
||||
expect(storage.getItem('auth/v1/refresh-token')).toBe('new-refresh-token')
|
||||
expect(storage.getItem('auth/v1/oidc-id-token')).toBe('new-id-token')
|
||||
expect(storage.getItem('auth/v1/oidc-client-id')).toBe('airi-stage-electron')
|
||||
expect(storage.getItem('auth/v1/oidc-token-expiry')).not.toBeNull()
|
||||
|
||||
await authStore.clearAllAuthState()
|
||||
})
|
||||
|
||||
it('loads persisted credentials only when the store initializes', async () => {
|
||||
storage.values.set('auth/v1/token', 'persisted-access-token')
|
||||
storage.values.set('auth/v1/refresh-token', 'persisted-refresh-token')
|
||||
storage.values.set('auth/v1/oidc-id-token', 'persisted-id-token')
|
||||
storage.values.set('auth/v1/oidc-client-id', 'airi-stage-web')
|
||||
storage.values.set('auth/v1/oidc-token-expiry', String(Date.now() + 60_000))
|
||||
const authStore = useAuthStore()
|
||||
|
||||
expect(authStore.token).toBeNull()
|
||||
|
||||
await authStore.initialize()
|
||||
|
||||
expect(requestAuthSession).toHaveBeenCalledWith('persisted-access-token')
|
||||
expect(authStore.token).toBe('persisted-access-token')
|
||||
})
|
||||
|
||||
it('does not persist state patches received from another window', async () => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// `useLocalStorage` observed every Pinia patch and wrote it to storage.
|
||||
// The storage event then reached another window, whose synced Pinia patch
|
||||
// wrote the same value back. This formed an unbounded cross-window loop.
|
||||
// Auth persistence now runs only inside auth commands.
|
||||
authStore.$patch({ token: 'synced-access-token' })
|
||||
await nextTick()
|
||||
|
||||
expect(storage.values.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {} from 'pinia-plugin-synced'
|
||||
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
import { StorageSerializers, useLocalStorage, useTimeoutFn, whenever } from '@vueuse/core'
|
||||
import { useTimeoutFn, whenever } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
@@ -14,6 +14,45 @@ import { authClient, requestAuthSession } from '../libs/auth-client'
|
||||
import { refreshAccessToken } from '../libs/auth-oidc'
|
||||
import { SERVER_URL } from '../libs/server'
|
||||
|
||||
function createLocalStorageForAuth() {
|
||||
const keys = {
|
||||
accessToken: 'auth/v1/token',
|
||||
refreshToken: 'auth/v1/refresh-token',
|
||||
idToken: 'auth/v1/oidc-id-token',
|
||||
oidcClientId: 'auth/v1/oidc-client-id',
|
||||
tokenExpiry: 'auth/v1/oidc-token-expiry',
|
||||
} as const
|
||||
|
||||
function setOptional(key: string, value: string | null): void {
|
||||
if (value === null) {
|
||||
localStorage.removeItem(key)
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
|
||||
return {
|
||||
clear() {
|
||||
for (const key of Object.values(keys))
|
||||
localStorage.removeItem(key)
|
||||
},
|
||||
getAccessToken: () => localStorage.getItem(keys.accessToken),
|
||||
getRefreshToken: () => localStorage.getItem(keys.refreshToken),
|
||||
getIdToken: () => localStorage.getItem(keys.idToken),
|
||||
getOidcClientId: () => localStorage.getItem(keys.oidcClientId),
|
||||
getTokenExpiry() {
|
||||
const expiry = Number.parseInt(localStorage.getItem(keys.tokenExpiry) ?? '', 10)
|
||||
return Number.isFinite(expiry) ? expiry : null
|
||||
},
|
||||
setAccessToken: (value: string | null) => setOptional(keys.accessToken, value),
|
||||
setRefreshToken: (value: string | null) => setOptional(keys.refreshToken, value),
|
||||
setIdToken: (value: string | null) => setOptional(keys.idToken, value),
|
||||
setOidcClientId: (value: string | null) => setOptional(keys.oidcClientId, value),
|
||||
setTokenExpiry: (value: number | null) => setOptional(keys.tokenExpiry, value?.toString() ?? null),
|
||||
}
|
||||
}
|
||||
|
||||
/** Tokens that complete one OIDC sign-in flow. */
|
||||
export interface AuthTokenSet {
|
||||
accessToken: string
|
||||
@@ -30,29 +69,29 @@ export interface AuthTokenSet {
|
||||
* `providers` to safely depend on it without creating a circular import.
|
||||
*/
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = useLocalStorage<User | null>('auth/v1/user', null, {
|
||||
// Why: https://github.com/vueuse/vueuse/pull/614#issuecomment-875450160
|
||||
serializer: StorageSerializers.object,
|
||||
})
|
||||
const session = useLocalStorage<Session | null>('auth/v1/session', null, { serializer: StorageSerializers.object })
|
||||
const token = useLocalStorage<string | null>('auth/v1/token', null)
|
||||
const refreshToken = useLocalStorage<string | null>('auth/v1/refresh-token', null)
|
||||
const storage = createLocalStorageForAuth()
|
||||
|
||||
// Pinia owns live auth state. Persistence is command-driven so a state patch
|
||||
// received from another window cannot write back into the transport.
|
||||
const user = ref<User | null>(null)
|
||||
const session = ref<Session | null>(null)
|
||||
const token = ref<string | null>(null)
|
||||
const refreshToken = ref<string | null>(null)
|
||||
// NOTICE:
|
||||
// Persisted to drive `id_token_hint` on RP-Initiated Logout
|
||||
// (`/api/auth/oauth2/end-session`). The `sid` claim inside the ID token is
|
||||
// what lets the OIDC provider locate the server-side session row to delete
|
||||
// — without this we'd be back to relying on cross-site session cookies.
|
||||
const idToken = useLocalStorage<string | null>('auth/v1/oidc-id-token', null)
|
||||
const idToken = ref<string | null>(null)
|
||||
const isAuthenticated = computed(() => !!user.value && !!session.value)
|
||||
const userId = computed(() => user.value?.id ?? 'local')
|
||||
|
||||
// --- OIDC token refresh state ---
|
||||
// Persisted so refresh scheduling survives page reloads.
|
||||
const oidcClientId = useLocalStorage<string | null>('auth/v1/oidc-client-id', null)
|
||||
const tokenExpiry = useLocalStorage<number | null>('auth/v1/oidc-token-expiry', null)
|
||||
const oidcClientId = ref<string | null>(null)
|
||||
const tokenExpiry = ref<number | null>(null)
|
||||
const initialized = ref(false)
|
||||
|
||||
const credits = useLocalStorage<number>('user/v1/flux', 0)
|
||||
const credits = ref(0)
|
||||
|
||||
// Cross-app "user must log in" flag. Setting this to true triggers an
|
||||
// immediate OIDC redirect on web (mobile + desktop). Electron skips this
|
||||
@@ -104,30 +143,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch hooks when auth state changes
|
||||
watch(isAuthenticated, async (val, oldVal) => {
|
||||
if (val && !oldVal) {
|
||||
for (const hook of authenticatedHooks) {
|
||||
try {
|
||||
await hook()
|
||||
}
|
||||
catch (e) {
|
||||
console.error('auth hook error', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!val && oldVal) {
|
||||
for (const hook of logoutHooks) {
|
||||
try {
|
||||
await hook()
|
||||
}
|
||||
catch (e) {
|
||||
console.error('logout hook error', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// --- OIDC token refresh scheduling ---
|
||||
// Uses useTimeoutFn for automatic cleanup on store teardown.
|
||||
// The delay ref is updated by scheduleTokenRefresh before calling start().
|
||||
@@ -148,10 +163,14 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
try {
|
||||
const tokens = await refreshAccessToken(oidcClientId.value!, refreshToken.value!)
|
||||
token.value = tokens.access_token
|
||||
if (tokens.refresh_token)
|
||||
storage.setAccessToken(tokens.access_token)
|
||||
if (tokens.refresh_token) {
|
||||
refreshToken.value = tokens.refresh_token
|
||||
storage.setRefreshToken(tokens.refresh_token)
|
||||
}
|
||||
if (tokens.expires_in) {
|
||||
tokenExpiry.value = Date.now() + tokens.expires_in * 1000
|
||||
storage.setTokenExpiry(tokenExpiry.value)
|
||||
scheduleTokenRefresh(tokens.expires_in)
|
||||
}
|
||||
|
||||
@@ -217,6 +236,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
initialized.value = true
|
||||
|
||||
token.value = storage.getAccessToken()
|
||||
refreshToken.value = storage.getRefreshToken()
|
||||
idToken.value = storage.getIdToken()
|
||||
oidcClientId.value = storage.getOidcClientId()
|
||||
tokenExpiry.value = storage.getTokenExpiry()
|
||||
|
||||
const hasRefreshToken = !!refreshToken.value
|
||||
const hasClientId = !!oidcClientId.value
|
||||
if (hasRefreshToken !== hasClientId) {
|
||||
@@ -238,6 +263,11 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
? Date.now() + tokens.expiresIn * 1000
|
||||
: null
|
||||
scheduleTokenRefresh(tokens.expiresIn)
|
||||
storage.setAccessToken(token.value)
|
||||
storage.setRefreshToken(refreshToken.value)
|
||||
storage.setIdToken(idToken.value)
|
||||
storage.setOidcClientId(oidcClientId.value)
|
||||
storage.setTokenExpiry(tokenExpiry.value)
|
||||
|
||||
return await fetchSession(tokens.accessToken)
|
||||
}
|
||||
@@ -316,6 +346,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
oidcClientId.value = null
|
||||
tokenExpiry.value = null
|
||||
idToken.value = null
|
||||
storage.clear()
|
||||
}
|
||||
|
||||
async function clearAllAuthState(): Promise<void> {
|
||||
@@ -332,17 +363,36 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(isAuthenticated, async (val) => {
|
||||
if (val) {
|
||||
updateCredits()
|
||||
|
||||
// This is the only watcher that reacts to an auth-state transition. Each
|
||||
// window runs its own lifecycle hooks, while persistence remains owned by
|
||||
// the auth commands above.
|
||||
watch(isAuthenticated, async (authenticated, wasAuthenticated) => {
|
||||
if (authenticated) {
|
||||
void updateCredits()
|
||||
needsLogin.value = false
|
||||
|
||||
if (!wasAuthenticated)
|
||||
await dispatchHooks(authenticatedHooks, 'auth hook error')
|
||||
}
|
||||
else {
|
||||
credits.value = 0
|
||||
|
||||
if (wasAuthenticated)
|
||||
await dispatchHooks(logoutHooks, 'logout hook error')
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
async function dispatchHooks(hooks: AuthHook[], errorLabel: string): Promise<void> {
|
||||
for (const hook of hooks) {
|
||||
try {
|
||||
await hook()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(errorLabel, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
userId,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { SyncedPiniaRuntime } from 'pinia-plugin-synced'
|
||||
|
||||
import type { AiriCard } from './airi-card'
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
@@ -8,21 +6,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useSettingsStageModel } from '../settings/stage-model'
|
||||
import { useAiriCardStore } from './airi-card'
|
||||
|
||||
const syncedRuntime = {
|
||||
isLeader: false,
|
||||
leadershipListener: undefined as ((isLeader: boolean) => void) | undefined,
|
||||
stopLeadershipListener: vi.fn(() => {
|
||||
syncedRuntime.leadershipListener = undefined
|
||||
}),
|
||||
}
|
||||
|
||||
const syncedPinia = {
|
||||
onLeadershipChange(listener) {
|
||||
syncedRuntime.leadershipListener = listener
|
||||
listener(syncedRuntime.isLeader)
|
||||
return syncedRuntime.stopLeadershipListener
|
||||
},
|
||||
} satisfies Pick<SyncedPiniaRuntime, 'onLeadershipChange'>
|
||||
const { resetArtistryToGlobal } = vi.hoisted(() => ({
|
||||
resetArtistryToGlobal: vi.fn(),
|
||||
}))
|
||||
|
||||
// NOTICE:
|
||||
// Vitest runs these store tests in Node, where localforage cannot select a
|
||||
@@ -55,7 +41,7 @@ vi.mock('./artistry', async () => {
|
||||
providerOptions: {},
|
||||
}),
|
||||
actions: {
|
||||
resetToGlobal() {},
|
||||
resetToGlobal: resetArtistryToGlobal,
|
||||
},
|
||||
}),
|
||||
}
|
||||
@@ -114,24 +100,23 @@ vi.mock('vue-i18n', () => ({
|
||||
describe('airi-card store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
syncedRuntime.isLeader = false
|
||||
syncedRuntime.leadershipListener = undefined
|
||||
syncedRuntime.stopLeadershipListener.mockClear()
|
||||
resetArtistryToGlobal.mockClear()
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// A follower forwards its startup initialization to the current leader.
|
||||
// The follower therefore has no local active-card watcher when it becomes
|
||||
// the next leader.
|
||||
// A synchronized state snapshot replaced `activeCardId`. The old watcher
|
||||
// interpreted that replicated state as a user command and applied module
|
||||
// settings, which produced another synchronized snapshot.
|
||||
//
|
||||
// https://github.com/moeru-ai/airi/pull/2304
|
||||
it('reinstalls the card watcher when a follower becomes the leader', async () => {
|
||||
// We fixed this by applying settings only from the synchronized activation
|
||||
// action. State replication remains free of runtime side effects.
|
||||
it('applies card settings only through the activation command', async () => {
|
||||
const stageModelStore = useSettingsStageModel()
|
||||
const cardStore = useAiriCardStore()
|
||||
await cardStore.initialize()
|
||||
|
||||
const vrmCardId = cardStore.addCard({
|
||||
const vrmCardId = await cardStore.addCard({
|
||||
name: 'VRM card',
|
||||
version: '1.0.0',
|
||||
description: 'Card for the promoted leader.',
|
||||
@@ -147,7 +132,7 @@ describe('airi-card store', () => {
|
||||
},
|
||||
},
|
||||
}, 'scratch')
|
||||
const live2dCardId = cardStore.addCard({
|
||||
const live2dCardId = await cardStore.addCard({
|
||||
name: 'Live2D card',
|
||||
version: '1.0.0',
|
||||
description: 'Card for the active leader.',
|
||||
@@ -165,24 +150,14 @@ describe('airi-card store', () => {
|
||||
}, 'scratch')
|
||||
|
||||
stageModelStore.stageModelSelected = 'preset-live2d-1'
|
||||
cardStore.startRuntime(syncedPinia)
|
||||
cardStore.activeCardId = vrmCardId
|
||||
|
||||
expect(stageModelStore.stageModelSelected).toBe('preset-live2d-1')
|
||||
|
||||
syncedRuntime.leadershipListener?.(true)
|
||||
await cardStore.activateCard(vrmCardId)
|
||||
expect(stageModelStore.stageModelSelected).toBe('preset-vrm-1')
|
||||
|
||||
cardStore.activeCardId = live2dCardId
|
||||
expect(stageModelStore.stageModelSelected).toBe('preset-live2d-1')
|
||||
cardStore.$patch({ activeCardId: live2dCardId })
|
||||
expect(stageModelStore.stageModelSelected).toBe('preset-vrm-1')
|
||||
|
||||
syncedRuntime.leadershipListener?.(false)
|
||||
cardStore.activeCardId = vrmCardId
|
||||
await cardStore.activateCard(live2dCardId)
|
||||
expect(stageModelStore.stageModelSelected).toBe('preset-live2d-1')
|
||||
|
||||
cardStore.disposeRuntime()
|
||||
expect(syncedRuntime.stopLeadershipListener).toHaveBeenCalledTimes(1)
|
||||
expect(syncedRuntime.leadershipListener).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not create runtime module stores for metadata-only consumers', () => {
|
||||
@@ -205,17 +180,17 @@ describe('airi-card store', () => {
|
||||
* @example
|
||||
* it('persists selected module config on active card', () => {})
|
||||
*/
|
||||
it('persists selected module config on active card', () => {
|
||||
it('persists selected module config on active card', async () => {
|
||||
const stageModelStore = useSettingsStageModel()
|
||||
stageModelStore.stageModelSelected = 'preset-live2d-1'
|
||||
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
await cardStore.initialize()
|
||||
|
||||
expect(cardStore.updateActiveCardDisplayModel('display-model-iru-v2')).toBe(true)
|
||||
expect(cardStore.updateActiveCardConsciousness({ provider: 'openrouter-ai', model: 'anthropic/claude-sonnet' })).toBe(true)
|
||||
expect(cardStore.updateActiveCardVision({ provider: 'ollama', model: 'llava' })).toBe(true)
|
||||
expect(cardStore.updateActiveCardSpeech({ provider: 'elevenlabs', model: 'eleven_multilingual_v2', voice_id: 'aria' })).toBe(true)
|
||||
expect(await cardStore.updateActiveCardDisplayModel('display-model-iru-v2')).toBe(true)
|
||||
expect(await cardStore.updateActiveCardConsciousness({ provider: 'openrouter-ai', model: 'anthropic/claude-sonnet' })).toBe(true)
|
||||
expect(await cardStore.updateActiveCardVision({ provider: 'ollama', model: 'llava' })).toBe(true)
|
||||
expect(await cardStore.updateActiveCardSpeech({ provider: 'elevenlabs', model: 'eleven_multilingual_v2', voice_id: 'aria' })).toBe(true)
|
||||
expect(cardStore.activeCard?.extensions.airi.modules).toMatchObject({
|
||||
displayModelId: 'display-model-iru-v2',
|
||||
consciousness: { provider: 'openrouter-ai', model: 'anthropic/claude-sonnet' },
|
||||
@@ -234,12 +209,12 @@ describe('airi-card store', () => {
|
||||
//
|
||||
// We fixed this by applying card settings from the stable activation key.
|
||||
// https://github.com/moeru-ai/airi/issues/2089
|
||||
it('issue #2089: applies the activated card display model to the stage runtime', () => {
|
||||
it('issue #2089: applies the activated card display model to the stage runtime', async () => {
|
||||
const stageModelStore = useSettingsStageModel()
|
||||
stageModelStore.stageModelSelected = 'preset-live2d-1'
|
||||
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
await cardStore.initialize()
|
||||
|
||||
const card: AiriCard = {
|
||||
name: 'VRM card',
|
||||
@@ -257,21 +232,21 @@ describe('airi-card store', () => {
|
||||
},
|
||||
},
|
||||
}
|
||||
const cardId = cardStore.addCard(card, 'scratch')
|
||||
const cardId = await cardStore.addCard(card, 'scratch')
|
||||
|
||||
cardStore.activeCardId = cardId
|
||||
await cardStore.activateCard(cardId)
|
||||
|
||||
expect(stageModelStore.stageModelSelected).toBe('preset-vrm-1')
|
||||
})
|
||||
|
||||
it('applies edits to the currently active card display model', () => {
|
||||
it('applies edits to the currently active card display model', async () => {
|
||||
const stageModelStore = useSettingsStageModel()
|
||||
stageModelStore.stageModelSelected = 'preset-live2d-1'
|
||||
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
await cardStore.initialize()
|
||||
|
||||
const cardId = cardStore.addCard({
|
||||
const cardId = await cardStore.addCard({
|
||||
name: 'Editable card',
|
||||
version: '1.0.0',
|
||||
description: 'Card whose model can be edited',
|
||||
@@ -287,11 +262,11 @@ describe('airi-card store', () => {
|
||||
},
|
||||
},
|
||||
}, 'scratch')
|
||||
cardStore.activeCardId = cardId
|
||||
await cardStore.activateCard(cardId)
|
||||
|
||||
const card = cardStore.getCard(cardId)
|
||||
expect(card).toBeDefined()
|
||||
cardStore.updateCard(cardId, {
|
||||
await cardStore.updateCard(cardId, {
|
||||
...card!,
|
||||
extensions: {
|
||||
...card!.extensions,
|
||||
@@ -308,6 +283,44 @@ describe('airi-card store', () => {
|
||||
expect(stageModelStore.stageModelSelected).toBe('preset-vrm-1')
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// pinia-plugin-synced applies a structured clone of every synchronized
|
||||
// store. The clone replaced the active card object, so the runtime watcher
|
||||
// treated unchanged card settings as an edit. Applying those settings
|
||||
// mutated other synchronized stores and committed another full snapshot.
|
||||
//
|
||||
// We prevent the feedback loop by applying runtime settings only through an
|
||||
// explicit card command, never in response to a state snapshot.
|
||||
it('does not reapply runtime settings for an unchanged synchronized card snapshot', async () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
await cardStore.initialize()
|
||||
|
||||
const cardId = await cardStore.addCard({
|
||||
name: 'Artistry card',
|
||||
version: '1.0.0',
|
||||
description: 'A card with object-valued runtime settings.',
|
||||
extensions: {
|
||||
airi: {
|
||||
modules: {
|
||||
artistry: {
|
||||
options: { steps: 20 },
|
||||
},
|
||||
},
|
||||
agents: {},
|
||||
},
|
||||
},
|
||||
}, 'scratch')
|
||||
await cardStore.activateCard(cardId)
|
||||
|
||||
const applicationsBeforeSnapshot = resetArtistryToGlobal.mock.calls.length
|
||||
const synchronizedCards = new Map<string, AiriCard>(JSON.parse(JSON.stringify([...cardStore.cards])))
|
||||
|
||||
cardStore.$patch({ cards: synchronizedCards })
|
||||
|
||||
expect(resetArtistryToGlobal).toHaveBeenCalledTimes(applicationsBeforeSnapshot)
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The settings reset clears the runtime model before resetting card state.
|
||||
@@ -315,13 +328,13 @@ describe('airi-card store', () => {
|
||||
// card, allowing its display model to overwrite the reset runtime value.
|
||||
//
|
||||
// https://github.com/moeru-ai/airi/pull/2090#discussion_r3610810272
|
||||
it('does not restore a stale card model during card state reset', () => {
|
||||
it('does not restore a stale card model during card state reset', async () => {
|
||||
const stageModelStore = useSettingsStageModel()
|
||||
stageModelStore.stageModelSelected = 'preset-live2d-1'
|
||||
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
cardStore.updateActiveCardDisplayModel('preset-vrm-1')
|
||||
await cardStore.initialize()
|
||||
await cardStore.updateActiveCardDisplayModel('preset-vrm-1')
|
||||
stageModelStore.stageModelSelected = 'preset-live2d-1'
|
||||
|
||||
cardStore.resetState()
|
||||
@@ -333,11 +346,11 @@ describe('airi-card store', () => {
|
||||
* @example
|
||||
* it('updates speech config on the active card', () => {})
|
||||
*/
|
||||
it('updates speech config on the active card', () => {
|
||||
it('updates speech config on the active card', async () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
await cardStore.initialize()
|
||||
|
||||
expect(cardStore.updateActiveCardSpeech({ provider: 'elevenlabs', model: 'eleven_multilingual_v2', voice_id: 'aria' })).toBe(true)
|
||||
expect(await cardStore.updateActiveCardSpeech({ provider: 'elevenlabs', model: 'eleven_multilingual_v2', voice_id: 'aria' })).toBe(true)
|
||||
expect(cardStore.activeCard?.extensions.airi.modules.speech).toMatchObject({
|
||||
provider: 'elevenlabs',
|
||||
model: 'eleven_multilingual_v2',
|
||||
@@ -345,11 +358,11 @@ describe('airi-card store', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps position-sensitive CCv3 fields separate from the stable system prompt', () => {
|
||||
it('keeps position-sensitive CCv3 fields separate from the stable system prompt', async () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
await cardStore.initialize()
|
||||
|
||||
const cardId = cardStore.addCard({
|
||||
const cardId = await cardStore.addCard({
|
||||
name: 'Runtime context card',
|
||||
version: '1.0.0',
|
||||
systemPrompt: 'Follow the character rules.',
|
||||
@@ -374,7 +387,7 @@ describe('airi-card store', () => {
|
||||
},
|
||||
}, 'scratch')
|
||||
|
||||
cardStore.activeCardId = cardId
|
||||
await cardStore.activateCard(cardId)
|
||||
|
||||
expect(cardStore.systemPrompt).toBe([
|
||||
'Follow the character rules.',
|
||||
@@ -388,53 +401,53 @@ describe('airi-card store', () => {
|
||||
expect(cardStore.systemPrompt).not.toContain('What did you find?')
|
||||
})
|
||||
|
||||
it('falls back to the default card when the active custom card is deleted', () => {
|
||||
it('falls back to the default card when the active custom card is deleted', async () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
await cardStore.initialize()
|
||||
|
||||
const cardId = cardStore.addCard({
|
||||
const cardId = await cardStore.addCard({
|
||||
name: 'Custom card',
|
||||
version: '1.0.0',
|
||||
description: 'A removable card.',
|
||||
}, 'scratch')
|
||||
cardStore.activeCardId = cardId
|
||||
await cardStore.activateCard(cardId)
|
||||
|
||||
cardStore.removeCard(cardId)
|
||||
await cardStore.removeCard(cardId)
|
||||
|
||||
expect(cardStore.cards.has(cardId)).toBe(false)
|
||||
expect(cardStore.activeCardId).toBe('default')
|
||||
expect(cardStore.activeCard?.name).toBe('ReLU')
|
||||
})
|
||||
|
||||
it('keeps the built-in fallback card when deletion is requested directly', () => {
|
||||
it('keeps the built-in fallback card when deletion is requested directly', async () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
await cardStore.initialize()
|
||||
|
||||
expect(cardStore.removeCard('default')).toBe(false)
|
||||
expect(await cardStore.removeCard('default')).toBe(false)
|
||||
expect(cardStore.cards.has('default')).toBe(true)
|
||||
expect(cardStore.activeCardId).toBe('default')
|
||||
})
|
||||
|
||||
it('preserves a valid persisted active card during initialization', () => {
|
||||
it('preserves a valid persisted active card during initialization', async () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
const cardId = cardStore.addCard({
|
||||
const cardId = await cardStore.addCard({
|
||||
name: 'Persisted active card',
|
||||
version: '1.0.0',
|
||||
description: 'Keep this selection.',
|
||||
}, 'scratch')
|
||||
cardStore.activeCardId = cardId
|
||||
|
||||
cardStore.initialize()
|
||||
await cardStore.initialize()
|
||||
|
||||
expect(cardStore.activeCardId).toBe(cardId)
|
||||
expect(cardStore.activeCard?.name).toBe('Persisted active card')
|
||||
})
|
||||
|
||||
it('repairs a dangling persisted active card during initialization', () => {
|
||||
it('repairs a dangling persisted active card during initialization', async () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.activeCardId = 'missing-card'
|
||||
|
||||
cardStore.initialize()
|
||||
await cardStore.initialize()
|
||||
|
||||
expect(cardStore.activeCardId).toBe('default')
|
||||
expect(cardStore.activeCard?.name).toBe('ReLU')
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { Card, ccv3 } from '@proj-airi/ccc'
|
||||
import type { SyncedPiniaRuntime } from 'pinia-plugin-synced'
|
||||
|
||||
import type { AiriCard, AiriExtension } from '../../types/airiCard'
|
||||
|
||||
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SystemPromptV2 from '../../constants/prompts/system-v2'
|
||||
@@ -41,11 +40,13 @@ function resolveSystemPrompt(card: AiriCard | undefined): string {
|
||||
export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
const { t } = useI18n()
|
||||
|
||||
const cards = useLocalStorageManualReset<Map<string, AiriCard>>('airi-cards', new Map())
|
||||
const activeCardId = useLocalStorageManualReset<string>('airi-card-active-id', 'default')
|
||||
// Pinia synchronization owns cross-window updates. Local storage only loads
|
||||
// and saves this renderer's durable copy; listening to storage events here
|
||||
// would create a second cross-window state channel and echo cloned maps.
|
||||
const cards = useLocalStorageManualReset<Map<string, AiriCard>>('airi-cards', new Map(), { listenToStorageChanges: false })
|
||||
const activeCardId = useLocalStorageManualReset<string>('airi-card-active-id', 'default', { listenToStorageChanges: false })
|
||||
|
||||
const activeCard = computed(() => cards.value.get(activeCardId.value))
|
||||
|
||||
function useRuntimeModuleStores() {
|
||||
return {
|
||||
artistry: useArtistryStore(),
|
||||
@@ -62,14 +63,14 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
* from an existing card (profile switcher). Required so a new call site
|
||||
* can't silently degrade creation attribution.
|
||||
*/
|
||||
const addCard = (card: AiriCard | Card | ccv3.CharacterCardV3, source: 'scratch' | 'import' | 'duplicate') => {
|
||||
const addCard = async (card: AiriCard | Card | ccv3.CharacterCardV3, source: 'scratch' | 'import' | 'duplicate') => {
|
||||
const newCardId = nanoid()
|
||||
cards.value.set(newCardId, newAiriCard(card))
|
||||
captureAnalyticsEvent('card_created', { card_id: newCardId, source })
|
||||
return newCardId
|
||||
}
|
||||
|
||||
const removeCard = (id: string) => {
|
||||
const removeCard = async (id: string) => {
|
||||
// The built-in card is the guaranteed fallback for every runtime profile.
|
||||
if (id === 'default')
|
||||
return false
|
||||
@@ -80,14 +81,16 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
|
||||
// The active id is persisted independently from the card map. Reset it
|
||||
// before consumers observe a dangling runtime profile after deletion.
|
||||
if (activeCardId.value === id)
|
||||
if (activeCardId.value === id) {
|
||||
activeCardId.value = 'default'
|
||||
applyActiveCardSettings()
|
||||
}
|
||||
|
||||
captureAnalyticsEvent('character_deleted', { character_id: id })
|
||||
return true
|
||||
}
|
||||
|
||||
const updateCard = (id: string, updates: AiriCard | Card | ccv3.CharacterCardV3) => {
|
||||
const updateCard = async (id: string, updates: AiriCard | Card | ccv3.CharacterCardV3) => {
|
||||
const existingCard = cards.value.get(id)
|
||||
if (!existingCard)
|
||||
return false
|
||||
@@ -97,7 +100,11 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
...updates,
|
||||
}
|
||||
|
||||
cards.value.set(id, newAiriCard(updatedCard))
|
||||
const card = newAiriCard(updatedCard)
|
||||
cards.value.set(id, card)
|
||||
if (id === activeCardId.value)
|
||||
applyActiveCardSettings(card)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -129,25 +136,37 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
return true
|
||||
}
|
||||
|
||||
function updateActiveCardDisplayModel(displayModelId: string | undefined) {
|
||||
return updateActiveCardModules(() => ({ displayModelId }))
|
||||
async function updateActiveCardDisplayModel(displayModelId: string | undefined) {
|
||||
const updated = updateActiveCardModules(() => ({ displayModelId }))
|
||||
if (updated)
|
||||
applyActiveCardSettings()
|
||||
return updated
|
||||
}
|
||||
|
||||
function updateActiveCardConsciousness(consciousness: AiriExtension['modules']['consciousness']) {
|
||||
return updateActiveCardModules(() => ({ consciousness }))
|
||||
async function updateActiveCardConsciousness(consciousness: AiriExtension['modules']['consciousness']) {
|
||||
const updated = updateActiveCardModules(() => ({ consciousness }))
|
||||
if (updated)
|
||||
applyActiveCardSettings()
|
||||
return updated
|
||||
}
|
||||
|
||||
function updateActiveCardVision(vision: AiriExtension['modules']['vision']) {
|
||||
return updateActiveCardModules(() => ({ vision }))
|
||||
async function updateActiveCardVision(vision: AiriExtension['modules']['vision']) {
|
||||
const updated = updateActiveCardModules(() => ({ vision }))
|
||||
if (updated)
|
||||
applyActiveCardSettings()
|
||||
return updated
|
||||
}
|
||||
|
||||
function updateActiveCardSpeech(speech: Pick<AiriExtension['modules']['speech'], 'provider' | 'model' | 'voice_id'>) {
|
||||
return updateActiveCardModules(({ modules }) => ({
|
||||
async function updateActiveCardSpeech(speech: Pick<AiriExtension['modules']['speech'], 'provider' | 'model' | 'voice_id'>) {
|
||||
const updated = updateActiveCardModules(({ modules }) => ({
|
||||
speech: {
|
||||
...modules.speech,
|
||||
...speech,
|
||||
},
|
||||
}))
|
||||
if (updated)
|
||||
applyActiveCardSettings()
|
||||
return updated
|
||||
}
|
||||
|
||||
function resolveAiriExtension(card: Card | ccv3.CharacterCardV3): AiriExtension {
|
||||
@@ -309,7 +328,20 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
if (!cards.value.has(activeCardId.value))
|
||||
activeCardId.value = 'default'
|
||||
|
||||
initializeRuntimeModules()
|
||||
applyActiveCardSettings()
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a card and applies its module settings in the synchronization
|
||||
* leader. Replicated state snapshots never invoke this command.
|
||||
*/
|
||||
async function activateCard(id: string) {
|
||||
if (!cards.value.has(id))
|
||||
return false
|
||||
|
||||
activeCardId.value = id
|
||||
applyActiveCardSettings()
|
||||
return true
|
||||
}
|
||||
|
||||
function applyActiveCardSettings(newCard = activeCard.value) {
|
||||
@@ -360,55 +392,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
}
|
||||
|
||||
let stopLeadershipListener: (() => void) | undefined
|
||||
let stopRuntimeModuleWatcher: (() => void) | undefined
|
||||
|
||||
function initializeRuntimeModules() {
|
||||
if (stopRuntimeModuleWatcher)
|
||||
return
|
||||
|
||||
applyActiveCardSettings()
|
||||
|
||||
// Activation changes the stable card ID, while card editors replace the
|
||||
// active card object without changing that ID. Only the Stage lifecycle
|
||||
// owner applies those settings; metadata-only consumers stay lightweight.
|
||||
stopRuntimeModuleWatcher = watch([activeCardId, activeCard], ([, newCard]) => {
|
||||
applyActiveCardSettings(newCard)
|
||||
}, { flush: 'sync' })
|
||||
}
|
||||
|
||||
function stopRuntimeModules() {
|
||||
stopRuntimeModuleWatcher?.()
|
||||
stopRuntimeModuleWatcher = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps renderer-local card settings active only in the current leader.
|
||||
* Repeated calls keep the first listener until {@link disposeRuntime} runs.
|
||||
*/
|
||||
function startRuntime(syncedPinia: Pick<SyncedPiniaRuntime, 'onLeadershipChange'>) {
|
||||
if (stopLeadershipListener)
|
||||
return
|
||||
|
||||
stopLeadershipListener = syncedPinia.onLeadershipChange((isLeader) => {
|
||||
if (isLeader)
|
||||
initializeRuntimeModules()
|
||||
else
|
||||
stopRuntimeModules()
|
||||
})
|
||||
}
|
||||
|
||||
/** Stops renderer-local card settings and leadership tracking. */
|
||||
function disposeRuntime() {
|
||||
stopRuntimeModules()
|
||||
stopLeadershipListener?.()
|
||||
stopLeadershipListener = undefined
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
// Clear card data before the selected ID. Otherwise the synchronous
|
||||
// activation watcher can briefly resolve the old default card and restore
|
||||
// its display model during a full settings reset.
|
||||
cards.reset()
|
||||
activeCardId.reset()
|
||||
}
|
||||
@@ -427,8 +411,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
getCard,
|
||||
resetState,
|
||||
initialize,
|
||||
startRuntime,
|
||||
disposeRuntime,
|
||||
activateCard,
|
||||
|
||||
currentModels: computed(() => {
|
||||
const {
|
||||
@@ -460,7 +443,17 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
}, {
|
||||
synced: {
|
||||
actions: ['initialize'],
|
||||
actions: [
|
||||
'activateCard',
|
||||
'addCard',
|
||||
'initialize',
|
||||
'removeCard',
|
||||
'updateActiveCardConsciousness',
|
||||
'updateActiveCardDisplayModel',
|
||||
'updateActiveCardSpeech',
|
||||
'updateActiveCardVision',
|
||||
'updateCard',
|
||||
],
|
||||
state: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -271,7 +271,7 @@ LATEST ${target === 'assistant' ? 'COMPANION RESPONSE' : 'USER INPUT'}:
|
||||
switch (spawnMode) {
|
||||
case 'bg':
|
||||
// Update character's active background
|
||||
cardStore.updateCard(cardId, {
|
||||
await cardStore.updateCard(cardId, {
|
||||
extensions: {
|
||||
...activeCard.extensions,
|
||||
airi: {
|
||||
@@ -322,7 +322,7 @@ LATEST ${target === 'assistant' ? 'COMPANION RESPONSE' : 'USER INPUT'}:
|
||||
case 'bg_widget':
|
||||
default:
|
||||
// Both: Update background AND spawn widget
|
||||
cardStore.updateCard(cardId, {
|
||||
await cardStore.updateCard(cardId, {
|
||||
extensions: {
|
||||
...activeCard.extensions,
|
||||
airi: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useArtistryStore } from './artistry'
|
||||
|
||||
@@ -26,4 +27,51 @@ describe('artistry store', () => {
|
||||
// @example
|
||||
expect(artistryStore.configured).toBe(false)
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// pinia-plugin-synced restores the whole store with structured clones. The
|
||||
// global and active provider options then have equal values but different
|
||||
// object identities. Watching the global object and assigning it to the
|
||||
// active object creates a second mutation after the synchronized patch.
|
||||
// That mutation broadcasts another full snapshot and repeats indefinitely.
|
||||
//
|
||||
// We fixed this by making global-to-active resolution an explicit store
|
||||
// operation and by preventing persistence from reflecting its own write.
|
||||
it('does not mutate active options after applying an equal synchronized snapshot', async () => {
|
||||
const artistryStore = useArtistryStore()
|
||||
artistryStore.globalProviderOptions = { steps: 20 }
|
||||
artistryStore.providerOptions = { steps: 20 }
|
||||
artistryStore.comfyuiSavedWorkflows = [{
|
||||
id: 'workflow-1',
|
||||
name: 'Workflow',
|
||||
workflow: {},
|
||||
exposedFields: {},
|
||||
}]
|
||||
await nextTick()
|
||||
|
||||
let mutationCount = 0
|
||||
const stopSubscription = artistryStore.$subscribe(() => {
|
||||
mutationCount += 1
|
||||
}, { flush: 'sync' })
|
||||
|
||||
artistryStore.$patch((currentState) => {
|
||||
Object.assign(currentState, {
|
||||
globalProviderOptions: { steps: 20 },
|
||||
providerOptions: { steps: 20 },
|
||||
comfyuiSavedWorkflows: [{
|
||||
id: 'workflow-1',
|
||||
name: 'Workflow',
|
||||
workflow: {},
|
||||
exposedFields: {},
|
||||
}],
|
||||
})
|
||||
})
|
||||
const mutationCountAfterSnapshot = mutationCount
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(mutationCount).toBe(mutationCountAfterSnapshot)
|
||||
stopSubscription()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,11 +20,16 @@ export interface ComfyUIWorkflowTemplate {
|
||||
}
|
||||
|
||||
export const useArtistryStore = defineStore('artistry', () => {
|
||||
// Pinia synchronization is the only cross-window state channel. These refs
|
||||
// still load and save durable values, but storage events must not echo a
|
||||
// second copy of the same state between Electron renderers.
|
||||
const persistenceOptions = { listenToStorageChanges: false }
|
||||
|
||||
// --- Persistent Global Settings (User Preferences) ---
|
||||
const globalProvider = useLocalStorageManualReset<string>('artistry-provider', 'none')
|
||||
const globalModel = useLocalStorageManualReset<string>('artistry-model', '')
|
||||
const globalPromptPrefix = useLocalStorageManualReset<string>('artistry-prompt-prefix', '')
|
||||
const globalProviderOptions = useLocalStorageManualReset<Record<string, any> | undefined>('artistry-provider-options', undefined)
|
||||
const globalProvider = useLocalStorageManualReset<string>('artistry-provider', 'none', persistenceOptions)
|
||||
const globalModel = useLocalStorageManualReset<string>('artistry-model', '', persistenceOptions)
|
||||
const globalPromptPrefix = useLocalStorageManualReset<string>('artistry-prompt-prefix', '', persistenceOptions)
|
||||
const globalProviderOptions = useLocalStorageManualReset<Record<string, any> | undefined>('artistry-provider-options', undefined, persistenceOptions)
|
||||
|
||||
// --- Active settings (transient, can be overridden by cards) ---
|
||||
const activeProvider = ref(globalProvider.value)
|
||||
@@ -36,40 +41,48 @@ export const useArtistryStore = defineStore('artistry', () => {
|
||||
const comfyuiServerUrl = useLocalStorageManualReset<string>(
|
||||
'artistry-comfyui-server-url',
|
||||
'http://localhost:8188',
|
||||
persistenceOptions,
|
||||
)
|
||||
const comfyuiSavedWorkflows = useLocalStorageManualReset<ComfyUIWorkflowTemplate[]>(
|
||||
'artistry-comfyui-saved-workflows',
|
||||
[],
|
||||
persistenceOptions,
|
||||
)
|
||||
const comfyuiActiveWorkflow = useLocalStorageManualReset<string>(
|
||||
'artistry-comfyui-active-workflow',
|
||||
'',
|
||||
persistenceOptions,
|
||||
)
|
||||
|
||||
// --- Replicate provider settings ---
|
||||
const replicateApiKey = useLocalStorageManualReset<string>('artistry-replicate-api-key', '')
|
||||
const replicateApiKey = useLocalStorageManualReset<string>('artistry-replicate-api-key', '', persistenceOptions)
|
||||
const replicateDefaultModel = useLocalStorageManualReset<string>(
|
||||
'artistry-replicate-default-model',
|
||||
'black-forest-labs/flux-schnell',
|
||||
persistenceOptions,
|
||||
)
|
||||
const replicateAspectRatio = useLocalStorageManualReset<string>(
|
||||
'artistry-replicate-aspect-ratio',
|
||||
'16:9',
|
||||
persistenceOptions,
|
||||
)
|
||||
const replicateInferenceSteps = useLocalStorageManualReset<number>(
|
||||
'artistry-replicate-inference-steps',
|
||||
4,
|
||||
persistenceOptions,
|
||||
)
|
||||
|
||||
// --- Nano Banana (Google AI Studio) provider settings ---
|
||||
const nanobananaApiKey = useLocalStorageManualReset<string>('artistry-nanobanana-api-key', '')
|
||||
const nanobananaApiKey = useLocalStorageManualReset<string>('artistry-nanobanana-api-key', '', persistenceOptions)
|
||||
const nanobananaModel = useLocalStorageManualReset<string>(
|
||||
'artistry-nanobanana-model',
|
||||
'gemini-3.1-flash-image-preview',
|
||||
persistenceOptions,
|
||||
)
|
||||
const nanobananaResolution = useLocalStorageManualReset<string>(
|
||||
'artistry-nanobanana-resolution',
|
||||
'1K',
|
||||
persistenceOptions,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -115,7 +128,6 @@ export const useArtistryStore = defineStore('artistry', () => {
|
||||
watch(globalProvider, val => activeProvider.value = val)
|
||||
watch(globalModel, val => activeModel.value = val)
|
||||
watch(globalPromptPrefix, val => defaultPromptPrefix.value = val)
|
||||
watch(globalProviderOptions, val => providerOptions.value = val)
|
||||
|
||||
const configured = computed(() => {
|
||||
if (!activeProvider.value)
|
||||
|
||||
@@ -61,7 +61,7 @@ export const useConsciousnessStore = defineStore('consciousness', () => {
|
||||
// provider's model and chat requests failed upstream with model_not_found.
|
||||
//
|
||||
// The watcher is synchronous on purpose: call sites assign the provider
|
||||
// first and a new model right after (e.g. use-auth-provider-sync), so a
|
||||
// first and a new model right after, so a
|
||||
// deferred reset would wipe the model they just chose. Synchronous flush
|
||||
// makes "set provider, then set model" a safe, ordered operation.
|
||||
//
|
||||
|
||||
@@ -102,7 +102,7 @@ export const useProviderConfigStore = defineStore('provider-config', () => {
|
||||
}
|
||||
|
||||
function getProviderConfig(providerId: string) {
|
||||
return getProvider(providerId)?.config
|
||||
return providers.value[providerId]?.config
|
||||
}
|
||||
|
||||
function ensureProvider(providerId: string, definitionId: string, config: Record<string, unknown> = {}) {
|
||||
@@ -133,7 +133,7 @@ export const useProviderConfigStore = defineStore('provider-config', () => {
|
||||
}
|
||||
|
||||
function setProviderStatus(providerId: string, status: ProviderValidationStatus) {
|
||||
const provider = getProvider(providerId)
|
||||
const provider = providers.value[providerId]
|
||||
if (provider)
|
||||
provider.status = status
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { OFFICIAL_SPEECH_PROVIDER_ID } from '../../libs/providers/providers/official'
|
||||
import { useProviderConfigStore } from './config'
|
||||
import { useProviderStore } from './provider'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
@@ -39,6 +41,52 @@ describe('provider store synchronization boundary', () => {
|
||||
expect(store.providerRuntimeState.openai).toEqual(runtimeState)
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The provider store installed immediate watchers that called synchronized
|
||||
// background actions. Every renderer created the same watchers, so one
|
||||
// shared state transition produced one routed action per renderer.
|
||||
//
|
||||
// We fixed this by keeping background work behind explicit action calls.
|
||||
it('does not start background provider actions when shared configuration changes', async () => {
|
||||
const store = useProviderStore()
|
||||
const configStore = useProviderConfigStore()
|
||||
|
||||
await nextTick()
|
||||
await new Promise<void>(resolve => queueMicrotask(resolve))
|
||||
|
||||
const refreshValidation = vi.spyOn(store, 'refreshListedProviderValidation').mockResolvedValue()
|
||||
const refreshModels = vi.spyOn(store, 'refreshModelsForChangedCredentials').mockResolvedValue()
|
||||
|
||||
configStore.ensureProvider('openai', 'openai', { apiKey: 'test-key' })
|
||||
await nextTick()
|
||||
await new Promise<void>(resolve => queueMicrotask(resolve))
|
||||
|
||||
expect(refreshValidation).not.toHaveBeenCalled()
|
||||
expect(refreshModels).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// Provider metadata projection called the config store's `getProvider`
|
||||
// action once for every registered provider. Pinia tracing and plugins then
|
||||
// processed hundreds of action lifecycle events during renderer startup,
|
||||
// even though each call was only a read.
|
||||
// Internal provider projections now read the reactive provider map directly.
|
||||
it('does not dispatch config actions while projecting provider metadata', async () => {
|
||||
const configStore = useProviderConfigStore()
|
||||
let getProviderCalls = 0
|
||||
configStore.$onAction(({ name }) => {
|
||||
if (name === 'getProvider')
|
||||
getProviderCalls += 1
|
||||
})
|
||||
|
||||
useProviderStore()
|
||||
await nextTick()
|
||||
|
||||
expect(getProviderCalls).toBe(0)
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// A model request kept a reference to its runtime entry across an await.
|
||||
|
||||
@@ -19,7 +19,7 @@ import { computedAsync, useIntervalFn } from '@vueuse/core'
|
||||
import { listModels } from '@xsai/model'
|
||||
import { uniqBy } from 'es-toolkit'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
validateProvider as runProviderValidation,
|
||||
} from '../../libs/providers'
|
||||
import { selectProviderMetadata, selectProvidersMetadata } from '../../libs/providers/metadata'
|
||||
import { useAuthStore } from '../auth'
|
||||
import { useProviderConfigStore } from './config'
|
||||
|
||||
export type { ModelInfo, VoiceInfo } from '../../libs/providers/types'
|
||||
@@ -72,20 +71,15 @@ export const useProviderStore = defineStore('provider', () => {
|
||||
const providerStateStore = useProviderStateStore()
|
||||
const providerCredentials = computed(() => providerConfigStore.configs)
|
||||
const addedProviders = computed(() => providerConfigStore.addedProviders)
|
||||
// Synced state applies fresh object snapshots. Compare serialized values so
|
||||
// an equivalent snapshot does not restart validation and publish more state.
|
||||
const providerCredentialsSignature = computed(() => JSON.stringify(providerCredentials.value))
|
||||
const addedProvidersSignature = computed(() => JSON.stringify(addedProviders.value))
|
||||
// Provider instances contain functions and transport handles. Keep this map
|
||||
// private so it never enters Pinia state.
|
||||
const providerInstanceCache = new Map<string, unknown>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const authState = useAuthStore()
|
||||
const VISION_PROVIDER_ID_PREFIX = 'vision-'
|
||||
|
||||
function getProviderDefinitionId(providerId: string) {
|
||||
const configuredProvider = providerConfigStore.getProvider(providerId)
|
||||
const configuredProvider = providerConfigStore.providers[providerId]
|
||||
if (configuredProvider)
|
||||
return configuredProvider.definitionId
|
||||
|
||||
@@ -262,7 +256,7 @@ export const useProviderStore = defineStore('provider', () => {
|
||||
initializeProviderRuntimeState(providerId)
|
||||
const configString = JSON.stringify(config || {})
|
||||
const runtimeState = providerRuntimeState.value[providerId]
|
||||
const configuredProvider = providerConfigStore.getProvider(providerId)
|
||||
const configuredProvider = providerConfigStore.providers[providerId]
|
||||
const cacheKey = `${providerId}:${configString}`
|
||||
const forceValidation = options.force === true
|
||||
|
||||
@@ -414,17 +408,6 @@ export const useProviderStore = defineStore('provider', () => {
|
||||
startPeriodicRuntimeValidation()
|
||||
}
|
||||
|
||||
function requestListedProviderValidation() {
|
||||
// Store setup runs before pinia-plugin-synced installs its action wrappers.
|
||||
// Defer the public-store lookup so background validation is routed to the
|
||||
// leader instead of running independently in every renderer.
|
||||
queueMicrotask(() => void useProviderStore().refreshListedProviderValidation())
|
||||
}
|
||||
|
||||
watch(providerCredentialsSignature, requestListedProviderValidation, { immediate: true })
|
||||
watch(addedProvidersSignature, requestListedProviderValidation)
|
||||
watch(() => authState.isAuthenticated, requestListedProviderValidation)
|
||||
|
||||
// Available providers (only those that are properly configured)
|
||||
const availableProviders = computed(() => Object.values(providerConfigStore.providers)
|
||||
.filter(provider => provider.status === 'configured')
|
||||
@@ -679,18 +662,14 @@ export const useProviderStore = defineStore('provider', () => {
|
||||
await disposeProviderInstance(providerId)
|
||||
|
||||
// If the provider is configured and has the capability, refetch its models
|
||||
if (providerConfigStore.getProvider(providerId)?.status === 'configured' && supportsModelListing(providerId)) {
|
||||
if (providerConfigStore.providers[providerId]?.status === 'configured' && supportsModelListing(providerId)) {
|
||||
await fetchModelsForProvider(providerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(providerCredentialsSignature, () => {
|
||||
queueMicrotask(() => void useProviderStore().refreshModelsForChangedCredentials())
|
||||
}, { immediate: true })
|
||||
|
||||
function projectProvider(providerId: string): ProviderMetadata | undefined {
|
||||
const configuredProvider = providerConfigStore.getProvider(providerId)
|
||||
const configuredProvider = providerConfigStore.providers[providerId]
|
||||
const metadata = providerMetadata[providerId]
|
||||
?? providerMetadata[configuredProvider?.definitionId ?? '']
|
||||
|
||||
|
||||
Generated
+29
-239
@@ -877,8 +877,8 @@ catalogs:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
pinia-plugin-synced:
|
||||
specifier: ^0.1.0
|
||||
version: 0.1.0
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3
|
||||
pixi-filters:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -2127,7 +2127,7 @@ importers:
|
||||
version: 3.0.2(electron@41.2.1)
|
||||
'@electron-toolkit/tsconfig':
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0(@types/node@24.12.2)
|
||||
version: 2.0.0(@types/node@25.6.0)
|
||||
'@electron-toolkit/utils':
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0(electron@41.2.1)
|
||||
@@ -2166,7 +2166,7 @@ importers:
|
||||
version: 3.1.0
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: 'catalog:'
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.7.0))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.7.0))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: 'catalog:'
|
||||
version: 1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)
|
||||
@@ -2202,10 +2202,10 @@ importers:
|
||||
version: link:../../packages/ui-transitions
|
||||
'@proj-airi/unplugin-fetch':
|
||||
specifier: 'catalog:'
|
||||
version: 0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@proj-airi/unplugin-live2d-sdk':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
'@types/audioworklet':
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.97
|
||||
@@ -2232,7 +2232,7 @@ importers:
|
||||
version: 2.10.3
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/volar':
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
@@ -2265,7 +2265,7 @@ importers:
|
||||
version: 6.8.3
|
||||
electron-vite:
|
||||
specifier: 'catalog:'
|
||||
version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
get-port-please:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.0
|
||||
@@ -2286,31 +2286,31 @@ importers:
|
||||
version: 2.2.6
|
||||
unocss-preset-scrollbar:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
unplugin-info:
|
||||
specifier: 'catalog:'
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-yaml:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-bundle-visualizer:
|
||||
specifier: 'catalog:'
|
||||
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
|
||||
vite-plugin-mkcert:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: 'catalog:'
|
||||
version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vite-plugin-vue-layouts:
|
||||
specifier: 'catalog:'
|
||||
version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-macros:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 3.1.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.6(typescript@5.9.3)
|
||||
@@ -4437,7 +4437,7 @@ importers:
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3))
|
||||
pinia-plugin-synced:
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.0(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 0.1.3(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
posthog-js:
|
||||
specifier: 'catalog:'
|
||||
version: 1.306.1
|
||||
@@ -17170,8 +17170,8 @@ packages:
|
||||
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pinia-plugin-synced@0.1.0:
|
||||
resolution: {integrity: sha512-53LEjp6A5ub16fwPdM01ZwYtLF+uG5i3JOfwbA5/m1j7cmiIAdlb4dSd5NcY+fZDw51f/DXWBiLAqV/C1qyKTg==}
|
||||
pinia-plugin-synced@0.1.3:
|
||||
resolution: {integrity: sha512-qisOMMeMClWqHMdJSBZzIGxLQKRTWA2dqF48l++Ys1ay91fOUuT0UcNZWYsrs4eP1MvAR3DMXskknen8iYyq6Q==}
|
||||
peerDependencies:
|
||||
pinia: '>=3.0.4 <5'
|
||||
vue: ^3.5.0
|
||||
@@ -21699,9 +21699,9 @@ snapshots:
|
||||
dependencies:
|
||||
electron: 41.2.1
|
||||
|
||||
'@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)':
|
||||
'@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)':
|
||||
dependencies:
|
||||
'@types/node': 24.12.2
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@electron-toolkit/utils@4.0.0(electron@41.2.1)':
|
||||
dependencies:
|
||||
@@ -22638,31 +22638,6 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.7.0))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.7.0))
|
||||
'@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))
|
||||
'@intlify/shared': 11.3.2
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@typescript-eslint/scope-manager': 8.63.0
|
||||
'@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
pathe: 2.0.3
|
||||
picocolors: 1.1.1
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vue-i18n: 11.3.2(vue@3.5.32(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/compiler-dom'
|
||||
- eslint
|
||||
- rollup
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.7.0))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.7.0))
|
||||
@@ -24793,35 +24768,11 @@ snapshots:
|
||||
|
||||
'@proj-airi/unocss-preset-chromatic@1.1.1': {}
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
yauzl: 3.3.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- '@vitejs/devtools'
|
||||
- esbuild
|
||||
- jiti
|
||||
- less
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
@@ -26556,12 +26507,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||
@@ -26837,15 +26782,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
@@ -29000,7 +28936,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
|
||||
@@ -29008,7 +28944,7 @@ snapshots:
|
||||
esbuild: 0.25.12
|
||||
magic-string: 0.30.21
|
||||
picocolors: 1.1.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -32941,7 +32877,7 @@ snapshots:
|
||||
pify@4.0.1:
|
||||
optional: true
|
||||
|
||||
pinia-plugin-synced@0.1.0(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
pinia-plugin-synced@0.1.3(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@moeru/std': 0.1.0-beta.20
|
||||
es-toolkit: 1.50.0
|
||||
@@ -35168,6 +35104,11 @@ snapshots:
|
||||
'@unocss/preset-mini': 66.6.8
|
||||
unocss: 66.6.8(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))):
|
||||
dependencies:
|
||||
'@unocss/preset-mini': 66.6.8
|
||||
unocss: 66.6.8(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
unocss@66.6.8(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@unocss/cli': 66.6.8
|
||||
@@ -35234,14 +35175,6 @@ snapshots:
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-rc.16
|
||||
rollup: 4.60.1
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
@@ -35263,19 +35196,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
git-url-parse: 16.1.0
|
||||
simple-git: 3.36.0
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rollup: 4.60.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
@@ -35369,17 +35289,6 @@ snapshots:
|
||||
rollup: 2.80.0
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
unplugin: 3.0.0
|
||||
yaml: 2.8.3
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-rc.16
|
||||
rollup: 4.60.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
@@ -35618,22 +35527,12 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
@@ -35679,21 +35578,6 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
error-stack-parser-es: 1.0.5
|
||||
ohash: 2.0.11
|
||||
open: 10.2.0
|
||||
perfect-debounce: 2.1.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
@@ -35725,13 +35609,6 @@ snapshots:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
supports-color: 10.2.2
|
||||
undici: 8.1.0
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
@@ -35750,20 +35627,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 8.1.1
|
||||
'@vue/devtools-shared': 8.1.1
|
||||
sirv: 3.0.2
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
|
||||
@@ -35778,21 +35641,6 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
|
||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0)
|
||||
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0)
|
||||
'@vue/compiler-dom': 3.5.32
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
@@ -35808,16 +35656,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
vue-router: 5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
@@ -36083,54 +35921,6 @@ snapshots:
|
||||
- vue-tsc
|
||||
- webpack
|
||||
|
||||
vue-macros@3.1.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-prop': 3.1.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3)
|
||||
'@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
unplugin: 2.3.11
|
||||
unplugin-combine: 2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@rspack/core'
|
||||
- '@vueuse/core'
|
||||
- esbuild
|
||||
- rolldown
|
||||
- rollup
|
||||
- typescript
|
||||
- vite
|
||||
- vue-tsc
|
||||
- webpack
|
||||
|
||||
vue-macros@3.1.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.7.0)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(vue@3.5.32(typescript@5.9.3))
|
||||
|
||||
+1
-1
@@ -328,7 +328,7 @@ catalog:
|
||||
pathe: ^2.0.3
|
||||
pg: ^8.20.0
|
||||
pinia: ^3.0.4
|
||||
pinia-plugin-synced: ^0.1.0
|
||||
pinia-plugin-synced: ^0.1.3
|
||||
pixi-filters: ^4.2.0
|
||||
pixi-live2d-display: ^0.4.0
|
||||
playwright: ^1.60.0
|
||||
|
||||
Reference in New Issue
Block a user