diff options
Diffstat (limited to 'frontend/src/services')
| -rw-r--r-- | frontend/src/services/directiveApi.ts | 34 | ||||
| -rw-r--r-- | frontend/src/services/taskWs.ts | 88 |
2 files changed, 119 insertions, 3 deletions
diff --git a/frontend/src/services/directiveApi.ts b/frontend/src/services/directiveApi.ts index b82f594..4d1fd82 100644 --- a/frontend/src/services/directiveApi.ts +++ b/frontend/src/services/directiveApi.ts @@ -35,8 +35,9 @@ export interface DirectiveStep { taskPlan: string dependsOn: string[] status: string - taskId: string contractId: string + /** @deprecated Use contractId instead */ + taskId: string orderIndex: number sort_order?: number completedAt: string @@ -124,13 +125,40 @@ export async function pauseDirective(id: string): Promise<DirectiveWithSteps> { } export async function getUserSetting(key: string): Promise<any> { - const response = await apiFetch(`/api/v1/settings/${key}`) + const response = await apiFetch(`/api/v1/user-settings/${key}`) return response.json() } export async function upsertUserSetting(key: string, value: any): Promise<void> { - await apiFetch('/api/v1/settings', { + await apiFetch('/api/v1/user-settings', { method: 'PUT', body: JSON.stringify({ key, value }), }) } + +// ---- Task control APIs ---- + +export async function sendTaskMessage(taskId: string, message: string): Promise<void> { + await apiFetch(`/api/v1/mesh/tasks/${taskId}/message`, { + method: 'POST', + body: JSON.stringify({ message }), + }) +} + +export async function stopTask(taskId: string): Promise<void> { + await apiFetch(`/api/v1/mesh/tasks/${taskId}/stop`, { + method: 'POST', + }) +} + +export async function continueTask(taskId: string): Promise<void> { + await apiFetch(`/api/v1/mesh/tasks/${taskId}/continue`, { + method: 'POST', + }) +} + +export async function startTask(taskId: string): Promise<void> { + await apiFetch(`/api/v1/mesh/tasks/${taskId}/start`, { + method: 'POST', + }) +} 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 +} |
