From e5b1351f0761ffaf7ba2a0bb05876b5de40e3a34 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 16 May 2026 04:09:31 +0000 Subject: [PATCH 1/3] feat(web): TypeScript types for /api/v1/* surface --- web/src/api/types.ts | 90 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 web/src/api/types.ts diff --git a/web/src/api/types.ts b/web/src/api/types.ts new file mode 100644 index 0000000..945c18c --- /dev/null +++ b/web/src/api/types.ts @@ -0,0 +1,90 @@ +// web/src/api/types.ts +// Hand-authored to match /api/v1/* schemas. v2.1 should generate from OpenAPI. + +export type SessionId = string; // SES-YYYYMMDD-NNN + +export interface Session { + id: SessionId; + status: 'in_progress' | 'awaiting_input' | 'matched' | 'resolved' | 'escalated' | 'stopped' | 'error' | 'new'; + created_at: string; // ISO UTC + updated_at: string; + deleted_at: string | null; + agents_run: AgentRun[]; + tool_calls: ToolCall[]; + findings: Record; + pending_intervention: Record | null; + user_inputs: string[]; + parent_session_id: SessionId | null; + dedup_rationale: string | null; + extra_fields: Record; + version: number; +} + +export interface AgentRun { + agent: string; + started_at: string; + ended_at: string; + summary: string; + token_usage?: { input_tokens: number; output_tokens: number; total_tokens: number }; + confidence: number | null; + confidence_rationale: string | null; + signal: string | null; +} + +export interface ToolCall { + agent: string; + tool: string; + args: Record; + result: unknown; + ts: string; + risk: 'low' | 'medium' | 'high' | null; + status: 'executed' | 'pending_approval' | 'approved' | 'rejected' | 'auto_rejected'; + approver: string | null; + approved_at: string | null; + approval_rationale: string | null; +} + +export interface AgentDefinition { + name: string; + kind: string; // 'responsive' | 'gated' | etc. + model: string; + tools: string[]; + routes: Record; + system_prompt_excerpt: string; +} + +export interface SessionEvent { + seq: number; + kind: string; + payload: Record; + ts: string; + session_id?: string; // present on /sessions/recent/events stream +} + +export interface SessionFullBundle { + session: Session; + agents_run: AgentRun[]; + tool_calls: ToolCall[]; + events: SessionEvent[]; + agent_definitions: Record; + vm_seq: number; +} + +export interface UiHints { + brand_name: string; + brand_logo_url: string | null; + approval_rationale_templates: string[]; + hitl_question_templates: Record; + environments: string[]; +} + +export interface AppView { + id: string; + title: string; + applies_to: string; // 'always' | 'agent:NAME' | 'tool:NAME' + url: string; +} + +export interface ApiError { + error: { code: string; message: string; details: Record }; +} From d7132435a0acb84b1b818a1b72b4f6a973dc69cd Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 16 May 2026 04:08:58 +0000 Subject: [PATCH 2/3] feat(web): apiFetch client with bearer-token + structured-error handling --- web/src/api/client.ts | 44 ++++++++++++++++++++++++++++++++ web/tests/unit/client.test.ts | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 web/src/api/client.ts create mode 100644 web/tests/unit/client.test.ts diff --git a/web/src/api/client.ts b/web/src/api/client.ts new file mode 100644 index 0000000..c5cce43 --- /dev/null +++ b/web/src/api/client.ts @@ -0,0 +1,44 @@ +const API_PREFIX = '/api/v1'; + +export class ApiClientError extends Error { + status: number; + code: string; + details: Record; + constructor(status: number, code: string, message: string, details: Record) { + super(message); + this.status = status; + this.code = code; + this.details = details; + } +} + +interface FetchOptions extends RequestInit { + json?: unknown; +} + +export async function apiFetch( + path: string, + options: FetchOptions = {}, +): Promise { + const url = path.startsWith('http') ? path : `${API_PREFIX}${path}`; + const headers = new Headers(options.headers); + const token = localStorage.getItem('asr.token'); + if (token) headers.set('Authorization', `Bearer ${token}`); + if (options.json !== undefined) { + headers.set('Content-Type', 'application/json'); + options.body = JSON.stringify(options.json); + } + const res = await fetch(url, { ...options, headers, method: options.method ?? 'GET' }); + if (!res.ok) { + let body: { error?: { code?: string; message?: string; details?: Record } } = {}; + try { body = await res.clone().json(); } catch { /* not JSON */ } + throw new ApiClientError( + res.status, + body.error?.code ?? 'unknown', + body.error?.message ?? res.statusText, + body.error?.details ?? {}, + ); + } + if (res.status === 204) return undefined as T; + return await res.json() as T; +} diff --git a/web/tests/unit/client.test.ts b/web/tests/unit/client.test.ts new file mode 100644 index 0000000..f9ed4ef --- /dev/null +++ b/web/tests/unit/client.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { apiFetch, ApiClientError } from '@/api/client'; + +describe('apiFetch', () => { + beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); + }); + + it('prepends /api/v1 to relative paths', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } }), + ); + global.fetch = fetchMock; + + await apiFetch('/sessions'); + expect(fetchMock).toHaveBeenCalledWith( + '/api/v1/sessions', + expect.objectContaining({ method: 'GET' }), + ); + }); + + it('attaches Authorization header when token in localStorage', async () => { + localStorage.setItem('asr.token', 'tk-abc'); + const fetchMock = vi.fn().mockResolvedValue(new Response('{}', { status: 200 })); + global.fetch = fetchMock; + + await apiFetch('/sessions'); + const init = fetchMock.mock.calls[0]![1] as RequestInit; + const headers = new Headers(init.headers); + expect(headers.get('Authorization')).toBe('Bearer tk-abc'); + }); + + it('throws ApiClientError on 4xx with structured error body', async () => { + global.fetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ error: { code: 'not_found', message: 'session not found', details: {} } }), + { status: 404, headers: { 'content-type': 'application/json' } }, + ), + ); + + await expect(apiFetch('/sessions/SES-x')).rejects.toThrow(ApiClientError); + await expect(apiFetch('/sessions/SES-x')).rejects.toMatchObject({ + status: 404, + code: 'not_found', + }); + }); +}); From 12929ded07b7020b30a9462169e188f2e50082cd Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 16 May 2026 04:12:48 +0000 Subject: [PATCH 3/3] fix(web): include backend ToolCall statuses (executed_with_notify, timeout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TS spec from the design doc omitted two literal status values the backend actually emits. Adding them now so Task 25's reducer can switch on the full set without TS narrowing errors. The 'auto_rejected' value in the spec is kept — the UI synthesizes it on HITL timeout per §16.5. --- web/src/api/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 945c18c..100825e 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -38,7 +38,7 @@ export interface ToolCall { result: unknown; ts: string; risk: 'low' | 'medium' | 'high' | null; - status: 'executed' | 'pending_approval' | 'approved' | 'rejected' | 'auto_rejected'; + status: 'executed' | 'executed_with_notify' | 'pending_approval' | 'approved' | 'rejected' | 'timeout' | 'auto_rejected'; approver: string | null; approved_at: string | null; approval_rationale: string | null;