diff --git a/apps/stage-pocket/src/pages/settings/system/developer.vue b/apps/stage-pocket/src/pages/settings/system/developer.vue index 44e76f0a4..9aaff3cf4 100644 --- a/apps/stage-pocket/src/pages/settings/system/developer.vue +++ b/apps/stage-pocket/src/pages/settings/system/developer.vue @@ -80,6 +80,12 @@ const menu = computed(() => [ icon: 'i-solar:sledgehammer-bold-duotone', to: '/devtools/notifications', }, + { + title: 'WebSocket Inspector', + description: 'Inspect raw WebSocket traffic', + icon: 'i-solar:transfer-horizontal-bold-duotone', + to: '/devtools/websocket-inspector', + }, ]) diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue index 207ad0f2f..a285a3641 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue @@ -59,6 +59,12 @@ const menu = computed(() => [ icon: 'i-solar:chart-bold-duotone', to: '/devtools/beat-sync', }, + { + title: 'WebSocket Inspector', + description: 'Inspect raw WebSocket traffic', + icon: 'i-solar:transfer-horizontal-bold-duotone', + to: '/devtools/websocket-inspector', + }, ]) const openDevTools = useElectronEventaInvoke(electronOpenMainDevtools) diff --git a/apps/stage-web/src/pages/settings/system/developer.vue b/apps/stage-web/src/pages/settings/system/developer.vue index 911e7c6e2..9de17ad06 100644 --- a/apps/stage-web/src/pages/settings/system/developer.vue +++ b/apps/stage-web/src/pages/settings/system/developer.vue @@ -68,6 +68,12 @@ const menu = computed(() => [ icon: 'i-solar:chat-square-call-bold-duotone', to: '/devtools/context-flow', }, + { + title: 'WebSocket Inspector', + description: 'Inspect raw WebSocket traffic', + icon: 'i-solar:transfer-horizontal-bold-duotone', + to: '/devtools/websocket-inspector', + }, { title: t('settings.pages.system.sections.section.developer.sections.section.use-magic-keys.title'), description: t('settings.pages.system.sections.section.developer.sections.section.use-magic-keys.description'), diff --git a/packages/server-sdk/src/client.ts b/packages/server-sdk/src/client.ts index 336f2b21a..de3a8a9f1 100644 --- a/packages/server-sdk/src/client.ts +++ b/packages/server-sdk/src/client.ts @@ -30,6 +30,8 @@ export interface ClientOptions { autoConnect?: boolean autoReconnect?: boolean maxReconnectAttempts?: number + onAnyMessage?: (data: WebSocketEvent) => void + onAnySend?: (data: WebSocketEvent) => void } function createInstanceId() { @@ -60,6 +62,8 @@ export class Client { this.opts = { url: 'ws://localhost:6121/ws', + onAnyMessage: () => {}, + onAnySend: () => {}, possibleEvents: [], onError: () => {}, onClose: () => {}, @@ -246,6 +250,7 @@ export class Client { private async handleMessage(event: MessageEvent) { try { const data = JSON.parse(event.data as string) as WebSocketEvent + this.opts.onAnyMessage?.(data) const listeners = this.eventListeners.get(data.type) if (!listeners?.size) { return @@ -299,11 +304,15 @@ export class Client { send(data: WebSocketEventOptionalSource): void { if (this.websocket && this.connected) { - this.websocket.send(JSON.stringify({ + const payload = { source: this.opts.name as WebSocketEventSource | string, metadata: { source: this.identity }, ...data, - } as WebSocketEvent)) + } as WebSocketEvent + + this.opts.onAnySend?.(payload) + + this.websocket.send(JSON.stringify(payload)) } } diff --git a/packages/stage-layouts/src/layouts/settings.vue b/packages/stage-layouts/src/layouts/settings.vue index d3edb6903..ff04b3433 100644 --- a/packages/stage-layouts/src/layouts/settings.vue +++ b/packages/stage-layouts/src/layouts/settings.vue @@ -114,6 +114,10 @@ const routeHeaderMetadataMap = computed(() => { subtitle: t('tamagotchi.settings.devtools.title'), title: t('tamagotchi.settings.devtools.pages.context-flow.title'), }, + '/devtools/websocket-inspector': { + subtitle: t('tamagotchi.settings.devtools.title'), + title: 'WebSocket Inspector', + }, '/devtools/performance-visualizer': { subtitle: t('settings.title'), title: t('settings.pages.system.sections.section.developer.sections.section.performance-visualizer.title'), diff --git a/packages/stage-pages/src/pages/devtools/websocket-inspector.vue b/packages/stage-pages/src/pages/devtools/websocket-inspector.vue new file mode 100644 index 000000000..078ca77e5 --- /dev/null +++ b/packages/stage-pages/src/pages/devtools/websocket-inspector.vue @@ -0,0 +1,191 @@ + + + + + +meta: + layout: settings + diff --git a/packages/stage-ui/src/stores/devtools/websocket-inspector.ts b/packages/stage-ui/src/stores/devtools/websocket-inspector.ts new file mode 100644 index 000000000..fb96d1f3f --- /dev/null +++ b/packages/stage-ui/src/stores/devtools/websocket-inspector.ts @@ -0,0 +1,46 @@ +import type { WebSocketEvent } from '@proj-airi/server-sdk' + +import { nanoid } from 'nanoid' +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export interface WebSocketHistoryItem { + id: string + timestamp: number + direction: 'incoming' | 'outgoing' + event: WebSocketEvent +} + +export const useWebSocketInspectorStore = defineStore('devtools:websocket-inspector', () => { + const history = ref([]) + const isEnabled = ref(true) + const maxHistory = ref(1000) + + function add(direction: 'incoming' | 'outgoing', event: WebSocketEvent) { + if (!isEnabled.value) + return + + history.value.unshift({ + id: nanoid(), + timestamp: Date.now(), + direction, + event, + }) + + if (history.value.length > maxHistory.value) { + history.value.pop() + } + } + + function clear() { + history.value = [] + } + + return { + history, + isEnabled, + maxHistory, + add, + clear, + } +}) diff --git a/packages/stage-ui/src/stores/mods/api/channel-server.ts b/packages/stage-ui/src/stores/mods/api/channel-server.ts index dc6693a8a..b65285a09 100644 --- a/packages/stage-ui/src/stores/mods/api/channel-server.ts +++ b/packages/stage-ui/src/stores/mods/api/channel-server.ts @@ -6,6 +6,8 @@ import { nanoid } from 'nanoid' import { defineStore } from 'pinia' import { ref } from 'vue' +import { useWebSocketInspectorStore } from '../../devtools/websocket-inspector' + export const useModsServerChannelStore = defineStore('mods:channels:proj-airi:server', () => { const connected = ref(false) const client = ref() @@ -48,6 +50,12 @@ export const useModsServerChannelStore = defineStore('mods:channels:proj-airi:se url: import.meta.env.VITE_AIRI_WS_URL || 'ws://localhost:6121/ws', token: options?.token, possibleEvents, + onAnyMessage: (event) => { + useWebSocketInspectorStore().add('incoming', event) + }, + onAnySend: (event) => { + useWebSocketInspectorStore().add('outgoing', event) + }, onError: (error) => { connected.value = false initializing.value = null diff --git a/packages/stage-ui/src/stores/mods/api/context-bridge.ts b/packages/stage-ui/src/stores/mods/api/context-bridge.ts index 31d695271..e498f8f0c 100644 --- a/packages/stage-ui/src/stores/mods/api/context-bridge.ts +++ b/packages/stage-ui/src/stores/mods/api/context-bridge.ts @@ -6,7 +6,7 @@ import { isStageTamagotchi, isStageWeb } from '@proj-airi/stage-shared' import { useBroadcastChannel } from '@vueuse/core' import { Mutex } from 'es-toolkit' import { defineStore } from 'pinia' -import { ref, watch, toRaw } from 'vue' +import { ref, toRaw, watch } from 'vue' import { CHAT_STREAM_CHANNEL_NAME, CONTEXT_CHANNEL_NAME, useChatStore } from '../../chat' import { useModsServerChannelStore } from './channel-server' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29c787c80..248693b31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2919,7 +2919,7 @@ importers: version: 66.5.11 '@wxt-dev/module-vue': specifier: ^1.0.3 - version: 1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vue-tsc: specifier: ^3.1.8 version: 3.2.1(typescript@5.9.3) @@ -22215,12 +22215,6 @@ snapshots: vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vue: 3.5.25(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))': - dependencies: - '@rolldown/pluginutils': 1.0.0-beta.53 - vite: 8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vue: 3.5.25(typescript@5.9.3) - '@vitest/browser-playwright@4.0.16(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)': dependencies: '@vitest/browser': 4.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16) @@ -22967,9 +22961,9 @@ snapshots: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.16 - '@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@wxt-dev/module-vue@1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@vitejs/plugin-vue': 6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3)) + '@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3)) wxt: 0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - vite