Files
IdeA/frontend/src/adapters/http/frames.ts
Blomios 1b1476de52 fix(chat): le transcript s'affiche à nouveau après redémarrage automatique d'un agent — #295 (app-tauri ChatBridge: cartes successors/owners, rebind_session déplace sink+scrolloback du logical chat vers la session de remplacement et canonical_session résout les écritures avec l'ancien id sur toutes les opérations register/scrollback/detach/send_output; backend stream: ReplayOutputBridge::rekey prépare le scrollback source devant le destination et invalide les générations pré-rekey des deux pumps; commands: agent_send binde l'owner stable (projectId,agentId) au tour humain et rebind avant le flux de continuation au handoff restart; reattach_agent_chat résout le successeur du bridge puis fallback identité stable projectId/agentId explicite ou owner bindé, renvoie sessionId effectif + conversationId, NOT_FOUND sinon; web-server chat.attach même fallback stale→live par project/agent; DTO ReattachChatDto/ReattachResultDto documentent l'id effectif post-restart; frontend: ReattachAgentChatOptions, conversationId dans ChatAttachedPayload, CustomAgentChatView réattache sur agentRestartCompleted (dédupe par restartId, scrollback appliqué, tours restart préservés), retry identité stable sur NOT_FOUND dans openOrAttach avant fallback launch frais, publication sessionId effectif + conversationId, état vide «Conversation liée» quand une conversation/run/workstate lié existe; QA verte reconfirmée par Git: git diff --check, cargo fmt --all --check, app-tauri chat_bridge 34/34 dont 3 nouveaux restart_rebind + dto_chat 24/24, cargo check -p app-tauri, backend 91+7 ignored permissions socket, web-server websocket_chat_attach 3/3, npm test 77/77 CustomAgentChatView+agentGateway, npm run build; réserves non bloquantes: warnings Vite esbuild/oxc et chunk >500kB connus)
2026-09-09 18:44:01 +02:00

159 lines
4.6 KiB
TypeScript

/**
* WebSocket frame contract for the web (client/server) transport — ticket #13,
* lot F1. Mirrors the first-draft frames in
* `docs/ticket13-b0-backend-transport-inventory.md` (§ "WebSocket first draft").
*
* F1 delivers the *types + skeleton* only. The full PTY wiring runs on top of
* these in F3/B5 once a server exists (B3/B4). Keeping the frame shapes here — in
* the adapters layer — lets the terminal/agent/chat gateways be structurally
* correct against the contract today without a live backend.
*
* Envelope (B0): every frame carries an `id` (client→server) or optional
* `replyTo` (server→client, present for acknowledgements, absent for unsolicited
* output/events). PTY bytes travel base64-encoded (`bytesBase64`) so binary
* survives JSON.
*/
import type { ReplyChunk } from "@/domain";
// ---------------------------------------------------------------------------
// Base64 helpers (binary PTY bytes over JSON frames)
// ---------------------------------------------------------------------------
/** Encodes raw bytes to a base64 string for a `bytesBase64` frame field. */
export function bytesToBase64(bytes: Uint8Array): string {
let binary = "";
for (let i = 0; i < bytes.length; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
// `btoa` exists in browsers and jsdom.
return btoa(binary);
}
/** Decodes a base64 `bytesBase64` frame field back to raw bytes. */
export function base64ToBytes(base64: string): Uint8Array {
const binary = atob(base64);
const out = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
out[i] = binary.charCodeAt(i);
}
return out;
}
// ---------------------------------------------------------------------------
// Client → server frames
// ---------------------------------------------------------------------------
/** Client→server frame kinds (B0 § PTY + structured chat + ping). */
export type ClientFrameKind =
| "terminal.attach"
| "terminal.open"
| "agent.launch"
| "terminal.input"
| "terminal.resize"
| "terminal.detach"
| "terminal.close"
| "chat.attach"
| "chat.send"
| "chat.detach"
| "ping";
/** A client→server frame. `id` correlates the eventual `replyTo`. */
export interface ClientFrame {
id: string;
kind: ClientFrameKind;
payload: Record<string, unknown>;
}
// ---------------------------------------------------------------------------
// Server → client frames
// ---------------------------------------------------------------------------
/** Server→client frame kinds. */
export type ServerFrameKind =
| "terminal.attached"
| "terminal.output"
| "terminal.status"
| "chat.attached"
| "chat.output"
| "event.domain"
| "error"
| "pong";
/** Suggested lifecycle statuses (B0). */
export type TerminalStatus =
| "starting"
| "running"
| "exited"
| "closed"
| "detached"
| "reattached";
/** A server→client frame. `replyTo` is present only for acknowledgements. */
export interface ServerFrame {
kind: ServerFrameKind;
replyTo?: string;
payload: Record<string, unknown>;
}
/** Payload of a `terminal.attached` acknowledgement (B5, `server.rs`). */
export interface AttachedPayload {
session: {
sessionId: string;
projectId?: string;
nodeId?: string | null;
rows: number;
cols: number;
};
nextSeq: number;
/**
* Bounded scrollback replayed at (re)attach. B5 sends a single entry (`seq:0`)
* carrying all retained bytes, or an empty array when there is nothing to
* replay.
*/
scrollback: { seq: number; bytesBase64: string }[];
gap: boolean;
/** Lifecycle status carried on the ack (B5 sends `"running"`). */
status?: string;
/** Conversation id minted by an `agent.launch` (mirrors `assignedConversationId`). */
assignedConversationId?: string;
}
/** Payload of a `terminal.output` frame. */
export interface OutputPayload {
sessionId: string;
seq: number;
bytesBase64: string;
}
/**
* Payload of a `terminal.status` frame (B5). `status` is a lifecycle transition
* (`"exited"` on close); `exitCode` is present for `"exited"` (may be `null`).
*/
export interface StatusPayload {
sessionId: string;
status: string;
exitCode?: number | null;
}
/** Payload of a `chat.output` frame (structured assistant stream). */
export interface ChatOutputPayload {
sessionId: string;
chunk: ReplyChunk;
}
/** Payload of a `chat.attached` acknowledgement. */
export interface ChatAttachedPayload {
sessionId?: string;
session?: {
sessionId: string;
projectId?: string;
nodeId?: string | null;
};
cellKind?: "chat" | "pty";
assignedConversationId?: string;
conversationId?: string;
engineSessionId?: string;
scrollback?: ReplyChunk[];
}