diff options
| author | soryu <soryu@soryu.co> | 2026-04-28 17:35:08 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-28 17:35:08 +0100 |
| commit | d513f93c84ae985738e0f696fcb72fa1153046ef (patch) | |
| tree | d169fa48ce93f1e204a80b60ca9295772bc2fa63 /frontend/src/services/taskWs.ts | |
| parent | 5aa3fafb4acfa89c7d04e84abf7861607733e8ce (diff) | |
| download | soryu-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.ts | 88 |
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 +} |
