Skip to content

Commit 8ee9c26

Browse files
feat(stage-pages): add spark:notify to context flow devtool (#903)
--------- Co-authored-by: Neko <neko@ayaka.moe>
1 parent 82026be commit 8ee9c26

File tree

1 file changed

+112
-0
lines changed

1 file changed

+112
-0
lines changed

packages/stage-pages/src/pages/devtools/context-flow.vue

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
<script setup lang="ts">
2+
import type { WebSocketBaseEvent, WebSocketEvents } from '@proj-airi/server-sdk'
23
import type { ChatStreamEvent, ContextMessage } from '@proj-airi/stage-ui/types/chat'
34
45
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
56
import { Callout, Section } from '@proj-airi/stage-ui/components'
7+
import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character-orchestrator'
68
import { CHAT_STREAM_CHANNEL_NAME, CONTEXT_CHANNEL_NAME, useChatStore } from '@proj-airi/stage-ui/stores/chat'
79
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
810
import { Button, FieldCheckbox, FieldInput, FieldTextArea, Input, SelectTab } from '@proj-airi/ui'
911
import { useBroadcastChannel } from '@vueuse/core'
12+
import { nanoid } from 'nanoid'
1013
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
1114
1215
type FlowDirection = 'incoming' | 'outgoing'
@@ -24,6 +27,7 @@ interface FlowEntry {
2427
}
2528
2629
const chatStore = useChatStore()
30+
const characterOrchestratorStore = useCharacterOrchestratorStore()
2731
const serverChannelStore = useModsServerChannelStore()
2832
2933
const entries = ref<FlowEntry[]>([])
@@ -39,6 +43,17 @@ const maxEntries = ref('200')
3943
const testPayload = ref('{"type":"coding:context","data":{"file":{"path":"README.md"}}}')
4044
const testStrategy = ref<ContextUpdateStrategy>(ContextUpdateStrategy.ReplaceSelf)
4145
46+
const testSparkNotifyPayload = ref(JSON.stringify({
47+
kind: 'ping',
48+
urgency: 'immediate',
49+
headline: 'Devtools spark:notify test',
50+
note: 'Triggered from Context Flow devtools',
51+
destinations: ['character'],
52+
payload: {
53+
message: 'Hello from Context Flow devtools',
54+
},
55+
}, null, 2))
56+
4257
const streamContainer = ref<HTMLDivElement>()
4358
4459
const directionOptions = [
@@ -386,6 +401,90 @@ function sendTestContextUpdate() {
386401
})
387402
}
388403
404+
async function sendTestSparkNotify() {
405+
const raw = testSparkNotifyPayload.value.trim()
406+
if (!raw)
407+
return
408+
409+
let parsed: any
410+
try {
411+
parsed = JSON.parse(raw)
412+
}
413+
catch {
414+
pushEntry({
415+
direction: 'incoming',
416+
channel: 'devtools',
417+
type: 'spark:notify',
418+
summary: 'invalid json',
419+
payload: { raw },
420+
})
421+
return
422+
}
423+
424+
const destinations = Array.isArray(parsed?.destinations) ? parsed.destinations.filter((d: unknown) => typeof d === 'string') : []
425+
if (!parsed?.headline || !destinations.length) {
426+
pushEntry({
427+
direction: 'incoming',
428+
channel: 'devtools',
429+
type: 'spark:notify',
430+
summary: 'missing required fields (headline, destinations[])',
431+
payload: parsed,
432+
})
433+
return
434+
}
435+
436+
// TODO(@nekomeowww): improve server event, support to have zod or valibot schema validation for better cross runtime handling
437+
const notify = {
438+
id: typeof parsed.id === 'string' && parsed.id ? parsed.id : nanoid(),
439+
eventId: typeof parsed.eventId === 'string' && parsed.eventId ? parsed.eventId : nanoid(),
440+
lane: typeof parsed.lane === 'string' ? parsed.lane : undefined,
441+
kind: parsed.kind === 'alarm' || parsed.kind === 'ping' || parsed.kind === 'reminder' ? parsed.kind : 'ping',
442+
urgency: parsed.urgency === 'immediate' || parsed.urgency === 'soon' || parsed.urgency === 'later' ? parsed.urgency : 'immediate',
443+
headline: String(parsed.headline),
444+
note: typeof parsed.note === 'string' ? parsed.note : undefined,
445+
payload: parsed.payload && typeof parsed.payload === 'object' ? parsed.payload : undefined,
446+
ttlMs: typeof parsed.ttlMs === 'number' ? parsed.ttlMs : undefined,
447+
requiresAck: typeof parsed.requiresAck === 'boolean' ? parsed.requiresAck : undefined,
448+
destinations,
449+
metadata: parsed.metadata && typeof parsed.metadata === 'object' ? parsed.metadata : undefined,
450+
}
451+
452+
const simulatedEvent: WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify']> = {
453+
type: 'spark:notify',
454+
source: 'devtools',
455+
data: notify,
456+
}
457+
458+
pushEntry({
459+
direction: 'incoming',
460+
channel: 'server',
461+
type: 'spark:notify',
462+
summary: summarizeServerEvent(simulatedEvent as any),
463+
payload: simulatedEvent,
464+
})
465+
466+
try {
467+
const result = await characterOrchestratorStore.handleSparkNotify(simulatedEvent)
468+
if (result?.commands?.length) {
469+
for (const command of result.commands) {
470+
serverChannelStore.send({
471+
type: 'spark:command',
472+
data: command,
473+
})
474+
}
475+
}
476+
}
477+
catch (error) {
478+
pushEntry({
479+
direction: 'incoming',
480+
channel: 'devtools',
481+
type: 'spark:notify',
482+
summary: `handler error: ${String(error)}`,
483+
payload: simulatedEvent,
484+
})
485+
}
486+
}
487+
389488
const { data: incomingContext } = useBroadcastChannel<ContextMessage, ContextMessage>({
390489
name: CONTEXT_CHANNEL_NAME,
391490
})
@@ -667,6 +766,19 @@ onUnmounted(() => {
667766
</div>
668767
</div>
669768
</Section>
769+
<Section title="Simulate incoming" icon="i-solar:plain-2-bold-duotone" inner-class="gap-3" :expand="false">
770+
<div :class="['mt-4', 'border-t', 'border-neutral-200/70', 'pt-4', 'dark:border-neutral-800/80']">
771+
<FieldTextArea
772+
v-model="testSparkNotifyPayload"
773+
label="spark:notify"
774+
description="Raw JSON payload for spark:notify. Required: headline, destinations[]. id/eventId will be auto-filled if missing."
775+
:input-class="['font-mono', 'min-h-44']"
776+
/>
777+
<div :class="['flex', 'justify-end']">
778+
<Button label="Send spark:notify" icon="i-solar:bell-bing-bold-duotone" size="sm" @click="sendTestSparkNotify" />
779+
</div>
780+
</div>
781+
</Section>
670782

671783
<div :class="['flex']">
672784
<Input

0 commit comments

Comments
 (0)