diff --git a/apps/stage-web/src/pages/devtools/index.vue b/apps/stage-web/src/pages/devtools/index.vue new file mode 100644 index 000000000..0b749bbf7 --- /dev/null +++ b/apps/stage-web/src/pages/devtools/index.vue @@ -0,0 +1,13 @@ + + + + + +meta: + layout: settings + titleKey: tamagotchi.settings.devtools.title + diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index e1e7612f4..c9a563dc4 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -391,16 +391,30 @@ pages: description: Playing Minecraft! title: Minecraft enable: Enable Minecraft Integration - enable-description: Allow AIRI to interact with Minecraft game - server-address: Server Address - server-address-description: The IP address or hostname of the Minecraft server - server-address-placeholder: Enter server address (e.g., localhost or play.example.com) - server-port: Server Port - server-port-description: The port number of the Minecraft server (default 25565) - username: Username - username-description: Your username in the Minecraft server - username-placeholder: Enter your Minecraft username + enable-description: Include Minecraft self-knowledge in Stage chat turns and keep passive runtime visibility enabled. configured: Minecraft is properly configured! + read-only: + title: Read-only shell + description: Stage currently exposes Minecraft as a passive shell. Runtime status and traffic are visible here, but service configuration stays in the Minecraft service files. + setup: + title: Setup + description: You can find the instructions for setting up the bot in the following file + runtime: + title: Observed runtime + connection: Connection + latest-context: Latest bot context + waiting: Waiting for a fresh bot-originated context push. + unknown: Unknown + debug: + title: Minecraft traffic + empty: No Minecraft traffic observed yet. + source: 'Source: {source}' + status: + service-online: Service online + service-offline: Service offline + no-runtime-context: 'No bot context observed yet' + last-context-seconds: 'Last bot context: {seconds}s ago' + last-context-stale: 'Last bot context: Stale' hearing: description: Configure how speech recognition works title: Hearing diff --git a/packages/plugin-protocol/src/types/events.ts b/packages/plugin-protocol/src/types/events.ts index 20e6cf85b..b150981ce 100644 --- a/packages/plugin-protocol/src/types/events.ts +++ b/packages/plugin-protocol/src/types/events.ts @@ -471,7 +471,7 @@ interface InputSource { interface OutputSource { 'gen-ai:chat': { message: UserMessage - contexts: Record, string | CommonContentPart[]>[]> + contexts: Record, unknown>[]> composedMessage: Array input?: InputEventEnvelope } diff --git a/packages/server-runtime/src/bin/run.ts b/packages/server-runtime/src/bin/run.ts index 3cde056ec..1d4d04a6d 100644 --- a/packages/server-runtime/src/bin/run.ts +++ b/packages/server-runtime/src/bin/run.ts @@ -1,6 +1,6 @@ #!/usr/bin/env tsx -import { env, exit, on } from 'node:process' +import process, { env, exit } from 'node:process' import { createServer } from '../server' @@ -18,7 +18,7 @@ async function shutdown() { exit(0) } -on('SIGINT', shutdown) -on('SIGTERM', shutdown) +process.on('SIGINT', shutdown) +process.on('SIGTERM', shutdown) server.start() diff --git a/packages/stage-layouts/src/layouts/settings.vue b/packages/stage-layouts/src/layouts/settings.vue index cbbb18f67..d8264c2f6 100644 --- a/packages/stage-layouts/src/layouts/settings.vue +++ b/packages/stage-layouts/src/layouts/settings.vue @@ -78,14 +78,12 @@ onMounted(() => updateThemeColor()) paddingRight: 'env(safe-area-inset-right, 0px)', paddingLeft: 'env(safe-area-inset-left, 0px)', }" - h-full w-full + :class="['h-full w-full', 'flex flex-col']" > @@ -94,7 +92,7 @@ onMounted(() => updateThemeColor()) :class="[ 'px-3 py-0 2xl:max-w-screen-2xl md:py-0 xl:px-4', isStageTamagotchi() ? 'sm:max-h-[calc(100%)] max-h-[calc(100%)]' : 'sm:max-h-[calc(100%-56px)] max-h-[calc(100%-40px)]', - 'flex flex-col mx-auto h-full min-h-0', + 'mx-auto flex min-h-0 w-full flex-1 flex-col', ]" > updateThemeColor()) :subtitle="routeHeaderMetadata?.subtitle" :disable-back-button="isStageTamagotchi() && route.path === '/settings'" /> -
+
diff --git a/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-active-contexts.vue b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-active-contexts.vue new file mode 100644 index 000000000..e7bc8ec83 --- /dev/null +++ b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-active-contexts.vue @@ -0,0 +1,125 @@ + + + diff --git a/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-lifecycle.vue b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-lifecycle.vue new file mode 100644 index 000000000..15a0d88a1 --- /dev/null +++ b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-lifecycle.vue @@ -0,0 +1,134 @@ + + + diff --git a/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-prompt-projection.vue b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-prompt-projection.vue new file mode 100644 index 000000000..bc4beaba5 --- /dev/null +++ b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-prompt-projection.vue @@ -0,0 +1,122 @@ + + + diff --git a/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-runtime.vue b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-runtime.vue new file mode 100644 index 000000000..2b0430897 --- /dev/null +++ b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-runtime.vue @@ -0,0 +1,107 @@ + + + diff --git a/packages/stage-pages/src/pages/devtools/context-flow/index.vue b/packages/stage-pages/src/pages/devtools/context-flow/index.vue index 7e7ef3928..86c60f118 100644 --- a/packages/stage-pages/src/pages/devtools/context-flow/index.vue +++ b/packages/stage-pages/src/pages/devtools/context-flow/index.vue @@ -6,19 +6,28 @@ import type { FlowDirection, FlowEntry, SparkNotifyEntryState } from './context- import { errorMessageFrom } from '@moeru/std' import { ContextUpdateStrategy } from '@proj-airi/server-sdk' +import { Section } from '@proj-airi/stage-ui/components' import { useCharacterOrchestratorStore, useCharacterStore } from '@proj-airi/stage-ui/stores/character' import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat' import { CHAT_STREAM_CHANNEL_NAME, CONTEXT_CHANNEL_NAME } from '@proj-airi/stage-ui/stores/chat/constants' +import { formatContextPromptText } from '@proj-airi/stage-ui/stores/chat/context-prompt' +import { useChatContextStore } from '@proj-airi/stage-ui/stores/chat/context-store' +import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store' +import { useContextObservabilityStore } from '@proj-airi/stage-ui/stores/devtools/context-observability' import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server' import { getEventSourceKey } from '@proj-airi/stage-ui/utils' -import { Callout } from '@proj-airi/ui' import { useBroadcastChannel } from '@vueuse/core' import { nanoid } from 'nanoid' +import { storeToRefs } from 'pinia' import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { toast } from 'vue-sonner' import ContextFlowActions from './components/context-flow-actions.vue' +import ContextFlowActiveContexts from './components/context-flow-active-contexts.vue' import ContextFlowFilters from './components/context-flow-filters.vue' +import ContextFlowLifecycle from './components/context-flow-lifecycle.vue' +import ContextFlowPromptProjection from './components/context-flow-prompt-projection.vue' +import ContextFlowRuntime from './components/context-flow-runtime.vue' import ContextFlowStream from './components/context-flow-stream.vue' import { useContextFlowFormatters } from './composables/use-context-flow-formatters' @@ -33,9 +42,15 @@ const { } = useContextFlowFormatters() const chatStore = useChatOrchestratorStore() +const chatContextStore = useChatContextStore() +const chatSessionStore = useChatSessionStore() const characterStore = useCharacterStore() const characterOrchestratorStore = useCharacterOrchestratorStore() +const contextObservabilityStore = useContextObservabilityStore() const serverChannelStore = useModsServerChannelStore() +const { activeSessionId } = storeToRefs(chatSessionStore) +const { connected, pendingSendCount } = storeToRefs(serverChannelStore) +const { lastPromptProjection, lastBroadcastPostedAt, lastBroadcastReceivedAt } = storeToRefs(contextObservabilityStore) const entries = ref([]) const showIncoming = ref(true) @@ -64,6 +79,8 @@ const testSparkNotifyPayload = ref(JSON.stringify({ const directionFilter = ref('all') const sparkNotifyStates = ref>(new Map()) +const now = ref(Date.now()) +let nowTimer: ReturnType | null = null const maxEntriesValue = computed(() => { const parsed = Number.parseInt(maxEntries.value, 10) @@ -96,6 +113,10 @@ const filteredEntries = computed(() => { return filtered.slice().reverse() }) +const activeContextBuckets = computed(() => chatContextStore.getContextBucketsSnapshot()) +const currentPromptText = computed(() => formatContextPromptText(chatContextStore.getContextsSnapshot())) +const pendingQueuedSends = computed(() => chatStore.getPendingQueuedSendSnapshot()) + function normalizePayload(payload: unknown) { try { return JSON.parse(JSON.stringify(payload)) as unknown @@ -205,6 +226,7 @@ function pushEntry(entry: Omit) { function clearEntries() { entries.value = [] + contextObservabilityStore.clearHistory() } function sendTestContextUpdate() { @@ -327,6 +349,10 @@ const { data: incomingStreamEvent } = useBroadcastChannel void> = [] onMounted(() => { + nowTimer = setInterval(() => { + now.value = Date.now() + }, 1000) + cleanupFns.push(serverChannelStore.onContextUpdate((event) => { pushEntry({ direction: 'incoming', @@ -537,16 +563,16 @@ watch(maxEntriesValue, () => { onUnmounted(() => { for (const cleanup of cleanupFns) cleanup() + + if (nowTimer) { + clearInterval(nowTimer) + nowTimer = null + } })