From df83b1df4d04b92e82505d791edb3155abfc8cd8 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Sun, 22 Mar 2026 03:36:48 +0100 Subject: [PATCH] refactor: finalize @agor/types + TauriAdapter (agent completion) --- packages/types/backend.ts | 8 +- packages/types/btmsg.ts | 2 +- packages/types/bttask.ts | 2 +- packages/types/health.ts | 2 +- packages/types/index.ts | 18 ++-- packages/types/project.ts | 4 +- packages/types/protocol.ts | 2 +- src/lib/backend/TauriAdapter.ts | 155 ++++++++------------------------ 8 files changed, 56 insertions(+), 137 deletions(-) diff --git a/packages/types/backend.ts b/packages/types/backend.ts index 94165ad..e376e2f 100644 --- a/packages/types/backend.ts +++ b/packages/types/backend.ts @@ -1,9 +1,9 @@ // BackendAdapter — abstraction layer for Tauri and Electrobun backends -import type { AgentStartOptions, AgentMessage, AgentStatus } from './agent.ts'; -import type { FileEntry, FileContent, PtyCreateOptions } from './protocol.ts'; -import type { SettingsMap } from './settings.ts'; -import type { GroupsFile } from './project.ts'; +import type { AgentStartOptions, AgentMessage, AgentStatus } from './agent'; +import type { FileEntry, FileContent, PtyCreateOptions } from './protocol'; +import type { SettingsMap } from './settings'; +import type { GroupsFile } from './project'; // ── Backend capabilities ───────────────────────────────────────────────────── diff --git a/packages/types/btmsg.ts b/packages/types/btmsg.ts index c7c12db..1373faf 100644 --- a/packages/types/btmsg.ts +++ b/packages/types/btmsg.ts @@ -1,6 +1,6 @@ // btmsg types — agent messaging system -import type { AgentId, GroupId } from './ids.ts'; +import type { AgentId, GroupId } from './ids'; export interface BtmsgAgent { id: AgentId; diff --git a/packages/types/bttask.ts b/packages/types/bttask.ts index a17a189..7592636 100644 --- a/packages/types/bttask.ts +++ b/packages/types/bttask.ts @@ -1,6 +1,6 @@ // bttask types — task board system -import type { AgentId, GroupId } from './ids.ts'; +import type { AgentId, GroupId } from './ids'; export type TaskStatus = 'todo' | 'progress' | 'review' | 'done' | 'blocked'; export type TaskPriority = 'low' | 'medium' | 'high' | 'critical'; diff --git a/packages/types/health.ts b/packages/types/health.ts index a306e0f..c8ac847 100644 --- a/packages/types/health.ts +++ b/packages/types/health.ts @@ -1,6 +1,6 @@ // Health tracking types -import type { ProjectId, SessionId } from './ids.ts'; +import type { ProjectId, SessionId } from './ids'; export type ActivityState = 'inactive' | 'running' | 'idle' | 'stalled'; diff --git a/packages/types/index.ts b/packages/types/index.ts index 41fd765..04936ec 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -1,11 +1,11 @@ // @agor/types — shared type definitions for Tauri and Electrobun frontends -export * from './ids.ts'; -export * from './agent.ts'; -export * from './project.ts'; -export * from './settings.ts'; -export * from './btmsg.ts'; -export * from './bttask.ts'; -export * from './health.ts'; -export * from './protocol.ts'; -export * from './backend.ts'; +export * from './ids'; +export * from './agent'; +export * from './project'; +export * from './settings'; +export * from './btmsg'; +export * from './bttask'; +export * from './health'; +export * from './protocol'; +export * from './backend'; diff --git a/packages/types/project.ts b/packages/types/project.ts index ebe3e8b..02cbd37 100644 --- a/packages/types/project.ts +++ b/packages/types/project.ts @@ -1,7 +1,7 @@ // Project and Group configuration types -import type { ProviderId } from './agent.ts'; -import type { ProjectId, GroupId, AgentId } from './ids.ts'; +import type { ProviderId } from './agent'; +import type { ProjectId, GroupId, AgentId } from './ids'; // ── Anchor budget ──────────────────────────────────────────────────────────── diff --git a/packages/types/protocol.ts b/packages/types/protocol.ts index 20cf1ae..aabe668 100644 --- a/packages/types/protocol.ts +++ b/packages/types/protocol.ts @@ -1,7 +1,7 @@ // Unified RPC protocol types — covers both Tauri commands and Electrobun RPC schema // These define the request/response shapes for all backend operations. -import type { AgentStartOptions, AgentStatus, AgentWireMessage } from './agent.ts'; +import type { AgentWireMessage } from './agent'; // ── PTY ────────────────────────────────────────────────────────────────────── diff --git a/src/lib/backend/TauriAdapter.ts b/src/lib/backend/TauriAdapter.ts index a823658..cfabeae 100644 --- a/src/lib/backend/TauriAdapter.ts +++ b/src/lib/backend/TauriAdapter.ts @@ -4,21 +4,11 @@ import { invoke } from '@tauri-apps/api/core'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import type { - BackendAdapter, - BackendCapabilities, - UnsubscribeFn, - AgentStartOptions, - AgentMessage, - AgentStatus, - FileEntry, - FileContent, - PtyCreateOptions, - SettingsMap, - GroupsFile, + BackendAdapter, BackendCapabilities, UnsubscribeFn, AgentStartOptions, + AgentMessage, AgentStatus, FileEntry, FileContent, PtyCreateOptions, + SettingsMap, GroupsFile, } from '@agor/types'; -// ── Capabilities ───────────────────────────────────────────────────────────── - const TAURI_CAPABILITIES: BackendCapabilities = { supportsPtyMultiplexing: true, supportsPluginSandbox: true, @@ -30,40 +20,25 @@ const TAURI_CAPABILITIES: BackendCapabilities = { supportsTelemetry: true, }; -// ── Wire format types (Tauri event payloads) ───────────────────────────────── - interface SidecarMessage { type: string; sessionId?: string; event?: Record; message?: string; - exitCode?: number | null; - signal?: string | null; } interface TauriDirEntry { - name: string; - path: string; - is_dir: boolean; - size: number; - ext: string; + name: string; path: string; is_dir: boolean; size: number; ext: string; } -// ── Adapter ────────────────────────────────────────────────────────────────── - export class TauriAdapter implements BackendAdapter { readonly capabilities = TAURI_CAPABILITIES; - private unlisteners: UnlistenFn[] = []; - async init(): Promise { - // Tauri IPC is ready as soon as the webview loads — no setup needed. - } + async init(): Promise { /* Tauri IPC ready on webview load */ } async destroy(): Promise { - for (const unlisten of this.unlisteners) { - unlisten(); - } + for (const fn of this.unlisteners) fn(); this.unlisteners = []; } @@ -80,9 +55,7 @@ export class TauriAdapter implements BackendAdapter { async getAllSettings(): Promise { const pairs = await invoke<[string, string][]>('settings_list'); const map: SettingsMap = {}; - for (const [k, v] of pairs) { - map[k] = v; - } + for (const [k, v] of pairs) map[k] = v; return map; } @@ -99,8 +72,7 @@ export class TauriAdapter implements BackendAdapter { // ── Agent ──────────────────────────────────────────────────────────────── async startAgent(options: AgentStartOptions): Promise<{ ok: boolean; error?: string }> { - // Map camelCase options to snake_case for Tauri command - const tauriOptions = { + const tauriOpts = { provider: options.provider ?? 'claude', session_id: options.sessionId, prompt: options.prompt, @@ -119,7 +91,7 @@ export class TauriAdapter implements BackendAdapter { extra_env: options.extraEnv, }; try { - await invoke('agent_query', { options: tauriOptions }); + await invoke('agent_query', { options: tauriOpts }); return { ok: true }; } catch (err: unknown) { return { ok: false, error: String(err) }; @@ -136,14 +108,9 @@ export class TauriAdapter implements BackendAdapter { } async sendPrompt(sessionId: string, prompt: string): Promise<{ ok: boolean; error?: string }> { - // Tauri agent_query supports resume via resume_session_id try { await invoke('agent_query', { - options: { - session_id: sessionId, - prompt, - resume_session_id: sessionId, - }, + options: { session_id: sessionId, prompt, resume_session_id: sessionId }, }); return { ok: true }; } catch (err: unknown) { @@ -155,13 +122,7 @@ export class TauriAdapter implements BackendAdapter { async createPty(options: PtyCreateOptions): Promise { return invoke('pty_spawn', { - options: { - shell: options.shell, - cwd: options.cwd, - args: options.args, - cols: options.cols, - rows: options.rows, - }, + options: { shell: options.shell, cwd: options.cwd, args: options.args, cols: options.cols, rows: options.rows }, }); } @@ -182,11 +143,7 @@ export class TauriAdapter implements BackendAdapter { async listDirectory(path: string): Promise { const entries = await invoke('list_directory_children', { path }); return entries.map((e) => ({ - name: e.name, - path: e.path, - isDir: e.is_dir, - size: e.size, - ext: e.ext || undefined, + name: e.name, path: e.path, isDir: e.is_dir, size: e.size, ext: e.ext || undefined, })); } @@ -200,13 +157,24 @@ export class TauriAdapter implements BackendAdapter { // ── Events ─────────────────────────────────────────────────────────────── - onAgentMessage(callback: (sessionId: string, messages: AgentMessage[]) => void): UnsubscribeFn { + /** Subscribe to a Tauri event; tracks unlisten for cleanup. */ + private listenTauri(event: string, handler: (payload: T) => void): UnsubscribeFn { let unlisten: UnlistenFn | null = null; + listen(event, (e) => handler(e.payload)).then((fn) => { + unlisten = fn; + this.unlisteners.push(fn); + }); + return () => { + if (unlisten) { + unlisten(); + this.unlisteners = this.unlisteners.filter((f) => f !== unlisten); + } + }; + } - listen('sidecar-message', (event) => { - const msg = event.payload; + onAgentMessage(callback: (sessionId: string, messages: AgentMessage[]) => void): UnsubscribeFn { + return this.listenTauri('sidecar-message', (msg) => { if (msg.type !== 'message' || !msg.sessionId || !msg.event) return; - // The sidecar emits individual messages; wrap in array for uniform API const agentMsg: AgentMessage = { id: String(msg.event['id'] ?? Date.now()), role: mapSidecarRole(String(msg.event['type'] ?? '')), @@ -216,78 +184,32 @@ export class TauriAdapter implements BackendAdapter { timestamp: Number(msg.event['timestamp'] ?? Date.now()), }; callback(msg.sessionId, [agentMsg]); - }).then((fn) => { - unlisten = fn; - this.unlisteners.push(fn); }); - - return () => { - if (unlisten) { - unlisten(); - this.unlisteners = this.unlisteners.filter((fn) => fn !== unlisten); - } - }; } onAgentStatus(callback: (sessionId: string, status: AgentStatus, error?: string) => void): UnsubscribeFn { - let unlisten: UnlistenFn | null = null; - - listen('sidecar-message', (event) => { - const msg = event.payload; + return this.listenTauri('sidecar-message', (msg) => { if (msg.type !== 'status' || !msg.sessionId) return; - const status = normalizeAgentStatus(String(msg.event?.['status'] ?? 'idle')); - callback(msg.sessionId, status, msg.message); - }).then((fn) => { - unlisten = fn; - this.unlisteners.push(fn); + callback(msg.sessionId, normalizeStatus(String(msg.event?.['status'] ?? 'idle')), msg.message); }); - - return () => { - if (unlisten) { - unlisten(); - this.unlisteners = this.unlisteners.filter((fn) => fn !== unlisten); - } - }; } onAgentCost( callback: (sessionId: string, costUsd: number, inputTokens: number, outputTokens: number) => void, ): UnsubscribeFn { - let unlisten: UnlistenFn | null = null; - - listen('sidecar-message', (event) => { - const msg = event.payload; + return this.listenTauri('sidecar-message', (msg) => { if (msg.type !== 'cost' || !msg.sessionId || !msg.event) return; - callback( - msg.sessionId, - Number(msg.event['totalCostUsd'] ?? 0), - Number(msg.event['inputTokens'] ?? 0), - Number(msg.event['outputTokens'] ?? 0), - ); - }).then((fn) => { - unlisten = fn; - this.unlisteners.push(fn); + callback(msg.sessionId, Number(msg.event['totalCostUsd'] ?? 0), Number(msg.event['inputTokens'] ?? 0), Number(msg.event['outputTokens'] ?? 0)); }); - - return () => { - if (unlisten) { - unlisten(); - this.unlisteners = this.unlisteners.filter((fn) => fn !== unlisten); - } - }; } - onPtyOutput(callback: (sessionId: string, data: string) => void): UnsubscribeFn { - // Tauri PTY uses per-session event channels: pty-data-{id} - // This adapter requires the caller to register per-session; for the generic - // interface we listen to a global pty-data event if available, or this becomes - // a no-op that individual TerminalPane components handle directly. - // For Phase 1, PTY event wiring remains in pty-bridge.ts. + onPtyOutput(_callback: (sessionId: string, data: string) => void): UnsubscribeFn { + // Tauri PTY uses per-session event channels (pty-data-{id}). + // Phase 1: PTY event wiring remains in pty-bridge.ts per-session listeners. return () => {}; } - onPtyClosed(callback: (sessionId: string, exitCode: number | null) => void): UnsubscribeFn { - // Same as onPtyOutput — Tauri uses per-session events. + onPtyClosed(_callback: (sessionId: string, exitCode: number | null) => void): UnsubscribeFn { return () => {}; } } @@ -302,15 +224,12 @@ function mapSidecarRole(type: string): MsgRole { case 'thinking': return 'thinking'; case 'tool_call': return 'tool-call'; case 'tool_result': return 'tool-result'; - case 'init': - case 'error': - case 'status': - return 'system'; + case 'init': case 'error': case 'status': return 'system'; default: return 'assistant'; } } -function normalizeAgentStatus(status: string): AgentStatus { +function normalizeStatus(status: string): AgentStatus { if (status === 'idle' || status === 'starting' || status === 'running' || status === 'done' || status === 'error') { return status; }