feat(core-agent): harden registry buckets and bridge ingest isolation (#1819)

This commit is contained in:
Nashchennc
2026-05-13 12:02:35 +08:00
committed by GitHub
parent fdb8309559
commit e882ba7dd6
9 changed files with 1300 additions and 146 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ export type { AgentSessionPort } from './contracts/session-port'
export type { AgentForegroundStreamPort } from './contracts/stream-port'
export { createChatHooks } from './runtime/agent-hooks'
export type { ContextHistoryEntry, ContextRegistry } from './runtime/context-registry'
export type { ContextHistoryEntry, ContextIngestResult, ContextRegistry } from './runtime/context-registry'
export { createContextRegistry } from './runtime/context-registry'
export {
isContentArrayRelatedError,
@@ -0,0 +1,312 @@
import type { ContextMessage } from '../types/chat'
import { ContextUpdateStrategy } from '@proj-airi/server-shared/types'
import { describe, expect, it } from 'vitest'
import { createContextRegistry } from './context-registry'
type TestContextMessage = ContextMessage & { source?: string }
function createMetadata(pluginId: string, instanceId: string): NonNullable<ContextMessage['metadata']> {
return {
source: {
id: instanceId,
kind: 'plugin',
plugin: {
id: pluginId,
},
},
}
}
function createContextMessage(overrides: Partial<TestContextMessage> = {}): TestContextMessage {
const id = overrides.id ?? 'context-1'
return {
id,
contextId: overrides.contextId ?? id,
strategy: overrides.strategy ?? ContextUpdateStrategy.ReplaceSelf,
text: overrides.text ?? 'context text',
createdAt: overrides.createdAt ?? 1,
...overrides,
}
}
/**
* @example
* const registry = createContextRegistry()
* registry.ingest({ strategy: ContextUpdateStrategy.ReplaceSelf, text: 'now' })
*/
describe('createContextRegistry', () => {
/**
* @example
* replace-self from the same source leaves one active entry and reports replace.
*/
it('replaces the same source bucket for replace-self updates and returns entry count', () => {
const registry = createContextRegistry()
const firstResult = registry.ingest(createContextMessage({
id: 'first',
source: 'sensor',
text: 'first reading',
}))
const secondResult = registry.ingest(createContextMessage({
id: 'second',
source: 'sensor',
text: 'second reading',
}))
expect(firstResult).toEqual({
sourceKey: 'sensor',
mutation: 'replace',
entryCount: 1,
})
expect(secondResult).toEqual({
sourceKey: 'sensor',
mutation: 'replace',
entryCount: 1,
})
expect(registry.snapshot().sensor?.map(message => message.text)).toEqual(['second reading'])
expect(registry.contextHistory().map(message => message.id)).toEqual(['first', 'second'])
})
/**
* @example
* append-self from the same source grows the active bucket and reports append.
*/
it('appends to the same source bucket for append-self updates and returns the new entry count', () => {
const registry = createContextRegistry()
const firstResult = registry.ingest(createContextMessage({
id: 'first',
source: 'sensor',
strategy: ContextUpdateStrategy.AppendSelf,
text: 'first reading',
}))
const secondResult = registry.ingest(createContextMessage({
id: 'second',
source: 'sensor',
strategy: ContextUpdateStrategy.AppendSelf,
text: 'second reading',
}))
expect(firstResult).toEqual({
sourceKey: 'sensor',
mutation: 'append',
entryCount: 1,
})
expect(secondResult).toEqual({
sourceKey: 'sensor',
mutation: 'append',
entryCount: 2,
})
expect(registry.snapshot().sensor?.map(message => message.text)).toEqual(['first reading', 'second reading'])
})
/**
* @example
* metadata.source.plugin.id + metadata.source.id becomes "plugin:instance".
*/
it('resolves metadata source keys before source fallback and unknown fallback', () => {
const registry = createContextRegistry()
const pluginInstanceResult = registry.ingest(createContextMessage({
id: 'with-instance',
source: 'fallback-source',
metadata: createMetadata('weather', 'station-1'),
}))
const pluginOnlyResult = registry.ingest(createContextMessage({
id: 'plugin-only',
metadata: createMetadata('weather', ''),
}))
const sourceResult = registry.ingest(createContextMessage({
id: 'source-only',
source: 'legacy-source',
}))
const unknownResult = registry.ingest(createContextMessage({
id: 'unknown-source',
}))
expect(pluginInstanceResult?.sourceKey).toBe('weather:station-1')
expect(pluginOnlyResult?.sourceKey).toBe('weather')
expect(sourceResult?.sourceKey).toBe('legacy-source')
expect(unknownResult?.sourceKey).toBe('unknown')
expect(Object.keys(registry.snapshot())).toEqual([
'weather:station-1',
'weather',
'legacy-source',
'unknown',
])
})
/**
* @example
* createContextRegistry({ historyLimit: 2 }) keeps only the two newest history entries.
*/
it('trims context history to the configured history limit', () => {
const registry = createContextRegistry({ historyLimit: 2 })
registry.ingest(createContextMessage({ id: 'first', source: 'sensor' }))
registry.ingest(createContextMessage({ id: 'second', source: 'sensor' }))
registry.ingest(createContextMessage({ id: 'third', source: 'sensor' }))
expect(registry.contextHistory().map(message => message.id)).toEqual(['second', 'third'])
})
/**
* @example
* createContextRegistry() keeps the latest 400 history entries by default.
*/
it('trims context history to the default 400 record history limit', () => {
const registry = createContextRegistry()
for (let index = 0; index < 401; index += 1) {
registry.ingest(createContextMessage({
id: `context-${index}`,
source: 'sensor',
}))
}
const historyIds = registry.contextHistory().map(message => message.id)
expect(historyIds).toHaveLength(400)
expect(historyIds[0]).toBe('context-1')
expect(historyIds.at(-1)).toBe('context-400')
})
/**
* @example
* "__proto__" is a valid source key and cannot rewrite the snapshot prototype.
*/
it('keeps __proto__ source keys as bucket data instead of mutating object prototypes', () => {
const registry = createContextRegistry()
const result = registry.ingest(createContextMessage({
id: 'proto-source',
source: '__proto__',
text: 'safe proto bucket',
}))
const snapshot = registry.snapshot()
expect(result).toEqual({
sourceKey: '__proto__',
mutation: 'replace',
entryCount: 1,
})
expect(Object.getPrototypeOf(snapshot)).toBe(Object.prototype)
expect(Object.hasOwn(snapshot, '__proto__')).toBe(true)
expect(Object.getOwnPropertyDescriptor(snapshot, '__proto__')?.value?.map((message: ContextMessage) => message.text)).toEqual(['safe proto bucket'])
})
/**
* @example
* "toString" is a valid source key and cannot collide with inherited methods.
*/
it('keeps toString source keys as bucket data instead of colliding with inherited methods', () => {
const registry = createContextRegistry()
const firstResult = registry.ingest(createContextMessage({
id: 'first',
source: 'toString',
strategy: ContextUpdateStrategy.AppendSelf,
text: 'first toString bucket entry',
}))
const secondResult = registry.ingest(createContextMessage({
id: 'second',
source: 'toString',
strategy: ContextUpdateStrategy.AppendSelf,
text: 'second toString bucket entry',
}))
expect(firstResult?.entryCount).toBe(1)
expect(secondResult).toEqual({
sourceKey: 'toString',
mutation: 'append',
entryCount: 2,
})
expect(Object.getOwnPropertyDescriptor(registry.snapshot(), 'toString')?.value?.map((message: ContextMessage) => message.text)).toEqual([
'first toString bucket entry',
'second toString bucket entry',
])
})
/**
* @example
* Mutating a returned snapshot never mutates the registry internals.
*/
it('returns cloned snapshots and active contexts so external mutation cannot pollute the registry', () => {
const registry = createContextRegistry()
registry.ingest(createContextMessage({
source: 'sensor',
text: 'original',
}))
const snapshot = registry.snapshot()
const activeContexts = registry.activeContexts()
const snapshotMessage = snapshot.sensor?.[0]
const activeContextMessage = activeContexts.sensor?.[0]
expect(snapshotMessage).toBeDefined()
expect(activeContextMessage).toBeDefined()
if (!snapshotMessage || !activeContextMessage)
throw new Error('Expected cloned registry messages to exist')
snapshotMessage.text = 'mutated snapshot'
activeContextMessage.text = 'mutated active context'
expect(registry.snapshot().sensor?.[0]?.text).toBe('original')
})
/**
* @example
* Unknown strategies return undefined but remain visible in history.
*/
it('records unknown strategies in history without returning a mutation result', () => {
const registry = createContextRegistry()
const unsupportedStrategy = 'unknown-strategy' as ContextMessage['strategy']
const result = registry.ingest(createContextMessage({
id: 'unsupported',
source: 'sensor',
strategy: unsupportedStrategy,
}))
expect(result).toBeUndefined()
expect(registry.contextHistory()).toEqual([
expect.objectContaining({
id: 'unsupported',
sourceKey: 'sensor',
}),
])
expect(registry.activeContexts().sensor).toEqual([])
})
/**
* @example
* Failed cloning leaves the registry exactly as it was before ingest.
*/
it('keeps registry state unchanged when an envelope cannot be cloned', () => {
const registry = createContextRegistry()
registry.ingest(createContextMessage({
id: 'stable',
source: 'sensor',
text: 'stable context',
}))
expect(() => registry.ingest(createContextMessage({
id: 'uncloneable',
source: 'broken-source',
content: () => 'functions cannot be structured-cloned',
}))).toThrow()
expect(registry.snapshot()).toEqual({
sensor: [
expect.objectContaining({
id: 'stable',
text: 'stable context',
}),
],
})
expect(registry.contextHistory().map(message => message.id)).toEqual(['stable'])
})
})
@@ -10,20 +10,54 @@ interface EventSourcePayload {
metadata?: { source?: MetadataEventSource }
}
/**
* Stored context event with the registry bucket key resolved at ingest time.
*/
export interface ContextHistoryEntry extends ContextMessage {
/** Stable source bucket key derived from metadata, source, or fallback. */
sourceKey: string
}
/**
* Observable result emitted when a context update mutates an active bucket.
*/
export interface ContextIngestResult {
/** Stable source bucket key affected by the ingest. */
sourceKey: string
/** Registry mutation applied to the active bucket. */
mutation: 'replace' | 'append'
/** Number of active entries in the affected bucket after mutation. */
entryCount: number
}
/**
* Mutable runtime registry for active context buckets and bounded ingest history.
*/
export interface ContextRegistry {
ingest: (envelope: ContextMessage) => void
/** Stores a context message and returns a mutation summary for known strategies. */
ingest: (envelope: ContextMessage) => ContextIngestResult | undefined
/** Clears active context buckets and ingest history. */
reset: () => void
/** Returns a cloned active context bucket snapshot. */
snapshot: () => Record<string, ContextMessage[]>
/** Returns cloned active context buckets for callers that prefer explicit naming. */
activeContexts: () => Record<string, ContextMessage[]>
/** Returns cloned ingest history entries in chronological order. */
contextHistory: () => ContextHistoryEntry[]
}
interface CreateContextRegistryOptions {
/**
* Maximum number of history records retained by the registry.
*
* @default 400
*/
historyLimit?: number
/**
* Resolves a context message into a stable source bucket key.
*
* @default metadata plugin/instance key, then event source, then "unknown"
*/
getSourceKey?: (event: EventSourcePayload, fallback?: string) => string
}
@@ -45,26 +79,52 @@ function defaultGetSourceKey(event: EventSourcePayload, fallback = 'unknown') {
)
}
/**
* Creates a context registry that owns active buckets and bounded ingest history.
*
* Use when:
* - Runtime contexts need replace-self or append-self bucket semantics.
* - UI or transport layers need cloned snapshots without owning mutation policy.
*
* Expects:
* - Context messages are structured-cloneable before they enter the registry.
* - Unknown strategies should still be recorded in history for observability.
*
* Returns:
* - A registry whose snapshots cannot mutate internal active bucket state.
*/
export function createContextRegistry(options: CreateContextRegistryOptions = {}): ContextRegistry {
const historyLimit = options.historyLimit ?? 400
const getSourceKey = options.getSourceKey ?? defaultGetSourceKey
let currentActiveContexts: Record<string, ContextMessage[]> = {}
let currentActiveContexts = new Map<string, ContextMessage[]>()
let currentContextHistory: ContextHistoryEntry[] = []
function ingest(envelope: ContextMessage) {
function ingest(envelope: ContextMessage): ContextIngestResult | undefined {
const sourceKey = getSourceKey(envelope)
if (!currentActiveContexts[sourceKey]) {
currentActiveContexts[sourceKey] = []
}
const safeEnvelopeToStore = structuredClone(envelope)
if (!currentActiveContexts.has(sourceKey)) {
currentActiveContexts.set(sourceKey, [])
}
let result: ContextIngestResult | undefined
if (envelope.strategy === CONTEXT_UPDATE_REPLACE_SELF) {
currentActiveContexts[sourceKey] = [safeEnvelopeToStore]
currentActiveContexts.set(sourceKey, [safeEnvelopeToStore])
result = {
sourceKey,
mutation: 'replace',
entryCount: currentActiveContexts.get(sourceKey)?.length ?? 0,
}
}
else if (envelope.strategy === CONTEXT_UPDATE_APPEND_SELF) {
currentActiveContexts[sourceKey].push(safeEnvelopeToStore)
currentActiveContexts.get(sourceKey)?.push(safeEnvelopeToStore)
result = {
sourceKey,
mutation: 'append',
entryCount: currentActiveContexts.get(sourceKey)?.length ?? 0,
}
}
currentContextHistory = [
@@ -74,22 +134,29 @@ export function createContextRegistry(options: CreateContextRegistryOptions = {}
sourceKey,
},
].slice(-historyLimit)
return result
}
function reset() {
currentActiveContexts = {}
currentActiveContexts = new Map<string, ContextMessage[]>()
currentContextHistory = []
}
function snapshot() {
return structuredClone(currentActiveContexts)
return Object.fromEntries(
Array.from(currentActiveContexts, ([sourceKey, messages]) => [
sourceKey,
structuredClone(messages),
]),
)
}
return {
ingest,
reset,
snapshot,
activeContexts: () => structuredClone(currentActiveContexts),
contextHistory: () => [...currentContextHistory],
activeContexts: snapshot,
contextHistory: () => structuredClone(currentContextHistory),
}
}
@@ -0,0 +1,227 @@
import type { ContextMessage } from '../../types/chat'
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { isReadonly, reactive } from 'vue'
import { useChatContextStore } from './context-store'
type TestContextMessage = ContextMessage & { source?: string }
function createMetadata(pluginId: string, instanceId: string): NonNullable<ContextMessage['metadata']> {
return {
source: {
id: instanceId,
kind: 'plugin',
plugin: {
id: pluginId,
},
},
}
}
function createContextMessage(overrides: Partial<TestContextMessage> = {}): TestContextMessage {
const id = overrides.id ?? 'context-1'
return {
id,
contextId: overrides.contextId ?? id,
strategy: overrides.strategy ?? ContextUpdateStrategy.ReplaceSelf,
text: overrides.text ?? 'context text',
createdAt: overrides.createdAt ?? 1,
...overrides,
}
}
/**
* @example
* const store = useChatContextStore()
* store.ingestContextMessage(contextMessage)
*/
describe('useChatContextStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
/**
* @example
* Ingesting append-self updates mirrors activeContexts and contextHistory from core registry.
*/
it('keeps reactive mirrors aligned with the core registry after ingest', () => {
const store = useChatContextStore()
const firstMessage = createContextMessage({
id: 'first',
metadata: createMetadata('weather', 'station-1'),
strategy: ContextUpdateStrategy.AppendSelf,
text: 'sunny',
})
const secondMessage = createContextMessage({
id: 'second',
metadata: createMetadata('weather', 'station-1'),
strategy: ContextUpdateStrategy.AppendSelf,
text: 'windy',
})
const firstResult = store.ingestContextMessage(firstMessage)
const secondResult = store.ingestContextMessage(secondMessage)
expect(firstResult).toEqual({
sourceKey: 'weather:station-1',
mutation: 'append',
entryCount: 1,
})
expect(secondResult).toEqual({
sourceKey: 'weather:station-1',
mutation: 'append',
entryCount: 2,
})
expect(store.activeContexts).toEqual(store.getContextsSnapshot())
expect(store.activeContexts['weather:station-1']?.map(message => message.text)).toEqual(['sunny', 'windy'])
expect(store.contextHistory.map(message => message.sourceKey)).toEqual(['weather:station-1', 'weather:station-1'])
})
/**
* @example
* Vue reactive envelopes are unwrapped before they enter the core registry.
*/
it('unwraps Vue reactive envelopes before ingesting through the core registry', () => {
const store = useChatContextStore()
const reactiveMessage = reactive(createContextMessage({
id: 'reactive-message',
metadata: createMetadata('weather', 'station-1'),
text: 'reactive weather',
}))
const result = store.ingestContextMessage(reactiveMessage)
expect(result).toEqual({
sourceKey: 'weather:station-1',
mutation: 'replace',
entryCount: 1,
})
expect(store.getContextsSnapshot()['weather:station-1']?.[0]?.text).toBe('reactive weather')
})
/**
* @example
* Consumers can read mirrors but cannot mutate the registry source of truth through them.
*/
it('exposes readonly mirrors that do not allow external writes to pollute registry state', () => {
const store = useChatContextStore()
store.ingestContextMessage(createContextMessage({
id: 'stable',
source: 'sensor',
text: 'stable context',
}))
expect(isReadonly(store.activeContexts)).toBe(true)
expect(isReadonly(store.contextHistory)).toBe(true)
Reflect.set(store.activeContexts, 'external', [createContextMessage({
id: 'external',
source: 'external',
text: 'external write',
})])
Reflect.set(store.contextHistory, '0', {
...createContextMessage({
id: 'external-history',
source: 'external',
text: 'external history write',
}),
sourceKey: 'external',
})
expect(store.activeContexts.external).toBeUndefined()
expect(store.contextHistory[0]?.id).toBe('stable')
expect(store.getContextsSnapshot()).toEqual({
sensor: [
expect.objectContaining({
id: 'stable',
text: 'stable context',
}),
],
})
})
/**
* @example
* resetContexts() clears both Pinia mirrors and the backing registry snapshot.
*/
it('clears reactive mirrors and the backing registry when reset', () => {
const store = useChatContextStore()
store.ingestContextMessage(createContextMessage({
source: 'sensor',
text: 'before reset',
}))
store.resetContexts()
expect(store.activeContexts).toEqual({})
expect(store.contextHistory).toEqual([])
expect(store.getContextsSnapshot()).toEqual({})
})
/**
* @example
* getContextBucketsSnapshot() returns entryCount, latestCreatedAt, and cloned messages.
*/
it('preserves context bucket snapshot fields and latestCreatedAt calculation', () => {
const store = useChatContextStore()
store.ingestContextMessage(createContextMessage({
id: 'first',
source: 'sensor',
strategy: ContextUpdateStrategy.AppendSelf,
text: 'early',
createdAt: 10,
}))
store.ingestContextMessage(createContextMessage({
id: 'second',
source: 'sensor',
strategy: ContextUpdateStrategy.AppendSelf,
text: 'late',
createdAt: 30,
}))
const bucket = store.getContextBucketsSnapshot().find(snapshot => snapshot.sourceKey === 'sensor')
expect(bucket).toBeDefined()
if (!bucket)
throw new Error('Expected sensor context bucket to exist')
expect(bucket.sourceKey).toBe('sensor')
expect(bucket.entryCount).toBe(2)
expect(bucket.latestCreatedAt).toBe(30)
expect(bucket.messages.map(message => message.text)).toEqual(['early', 'late'])
})
/**
* @example
* Mutating bucket snapshot messages never mutates the core registry.
*/
it('keeps bucket snapshot message mutation isolated from the core registry', () => {
const store = useChatContextStore()
store.ingestContextMessage(createContextMessage({
source: 'sensor',
text: 'original bucket text',
}))
const bucket = store.getContextBucketsSnapshot().find(snapshot => snapshot.sourceKey === 'sensor')
expect(bucket).toBeDefined()
if (!bucket)
throw new Error('Expected sensor context bucket to exist')
const message = bucket.messages[0]
expect(message).toBeDefined()
if (!message)
throw new Error('Expected sensor context message to exist')
message.text = 'mutated bucket snapshot'
expect(store.getContextsSnapshot().sensor?.[0]?.text).toBe('original bucket text')
})
})
@@ -1,102 +1,61 @@
import type { ContextMessage } from '../../types/chat'
import type { ContextHistoryEntry, ContextIngestResult, ContextMessage } from '@proj-airi/core-agent'
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
import { createContextRegistry } from '@proj-airi/core-agent'
import { defineStore } from 'pinia'
import { ref, toRaw } from 'vue'
import { readonly, ref, toRaw } from 'vue'
import { getEventSourceKey } from '../../utils/event-source'
export type { ContextHistoryEntry, ContextIngestResult } from '@proj-airi/core-agent'
/**
* UI-facing view of one active context source bucket.
*/
export interface ContextBucketSnapshot {
/** Stable registry source bucket key. */
sourceKey: string
/** Number of active messages currently stored for this bucket. */
entryCount: number
/** Latest `createdAt` timestamp across messages in this bucket. */
latestCreatedAt?: number
/** Cloned context messages for devtools and UI consumers. */
messages: ContextMessage[]
}
export interface ContextIngestResult {
sourceKey: string
mutation: 'replace' | 'append'
entryCount: number
}
export interface ContextHistoryEntry extends ContextMessage {
sourceKey: string
}
const CONTEXT_HISTORY_LIMIT = 400
export const useChatContextStore = defineStore('chat-context', () => {
const activeContexts = ref<Record<string, ContextMessage[]>>({})
const contextHistory = ref<ContextHistoryEntry[]>([])
const registry = createContextRegistry({
historyLimit: CONTEXT_HISTORY_LIMIT,
getSourceKey: getEventSourceKey,
})
const activeContextsMirror = ref<Record<string, ContextMessage[]>>({})
const contextHistoryMirror = ref<ContextHistoryEntry[]>([])
const activeContexts = readonly(activeContextsMirror)
const contextHistory = readonly(contextHistoryMirror)
function cloneMessage(message: ContextMessage): ContextMessage {
const rawMessage = toRaw(message)
try {
return structuredClone(rawMessage)
}
catch {
return JSON.parse(JSON.stringify(rawMessage)) as ContextMessage
}
function syncRegistrySnapshot() {
activeContextsMirror.value = registry.activeContexts()
contextHistoryMirror.value = registry.contextHistory()
}
function cloneSnapshot() {
return Object.fromEntries(
Object.entries(activeContexts.value).map(([sourceKey, messages]) => [
sourceKey,
messages.map(cloneMessage),
]),
)
}
function ingestContextMessage(envelope: ContextMessage) {
const normalizedEnvelope = cloneMessage(envelope)
const sourceKey = getEventSourceKey(normalizedEnvelope)
let result: ContextIngestResult | undefined
if (!activeContexts.value[sourceKey]) {
activeContexts.value[sourceKey] = []
}
if (normalizedEnvelope.strategy === ContextUpdateStrategy.ReplaceSelf) {
activeContexts.value[sourceKey] = [normalizedEnvelope]
result = {
sourceKey,
mutation: 'replace',
entryCount: activeContexts.value[sourceKey].length,
} satisfies ContextIngestResult
}
else if (normalizedEnvelope.strategy === ContextUpdateStrategy.AppendSelf) {
activeContexts.value[sourceKey].push(normalizedEnvelope)
result = {
sourceKey,
mutation: 'append',
entryCount: activeContexts.value[sourceKey].length,
} satisfies ContextIngestResult
}
contextHistory.value = [
...contextHistory.value,
{
...normalizedEnvelope,
sourceKey,
},
].slice(-CONTEXT_HISTORY_LIMIT)
function ingestContextMessage(envelope: ContextMessage): ContextIngestResult | undefined {
const result = registry.ingest(toRaw(envelope))
syncRegistrySnapshot()
return result
}
function resetContexts() {
activeContexts.value = {}
contextHistory.value = []
registry.reset()
syncRegistrySnapshot()
}
function getContextsSnapshot() {
return cloneSnapshot()
return registry.snapshot()
}
function getContextBucketsSnapshot() {
return Object.entries(activeContexts.value).map(([sourceKey, messages]) => ({
return Object.entries(registry.activeContexts()).map(([sourceKey, messages]) => ({
sourceKey,
entryCount: messages.length,
latestCreatedAt: messages.reduce<number | undefined>((latest, message) => {
@@ -104,7 +63,7 @@ export const useChatContextStore = defineStore('chat-context', () => {
return message.createdAt
return Math.max(latest, message.createdAt)
}, undefined),
messages: messages.map(cloneMessage),
messages,
} satisfies ContextBucketSnapshot))
}
@@ -15,6 +15,7 @@ export type ContextLifecyclePhase
| 'broadcast-posted'
| 'broadcast-received'
| 'store-ingested'
| 'store-ingest-rejected'
| 'before-compose'
| 'prompt-context-built'
| 'after-compose'
@@ -1,12 +1,16 @@
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
import { createPinia, setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { CHAT_STREAM_CHANNEL_NAME } from '../../chat/constants'
import { CHAT_STREAM_CHANNEL_NAME, CONTEXT_CHANNEL_NAME } from '../../chat/constants'
type HookCallback = (...args: any[]) => Promise<void> | void
type HookCallback = (...args: unknown[]) => Promise<void> | void
type UseContextBridgeStore = typeof import('./context-bridge')['useContextBridgeStore']
const contextUpdateHooks: HookCallback[] = []
const serverEventHooks = new Map<string, HookCallback[]>()
const chatContextIngestMock = vi.fn()
const beginStreamMock = vi.fn()
const appendStreamLiteralMock = vi.fn()
@@ -15,9 +19,10 @@ const resetStreamMock = vi.fn()
const serverSendMock = vi.fn()
const ensureConnectedMock = vi.fn().mockResolvedValue(undefined)
const onReconnectedMock = vi.fn(() => () => {})
const onContextUpdateMock = vi.fn(() => () => {})
const onEventMock = vi.fn(() => () => {})
const onContextUpdateMock = vi.fn((callback: HookCallback) => registerHook(contextUpdateHooks, callback))
const onEventMock = vi.fn((eventName: string, callback: HookCallback) => registerServerEventHook(eventName, callback))
const getProviderInstanceMock = vi.fn()
const recordLifecycleMock = vi.fn()
const activeProviderRef = ref<string | null>(null)
const activeModelRef = ref<string | null>(null)
@@ -47,6 +52,12 @@ function registerHook(target: HookCallback[], callback: HookCallback) {
}
}
function registerServerEventHook(eventName: string, callback: HookCallback) {
const hooks = serverEventHooks.get(eventName) ?? []
serverEventHooks.set(eventName, hooks)
return registerHook(hooks, callback)
}
function createTestChannel(name: string) {
const channel = new BroadcastChannel(name)
testChannels.push(channel)
@@ -73,12 +84,62 @@ async function waitForBroadcastDelivery() {
await new Promise(resolve => setTimeout(resolve, 50))
}
async function emitHooks(target: HookCallback[], ...args: any[]) {
async function emitHooks(target: HookCallback[], ...args: unknown[]) {
for (const callback of target) {
await callback(...args)
}
}
async function emitContextUpdate(event: unknown) {
await emitHooks(contextUpdateHooks, event)
}
async function emitServerEvent(eventName: string, event: unknown) {
await emitHooks(serverEventHooks.get(eventName) ?? [], event)
}
function createMetadata(pluginId: string, instanceId: string) {
return {
source: {
id: instanceId,
kind: 'plugin',
plugin: {
id: pluginId,
},
},
}
}
function createContextMessage(overrides: Record<string, unknown> = {}) {
const id = typeof overrides.id === 'string' ? overrides.id : 'context-1'
return {
id,
contextId: typeof overrides.contextId === 'string' ? overrides.contextId : id,
strategy: ContextUpdateStrategy.AppendSelf,
text: 'context text',
createdAt: 1,
...overrides,
}
}
function createContextUpdateEvent(overrides: Record<string, unknown> = {}) {
const id = typeof overrides.id === 'string' ? overrides.id : 'context-1'
return {
type: 'context:update',
source: 'plugin-module-host',
metadata: createMetadata('weather', 'station-1'),
data: {
id,
contextId: id,
strategy: ContextUpdateStrategy.AppendSelf,
text: 'weather changed',
...overrides,
},
}
}
const chatOrchestratorMock = {
sending: false,
ingest: vi.fn(),
@@ -94,21 +155,21 @@ const chatOrchestratorMock = {
onAssistantMessage: (callback: HookCallback) => registerHook(assistantMessageHooks, callback),
onChatTurnComplete: (callback: HookCallback) => registerHook(turnCompleteHooks, callback),
emitBeforeMessageComposedHooks: (...args: any[]) => emitHooks(beforeComposeHooks, ...args),
emitAfterMessageComposedHooks: (...args: any[]) => emitHooks(afterComposeHooks, ...args),
emitBeforeSendHooks: (...args: any[]) => emitHooks(beforeSendHooks, ...args),
emitAfterSendHooks: (...args: any[]) => emitHooks(afterSendHooks, ...args),
emitTokenLiteralHooks: (...args: any[]) => emitHooks(tokenLiteralHooks, ...args),
emitTokenSpecialHooks: (...args: any[]) => emitHooks(tokenSpecialHooks, ...args),
emitStreamEndHooks: (...args: any[]) => emitHooks(streamEndHooks, ...args),
emitAssistantResponseEndHooks: (...args: any[]) => emitHooks(assistantEndHooks, ...args),
emitBeforeMessageComposedHooks: (...args: unknown[]) => emitHooks(beforeComposeHooks, ...args),
emitAfterMessageComposedHooks: (...args: unknown[]) => emitHooks(afterComposeHooks, ...args),
emitBeforeSendHooks: (...args: unknown[]) => emitHooks(beforeSendHooks, ...args),
emitAfterSendHooks: (...args: unknown[]) => emitHooks(afterSendHooks, ...args),
emitTokenLiteralHooks: (...args: unknown[]) => emitHooks(tokenLiteralHooks, ...args),
emitTokenSpecialHooks: (...args: unknown[]) => emitHooks(tokenSpecialHooks, ...args),
emitStreamEndHooks: (...args: unknown[]) => emitHooks(streamEndHooks, ...args),
emitAssistantResponseEndHooks: (...args: unknown[]) => emitHooks(assistantEndHooks, ...args),
}
vi.mock('pinia', async () => {
const actual = await vi.importActual<typeof import('pinia')>('pinia')
return {
...actual,
storeToRefs: (store: any) => store,
storeToRefs: (store: unknown) => store,
}
})
@@ -170,7 +231,7 @@ vi.mock('../../chat/stream-store', () => ({
vi.mock('../../devtools/context-observability', () => ({
useContextObservabilityStore: () => ({
recordLifecycle: vi.fn(),
recordLifecycle: recordLifecycleMock,
}),
}))
@@ -220,6 +281,7 @@ describe('context bridge contract', () => {
onContextUpdateMock.mockClear()
onEventMock.mockClear()
getProviderInstanceMock.mockReset()
recordLifecycleMock.mockReset()
chatOrchestratorMock.ingest.mockReset()
activeProviderRef.value = null
@@ -238,12 +300,251 @@ describe('context bridge contract', () => {
assistantEndHooks.length = 0
assistantMessageHooks.length = 0
turnCompleteHooks.length = 0
contextUpdateHooks.length = 0
serverEventHooks.clear()
})
afterEach(() => {
closeTestChannels()
})
/**
* @example
* Broadcast context updates record store-ingested with core result fields.
*/
it('records core ingest result for broadcast context updates', async () => {
chatContextIngestMock.mockReturnValueOnce({
sourceKey: 'weather:station-1',
mutation: 'append',
entryCount: 2,
})
const store = useContextBridgeStore()
await store.initialize()
const contextSender = createTestChannel(CONTEXT_CHANNEL_NAME)
contextSender.postMessage(createContextMessage({
id: 'broadcast-context',
metadata: createMetadata('weather', 'station-1'),
text: 'broadcast weather',
}))
await vi.waitFor(() => {
expect(chatContextIngestMock).toHaveBeenCalledTimes(1)
})
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
phase: 'store-ingested',
channel: 'broadcast',
sourceKey: 'weather:station-1',
mutation: 'append',
details: expect.objectContaining({
entryCount: 2,
}),
}))
await store.dispose()
})
/**
* @example
* Server context updates record store-ingested before broadcast-posted.
*/
it('records core ingest result for server context updates before broadcasting', async () => {
chatContextIngestMock.mockReturnValueOnce({
sourceKey: 'weather:station-1',
mutation: 'replace',
entryCount: 1,
})
const store = useContextBridgeStore()
await store.initialize()
await emitContextUpdate(createContextUpdateEvent({
id: 'server-context',
strategy: ContextUpdateStrategy.ReplaceSelf,
text: 'server weather',
}))
expect(chatContextIngestMock).toHaveBeenCalledTimes(1)
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
phase: 'store-ingested',
channel: 'server',
sourceKey: 'weather:station-1',
mutation: 'replace',
details: expect.objectContaining({
entryCount: 1,
}),
}))
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
phase: 'broadcast-posted',
channel: 'broadcast',
contextId: 'server-context',
}))
await store.dispose()
})
/**
* @example
* Input context updates record store-ingested and stay in chat input payload.
*/
it('records core ingest result for input context updates and forwards accepted updates', async () => {
chatContextIngestMock.mockReturnValueOnce({
sourceKey: 'weather:station-1',
mutation: 'append',
entryCount: 1,
})
activeProviderRef.value = 'mock-provider'
activeModelRef.value = 'mock-model'
getProviderInstanceMock.mockResolvedValueOnce({})
const store = useContextBridgeStore()
await store.initialize()
await emitServerEvent('input:text', {
type: 'input:text',
source: 'plugin-module-host',
metadata: createMetadata('weather', 'station-1'),
data: {
text: 'hello',
contextUpdates: [
{
strategy: ContextUpdateStrategy.AppendSelf,
text: 'input weather',
},
],
},
})
expect(chatContextIngestMock).toHaveBeenCalledTimes(1)
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
phase: 'store-ingested',
channel: 'input',
sourceKey: 'weather:station-1',
mutation: 'append',
details: expect.objectContaining({
entryCount: 1,
inputType: 'input:text',
}),
}))
expect(chatOrchestratorMock.ingest).toHaveBeenCalledTimes(1)
expect(chatOrchestratorMock.ingest.mock.calls[0]?.[1]?.input?.data.contextUpdates).toEqual([
expect.objectContaining({
contextId: expect.any(String),
id: expect.any(String),
text: 'input weather',
}),
])
await store.dispose()
})
/**
* @example
* Broadcast context ingest failures record store-ingest-rejected instead of escaping.
*/
it('records rejected lifecycle for broadcast ingest failures without interrupting the watcher', async () => {
chatContextIngestMock.mockImplementationOnce(() => {
throw new Error('Cannot clone broadcast context')
})
const store = useContextBridgeStore()
await store.initialize()
const contextSender = createTestChannel(CONTEXT_CHANNEL_NAME)
contextSender.postMessage(createContextMessage({
id: 'bad-broadcast-context',
metadata: createMetadata('weather', 'station-1'),
text: 'bad broadcast weather',
}))
await vi.waitFor(() => {
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
phase: 'store-ingest-rejected',
channel: 'broadcast',
contextId: 'bad-broadcast-context',
details: expect.objectContaining({
errorMessage: 'Cannot clone broadcast context',
}),
}))
})
await store.dispose()
})
/**
* @example
* Server context ingest failures are not rebroadcast.
*/
it('records rejected lifecycle and skips broadcast when server context ingest fails', async () => {
chatContextIngestMock.mockImplementationOnce(() => {
throw new Error('Cannot clone server context')
})
const postedContexts = collectChannelMessages(CONTEXT_CHANNEL_NAME)
const store = useContextBridgeStore()
await store.initialize()
await emitContextUpdate(createContextUpdateEvent({
id: 'bad-server-context',
text: 'bad server weather',
}))
await waitForBroadcastDelivery()
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
phase: 'store-ingest-rejected',
channel: 'server',
contextId: 'bad-server-context',
details: expect.objectContaining({
errorMessage: 'Cannot clone server context',
}),
}))
expect(recordLifecycleMock).not.toHaveBeenCalledWith(expect.objectContaining({
phase: 'broadcast-posted',
contextId: 'bad-server-context',
}))
expect(postedContexts).toHaveLength(0)
await store.dispose()
})
/**
* @example
* Input context ingest failures drop only the failed context update.
*/
it('records rejected lifecycle and continues text ingestion when input context ingest fails', async () => {
chatContextIngestMock.mockImplementationOnce(() => {
throw new Error('Cannot clone input context')
})
activeProviderRef.value = 'mock-provider'
activeModelRef.value = 'mock-model'
getProviderInstanceMock.mockResolvedValueOnce({})
const store = useContextBridgeStore()
await store.initialize()
await emitServerEvent('input:text', {
type: 'input:text',
source: 'plugin-module-host',
metadata: createMetadata('weather', 'station-1'),
data: {
text: 'hello',
contextUpdates: [
{
strategy: ContextUpdateStrategy.AppendSelf,
text: 'bad input weather',
},
],
},
})
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
phase: 'store-ingest-rejected',
channel: 'input',
details: expect.objectContaining({
errorMessage: 'Cannot clone input context',
}),
}))
expect(chatOrchestratorMock.ingest).toHaveBeenCalledTimes(1)
expect(chatOrchestratorMock.ingest.mock.calls[0]?.[1]?.input?.data.contextUpdates).toEqual([])
await store.dispose()
})
it('replays remote stream lifecycle into sending and stream store APIs', async () => {
const store = useContextBridgeStore()
await store.initialize()
@@ -5,6 +5,7 @@ import type { UserMessage } from '@xsai/shared-chat'
import type { ChatStreamEvent, ChatStreamEventContext, ContextMessage } from '../../../types/chat'
import type { SparkNotifyReactionOptions } from './spark-notify-reaction'
import { errorMessageFrom } from '@moeru/std'
import { isStageTamagotchi, isStageWeb } from '@proj-airi/stage-shared'
import { useBroadcastChannel } from '@vueuse/core'
import { Mutex } from 'es-toolkit'
@@ -85,6 +86,53 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
let remoteStreamGuard: { sessionId: string, generation: number } | null = null
let initialized = false
function recordContextIngestRejected(options: {
channel: 'server' | 'broadcast' | 'input'
contextMessage: ContextMessage
details?: unknown
error: unknown
sourceLabel?: string
}) {
contextObservability.recordLifecycle({
phase: 'store-ingest-rejected',
channel: options.channel,
sourceKey: getEventSourceKey(options.contextMessage),
strategy: options.contextMessage.strategy,
lane: options.contextMessage.lane,
contextId: options.contextMessage.contextId,
eventId: options.contextMessage.id,
textPreview: options.contextMessage.text,
sourceLabel: options.sourceLabel,
details: {
errorMessage: errorMessageFrom(options.error) ?? 'Unknown context ingest error',
event: options.details,
},
})
}
function ingestContextMessageSafely(options: {
channel: 'server' | 'broadcast' | 'input'
contextMessage: ContextMessage
details?: unknown
sourceLabel?: string
}) {
try {
return {
ok: true as const,
result: chatContext.ingestContextMessage(options.contextMessage),
}
}
catch (error) {
recordContextIngestRejected({
...options,
error,
})
return {
ok: false as const,
}
}
}
async function handleSparkNotifyReactionLocal(options: SparkNotifyReactionOptions) {
const event: WebSocketEventOf<'spark:notify'> = {
type: 'spark:notify',
@@ -196,21 +244,26 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id,
details: event,
})
const result = chatContext.ingestContextMessage(event)
if (result) {
const ingestAttempt = ingestContextMessageSafely({
channel: 'broadcast',
contextMessage: event,
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id,
details: event,
})
if (ingestAttempt.ok && ingestAttempt.result) {
contextObservability.recordLifecycle({
phase: 'store-ingested',
channel: 'broadcast',
sourceKey: result.sourceKey,
sourceKey: ingestAttempt.result.sourceKey,
strategy: event.strategy,
lane: event.lane,
contextId: event.contextId,
eventId: event.id,
mutation: result.mutation,
mutation: ingestAttempt.result.mutation,
textPreview: event.text,
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id,
details: {
entryCount: result.entryCount,
entryCount: ingestAttempt.result.entryCount,
event,
},
})
@@ -272,21 +325,29 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
metadata: event.metadata,
createdAt: Date.now(),
}
const result = chatContext.ingestContextMessage(contextMessage)
if (result) {
const ingestAttempt = ingestContextMessageSafely({
channel: 'server',
contextMessage,
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id ?? event.source,
details: event,
})
if (!ingestAttempt.ok)
return
if (ingestAttempt.result) {
contextObservability.recordLifecycle({
phase: 'store-ingested',
channel: 'server',
sourceKey: result.sourceKey,
sourceKey: ingestAttempt.result.sourceKey,
strategy: contextMessage.strategy,
lane: contextMessage.lane,
contextId: contextMessage.contextId,
eventId: contextMessage.id,
mutation: result.mutation,
mutation: ingestAttempt.result.mutation,
textPreview: contextMessage.text,
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id ?? event.source,
details: {
entryCount: result.entryCount,
entryCount: ingestAttempt.result.entryCount,
event,
},
})
@@ -330,6 +391,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
contextId,
}
})
const acceptedContextUpdates: typeof normalizedContextUpdates = normalizedContextUpdates ? [] : undefined
if (normalizedContextUpdates?.length) {
const createdAt = Date.now()
@@ -348,26 +410,39 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
update,
},
})
const contextMessage = {
const contextMessage: ContextMessage = {
...update,
metadata: event.metadata,
createdAt,
}
const result = chatContext.ingestContextMessage(contextMessage)
if (result) {
const ingestAttempt = ingestContextMessageSafely({
channel: 'input',
contextMessage,
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id ?? event.source,
details: {
inputType: event.type,
update: contextMessage,
},
})
if (!ingestAttempt.ok)
continue
acceptedContextUpdates?.push(update)
if (ingestAttempt.result) {
contextObservability.recordLifecycle({
phase: 'store-ingested',
channel: 'input',
sourceKey: result.sourceKey,
sourceKey: ingestAttempt.result.sourceKey,
strategy: contextMessage.strategy,
lane: contextMessage.lane,
contextId: contextMessage.contextId,
eventId: contextMessage.id,
mutation: result.mutation,
mutation: ingestAttempt.result.mutation,
textPreview: contextMessage.text,
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id ?? event.source,
details: {
entryCount: result.entryCount,
entryCount: ingestAttempt.result.entryCount,
inputType: event.type,
update: contextMessage,
},
@@ -427,7 +502,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
text,
textRaw,
overrides,
contextUpdates: normalizedContextUpdates,
contextUpdates: acceptedContextUpdates,
},
},
}, targetSessionId)
@@ -500,7 +575,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
'gen-ai:chat': {
message: context.message as UserMessage,
composedMessage: context.composedMessage,
contexts: context.contexts as any,
contexts: context.contexts,
input: context.input,
},
},
@@ -527,7 +602,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
'gen-ai:chat': {
message: context.message as UserMessage,
composedMessage: context.composedMessage,
contexts: context.contexts as any,
contexts: context.contexts,
input: context.input,
},
},
+238 -26
View File
@@ -1453,7 +1453,7 @@ importers:
version: 3.0.2(electron@41.2.1)
'@electron-toolkit/tsconfig':
specifier: ^2.0.0
version: 2.0.0(@types/node@25.6.0)
version: 2.0.0(@types/node@24.12.2)
'@electron-toolkit/utils':
specifier: ^4.0.0
version: 4.0.0(electron@41.2.1)
@@ -1492,7 +1492,7 @@ importers:
version: 3.1.0
'@intlify/unplugin-vue-i18n':
specifier: ^11.0.7
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)
@@ -1528,10 +1528,10 @@ importers:
version: link:../../packages/ui-transitions
'@proj-airi/unplugin-fetch':
specifier: 'catalog:'
version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@proj-airi/unplugin-live2d-sdk':
specifier: ^0.1.7
version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
'@types/audioworklet':
specifier: 'catalog:'
version: 0.0.97
@@ -1558,7 +1558,7 @@ importers:
version: 2.10.3
'@vitejs/plugin-vue':
specifier: ^6.0.6
version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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: ^3.1.2
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))
@@ -1591,7 +1591,7 @@ importers:
version: 6.8.3
electron-vite:
specifier: ^5.0.0
version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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
@@ -1612,31 +1612,31 @@ importers:
version: 2.2.6
unocss-preset-scrollbar:
specifier: ^4.0.0
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
unplugin-info:
specifier: ^1.3.2
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.6.1)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unplugin-yaml:
specifier: ^4.1.0
version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(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.6.1)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-bundle-visualizer:
specifier: ^1.2.1
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@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-plugin-vue-devtools:
specifier: ^8.1.1
version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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: ^0.11.0
version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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: ^3.1.2
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.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.6.1)(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.9.2)(@emnapi/runtime@1.9.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.6.1)(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: ^3.2.6
version: 3.2.6(typescript@5.9.3)
@@ -19916,9 +19916,9 @@ snapshots:
dependencies:
electron: 41.2.1
'@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)':
'@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)':
dependencies:
'@types/node': 25.6.0
'@types/node': 24.12.2
'@electron-toolkit/utils@4.0.0(electron@41.2.1)':
dependencies:
@@ -20812,6 +20812,31 @@ snapshots:
- supports-color
- typescript
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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.6.1))
'@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.58.1
'@typescript-eslint/typescript-estree': 8.58.1(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.6.1)(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.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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.6.1))
@@ -22790,11 +22815,35 @@ snapshots:
transitivePeerDependencies:
- magicast
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
dependencies:
ofetch: 1.5.1
@@ -23744,6 +23793,7 @@ snapshots:
'@types/node@25.6.0':
dependencies:
undici-types: 7.19.2
optional: true
'@types/nprogress@0.2.3': {}
@@ -24409,6 +24459,12 @@ snapshots:
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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.6.1)(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.6.1)(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
@@ -24489,9 +24545,9 @@ snapshots:
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
optionalDependencies:
'@vitest/browser': 4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
'@vitest/browser': 4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
'@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4)':
dependencies:
@@ -24711,6 +24767,15 @@ 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.6.1)(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.6.1)(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.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
sirv: 3.0.2
@@ -26852,7 +26917,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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)
@@ -26860,7 +26925,7 @@ snapshots:
esbuild: 0.25.12
magic-string: 0.30.21
picocolors: 1.1.1
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
@@ -32787,7 +32852,8 @@ snapshots:
undici-types@7.16.0: {}
undici-types@7.19.2: {}
undici-types@7.19.2:
optional: true
undici@6.24.1: {}
@@ -32899,11 +32965,6 @@ snapshots:
'@unocss/preset-mini': 66.6.8
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@unocss/cli': 66.6.8
@@ -32988,6 +33049,14 @@ snapshots:
unplugin: 2.3.11
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(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.6.1)(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.6.1)(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.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
esbuild: 0.27.2
@@ -33022,6 +33091,19 @@ 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.6.1)(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.6.1)(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.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ci-info: 4.4.0
@@ -33131,6 +33213,17 @@ snapshots:
rollup: 4.60.1
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(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.6.1)(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.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
unplugin@2.3.11:
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -33362,12 +33455,22 @@ 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.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
@@ -33413,6 +33516,21 @@ snapshots:
- tsx
- yaml
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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.6.1)(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.6.1)(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.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ansis: 4.2.0
@@ -33444,6 +33562,13 @@ snapshots:
- typescript
- ws
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(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.6.1)(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.6.1)(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)
@@ -33462,6 +33587,20 @@ 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.6.1)(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.6.1)(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.6.1)(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.6.1)(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.6.1)(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))
@@ -33476,6 +33615,21 @@ 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.6.1)(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.6.1)(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.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@babel/core': 7.29.0
@@ -33491,6 +33645,16 @@ 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.6.1)(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.6.1)(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.6.1)(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)
@@ -33757,6 +33921,54 @@ snapshots:
- vue-tsc
- webpack
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.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.6.1)(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.9.2)(@emnapi/runtime@1.9.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.9.2)(@emnapi/runtime@1.9.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.6.1)(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.6.1)(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.9.2)(@emnapi/runtime@1.9.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.6.1)(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.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))