summaryrefslogtreecommitdiff
path: root/frontend/src/services/taskWs.ts
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-04-28 17:35:08 +0100
committerGitHub <noreply@github.com>2026-04-28 17:35:08 +0100
commitd513f93c84ae985738e0f696fcb72fa1153046ef (patch)
treed169fa48ce93f1e204a80b60ca9295772bc2fa63 /frontend/src/services/taskWs.ts
parent5aa3fafb4acfa89c7d04e84abf7861607733e8ce (diff)
downloadsoryu-d513f93c84ae985738e0f696fcb72fa1153046ef.tar.gz
soryu-d513f93c84ae985738e0f696fcb72fa1153046ef.zip
feat: document UI with contract blocks, expandable logs, and interaction controls (#97)
* feat: soryu-co/soryu - makima: Rename tasks to contracts in directive API and types * feat: soryu-co/soryu - makima: Add contract interaction panel with comment and interrupt * feat: soryu-co/soryu - makima: Build expandable contract log feed in StepsDiagram * feat: soryu-co/soryu - makima: Rename tasks to contracts throughout document UI and add contract block support * feat: soryu-co/soryu - makima: Add comment and interrupt controls to expanded step log feed * feat: soryu-co/soryu - makima: Audit and fix Document UI feature flag visibility and missing implementations * feat: soryu-co/soryu - makima: Add expandable step rows with live log feed in StepsDiagram * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Integrate all document UI components and final polish
Diffstat (limited to 'frontend/src/services/taskWs.ts')
-rw-r--r--frontend/src/services/taskWs.ts88
1 files changed, 88 insertions, 0 deletions
diff --git a/frontend/src/services/taskWs.ts b/frontend/src/services/taskWs.ts
new file mode 100644
index 0000000..832648e
--- /dev/null
+++ b/frontend/src/services/taskWs.ts
@@ -0,0 +1,88 @@
+import { VNWebSocket } from './ws'
+
+export interface TaskOutputMessage {
+ task_id: string
+ message_type: string
+ content: string
+ tool_name?: string
+ tool_input?: string
+ is_error?: boolean
+ cost_usd?: number
+ duration_ms?: number
+ is_partial?: boolean
+}
+
+type TaskOutputCallback = (msg: TaskOutputMessage) => void
+
+export class TaskOutputStream {
+ private ws: VNWebSocket
+ private listeners: Map<string, Set<TaskOutputCallback>> = new Map()
+ private connected = false
+
+ constructor() {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
+ this.ws = new VNWebSocket(`${protocol}//${window.location.host}/ws/tasks`)
+
+ this.ws.on('message', (data: any) => {
+ if (data && data.type === 'TaskOutput') {
+ const payload = data.payload || data
+ const taskId = payload.task_id
+ if (taskId && this.listeners.has(taskId)) {
+ this.listeners.get(taskId)!.forEach(cb => cb(payload))
+ }
+ }
+ })
+
+ this.ws.on('open', () => {
+ this.connected = true
+ // Re-subscribe all active subscriptions on reconnect
+ for (const taskId of this.listeners.keys()) {
+ this.ws.send({ type: 'SubscribeOutput', task_id: taskId })
+ }
+ })
+
+ this.ws.on('close', () => {
+ this.connected = false
+ })
+ }
+
+ connect() {
+ this.ws.connect()
+ }
+
+ subscribe(taskId: string, callback: TaskOutputCallback) {
+ if (!this.listeners.has(taskId)) {
+ this.listeners.set(taskId, new Set())
+ // Only send subscribe if this is a new task subscription
+ this.ws.send({ type: 'SubscribeOutput', task_id: taskId })
+ }
+ this.listeners.get(taskId)!.add(callback)
+ }
+
+ unsubscribe(taskId: string, callback?: TaskOutputCallback) {
+ if (!this.listeners.has(taskId)) return
+
+ if (callback) {
+ this.listeners.get(taskId)!.delete(callback)
+ if (this.listeners.get(taskId)!.size > 0) return
+ }
+
+ this.listeners.delete(taskId)
+ this.ws.send({ type: 'UnsubscribeOutput', task_id: taskId })
+ }
+
+ close() {
+ this.ws.close()
+ this.listeners.clear()
+ }
+}
+
+let instance: TaskOutputStream | null = null
+
+export function getTaskOutputStream(): TaskOutputStream {
+ if (!instance) {
+ instance = new TaskOutputStream()
+ instance.connect()
+ }
+ return instance
+}