package bridge import "encoding/json" // This file is the wire contract between the SPA and the backend, over a // WebSocket carrying one JSON message per text frame. ACP payloads (session // updates, permission requests) are relayed verbatim so the frontend works // with the protocol's own shapes, documented at // https://agentclientprotocol.com. // Incoming message types (browser → server). const ( // IncomingPrompt starts a turn: {"type":"prompt","text":"...","attachments":[...]}. IncomingPrompt = "prompt" // IncomingCancel interrupts the current turn: {"type":"cancel"}. IncomingCancel = "cancel" // IncomingPermissionResponse answers a permission request: // {"type":"permission_response","requestId":"...","optionId":"..."} or // {"type":"permission_response","requestId":"...","cancelled":true}. IncomingPermissionResponse = "permission_response" ) // Outgoing message types (server → browser). const ( // OutgoingHello is sent once per connection with the session state. OutgoingHello = "hello" // OutgoingUserMessage echoes the prompt that started a turn, so every // client (and every reconnection replay) renders the user's side of the // conversation from the same source of truth as the agent's side. OutgoingUserMessage = "user_message" // OutgoingSessionUpdate relays one ACP session/update notification; // its "update" field is the raw ACP SessionUpdate object. OutgoingSessionUpdate = "session_update" // OutgoingPermissionRequest relays an ACP permission request; its // "request" field is the raw ACP RequestPermissionRequest object. OutgoingPermissionRequest = "permission_request" // OutgoingPermissionResolved tells every client a permission request // was answered (so open dialogs close everywhere). OutgoingPermissionResolved = "permission_resolved" // OutgoingTurnStarted marks the start of a prompt turn. OutgoingTurnStarted = "turn_started" // OutgoingTurnEnded marks the end of a turn, with its stop reason. OutgoingTurnEnded = "turn_ended" // OutgoingError carries a human-readable error message. OutgoingError = "error" ) // Attachment is a workspace file the user mentioned in a prompt (with the // "@" selector). The server turns it into an ACP resource_link content block // so the agent can open the file, while the "@name" mention stays visible in // the prompt text. type Attachment struct { // Path is the file path, absolute or relative to the workspace root. Path string `json:"path"` // Name is the label shown for the file (the relative path in the UI); // it defaults to Path. Name string `json:"name,omitempty"` } // Incoming is a message sent by the browser. type Incoming struct { Type string `json:"type"` // Text is the prompt text (IncomingPrompt). Text string `json:"text,omitempty"` // Attachments are the files mentioned in the prompt (IncomingPrompt). Attachments []Attachment `json:"attachments,omitempty"` // RequestId identifies the permission request being answered. RequestId string `json:"requestId,omitempty"` // OptionId is the chosen ACP permission option. OptionId string `json:"optionId,omitempty"` // Cancelled dismisses the permission request instead of choosing. Cancelled bool `json:"cancelled,omitempty"` } // Outgoing is a message sent to the browser. type Outgoing struct { Type string `json:"type"` // SessionId is set on OutgoingHello. SessionId string `json:"sessionId,omitempty"` // TurnActive is set on OutgoingHello: true while a prompt turn runs. TurnActive bool `json:"turnActive,omitempty"` // Update is the raw ACP SessionUpdate (OutgoingSessionUpdate). Update json.RawMessage `json:"update,omitempty"` // RequestId identifies a permission request across messages. RequestId string `json:"requestId,omitempty"` // Request is the raw ACP RequestPermissionRequest (OutgoingPermissionRequest). Request json.RawMessage `json:"request,omitempty"` // Text is the user's prompt text (OutgoingUserMessage). Text string `json:"text,omitempty"` // Attachments are the files the prompt mentioned (OutgoingUserMessage). Attachments []Attachment `json:"attachments,omitempty"` // StopReason is the ACP stop reason (OutgoingTurnEnded). StopReason string `json:"stopReason,omitempty"` // Message is a human-readable error (OutgoingError). Message string `json:"message,omitempty"` } // encode marshals an Outgoing message; the bridge treats a marshalling // failure as a programming error and panics, because every field is // marshal-safe by construction. func encode(msg Outgoing) []byte { raw, err := json.Marshal(msg) if err != nil { panic(err) } return raw }