diff --git a/scripts/generate-types.sh b/scripts/generate-types.sh index 21171010..7305edbd 100755 --- a/scripts/generate-types.sh +++ b/scripts/generate-types.sh @@ -20,6 +20,9 @@ typeshare "$PROJECT_ROOT/src" \ --lang=typescript \ --output-file="$OUTPUT_FILE" +# Keep generated output stable for git diff --check. +perl -0pi -e 's/\n+\z/\n/' "$OUTPUT_FILE" + echo "" echo "TypeScript types generated successfully!" echo "Output: $OUTPUT_FILE" diff --git a/src/computer_use/actions.rs b/src/computer_use/actions.rs index 3e28d08c..dd879d37 100644 --- a/src/computer_use/actions.rs +++ b/src/computer_use/actions.rs @@ -29,7 +29,8 @@ impl Default for ComputerUseButton { } } -#[typeshare] +// Kept out of typeshare because these internally tagged enums are consumed by +// manually maintained frontend types and changing serde shape would break WS/API. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ComputerUseAction { @@ -89,7 +90,6 @@ pub struct ComputerUseScreenshot { pub height: u32, } -#[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "role", rename_all = "snake_case")] pub enum ComputerUseConversationMessage { @@ -145,7 +145,6 @@ pub struct ComputerUseSessionSummary { pub final_message: Option, } -#[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ComputerUseWsClientMessage { @@ -155,7 +154,6 @@ pub enum ComputerUseWsClientMessage { }, } -#[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ComputerUseWsServerMessage { @@ -166,3 +164,43 @@ pub enum ComputerUseWsServerMessage { ActionsExecuted { actions: Vec }, Error { message: String }, } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn computer_use_action_keeps_flat_tagged_json_shape() { + let action = ComputerUseAction::Click { + x: 10, + y: 20, + button: ComputerUseButton::Left, + }; + + assert_eq!( + serde_json::to_value(action).unwrap(), + json!({ + "type": "click", + "x": 10, + "y": 20, + "button": "left" + }) + ); + } + + #[test] + fn computer_use_ws_message_keeps_flat_tagged_json_shape() { + let message = ComputerUseWsServerMessage::ScreenshotRequested { + request_id: "req-1".to_string(), + }; + + assert_eq!( + serde_json::to_value(message).unwrap(), + json!({ + "type": "screenshot_requested", + "request_id": "req-1" + }) + ); + } +} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 9f6042c8..7b371ab6 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -1,5 +1,14 @@ import { request, ApiError } from './request' -import type { CanonicalKey, Ch9329DescriptorState } from '@/types/generated' +import type { + CanonicalKey, + Ch9329DescriptorState, + ComputerUseButton, + ComputerUseConfigResponse, + ComputerUseConfigUpdate, + ComputerUseSessionStatus, + ComputerUseSessionSummary, + ComputerUseStartRequest, +} from '@/types/generated' import { useHidWebSocket, type HidKeyboardEvent, type HidMouseEvent } from '@/composables/useHidWebSocket' const API_BASE = '/api' @@ -457,16 +466,7 @@ export const hidApi = { isWebSocketConnected: () => hidWs.connected.value, } -export type ComputerUseStatus = - | 'idle' - | 'waiting_screenshot' - | 'thinking' - | 'executing' - | 'completed' - | 'failed' - | 'stopped' - -export type ComputerUseButton = 'left' | 'middle' | 'right' +export type ComputerUseStatus = ComputerUseSessionStatus export type ComputerUseAction = | { type: 'click'; x: number; y: number; button?: ComputerUseButton } @@ -479,49 +479,18 @@ export type ComputerUseAction = | { type: 'wait'; ms: number } | { type: 'screenshot' } -export interface ComputerUseScreenshot { - data_url: string - width: number - height: number -} - export type ComputerUseConversationMessage = | { role: 'user'; text: string } | { role: 'assistant'; text: string } -export interface ComputerUseConfig { - enabled: boolean - provider: string - base_url: string - model: string - max_steps: number - timeout_seconds: number - api_key_configured: boolean - api_key_source: string -} +export type ComputerUseConfig = ComputerUseConfigResponse -export interface ComputerUseSession { - id: string | null - status: ComputerUseStatus - prompt: string | null - step: number - max_steps: number - last_error: string | null - final_message: string | null -} +export type ComputerUseSession = ComputerUseSessionSummary export const computerUseApi = { config: () => request('/config/computer-use'), - updateConfig: (data: { - enabled?: boolean - base_url?: string - model?: string - max_steps?: number - timeout_seconds?: number - openai_api_key?: string - clear_openai_api_key?: boolean - }) => + updateConfig: (data: ComputerUseConfigUpdate) => request('/config/computer-use', { method: 'PATCH', body: JSON.stringify(data), @@ -529,7 +498,7 @@ export const computerUseApi = { session: () => request('/computer-use/session'), - start: (data: { prompt: string; continue_conversation?: boolean; client_id: string; max_steps?: number; timeout_seconds?: number }) => + start: (data: ComputerUseStartRequest) => request('/computer-use/session', { method: 'POST', body: JSON.stringify(data), @@ -847,6 +816,13 @@ export type { StreamMode, EncoderType, BitratePreset, + ComputerUseButton, + ComputerUseConfigResponse, + ComputerUseConfigUpdate, + ComputerUseScreenshot, + ComputerUseSessionStatus, + ComputerUseSessionSummary, + ComputerUseStartRequest, } from '@/types/generated' export const audioApi = { diff --git a/web/src/types/generated.ts b/web/src/types/generated.ts index 7953a4be..e6d4d28f 100644 --- a/web/src/types/generated.ts +++ b/web/src/types/generated.ts @@ -168,6 +168,15 @@ export interface WebConfig { ssl_key_path?: string; } +export interface ComputerUseConfig { + enabled: boolean; + provider: string; + base_url: string; + model: string; + max_steps: number; + timeout_seconds: number; +} + export interface TtydConfig { enabled: boolean; shell: string; @@ -227,6 +236,11 @@ export interface ExtensionsConfig { frpc: FrpcConfig; } +export enum RustDeskCodec { + H264 = "h264", + H265 = "h265", +} + export interface RustDeskConfig { enabled: boolean; codec: RustDeskCodec; @@ -235,11 +249,6 @@ export interface RustDeskConfig { device_id: string; } -export enum RustDeskCodec { - H264 = "h264", - H265 = "h265", -} - export enum VncEncoding { TightJpeg = "tight_jpeg", H264 = "h264", @@ -283,6 +292,7 @@ export interface AppConfig { audio: AudioConfig; stream: StreamConfig; web: WebConfig; + computer_use: ComputerUseConfig; extensions: ExtensionsConfig; rustdesk: RustDeskConfig; vnc: VncConfig; @@ -356,6 +366,66 @@ export interface Ch9329DescriptorState { config_mode_available: boolean; } +export interface ComputerUseConfigResponse { + enabled: boolean; + provider: string; + base_url: string; + model: string; + max_steps: number; + timeout_seconds: number; + api_key_configured: boolean; + api_key_source: string; +} + +export interface ComputerUseConfigUpdate { + enabled?: boolean; + base_url?: string; + model?: string; + max_steps?: number; + timeout_seconds?: number; + openai_api_key?: string; + clear_openai_api_key?: boolean; +} + +export interface ComputerUsePoint { + x: number; + y: number; +} + +export interface ComputerUseScreenshot { + data_url: string; + width: number; + height: number; +} + +export enum ComputerUseSessionStatus { + Idle = "idle", + WaitingScreenshot = "waiting_screenshot", + Thinking = "thinking", + Executing = "executing", + Completed = "completed", + Failed = "failed", + Stopped = "stopped", +} + +export interface ComputerUseSessionSummary { + id?: string; + status: ComputerUseSessionStatus; + prompt?: string; + step: number; + max_steps: number; + last_error?: string; + final_message?: string; +} + +export interface ComputerUseStartRequest { + prompt: string; + continue_conversation?: boolean; + client_id: string; + max_steps?: number; + timeout_seconds?: number; +} + export interface EasytierConfigUpdate { enabled?: boolean; network_name?: string; @@ -738,3 +808,9 @@ export enum CanonicalKey { AltRight = "AltRight", MetaRight = "MetaRight", } + +export enum ComputerUseButton { + Left = "left", + Middle = "middle", + Right = "right", +}