mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 13:42:50 +00:00
feat(terminal): stream negotiated ANSI replay to clients
Restore configured client history up to host retention without collecting capable replay in the daemon. Preserve single-frame ANSI delivery for older clients and normalize older hosts at the SDK boundary.
This commit is contained in:
@@ -89,3 +89,18 @@ describe("host route binary framing", () => {
|
||||
).toThrow("Invalid host route ID");
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
TerminalStreamOpcode.ReplayBegin,
|
||||
TerminalStreamOpcode.ReplayData,
|
||||
TerminalStreamOpcode.ReplayEnd,
|
||||
])("decodes replay opcode %s directly and inside a host route", (opcode) => {
|
||||
const payload = new TextEncoder().encode("界 replay");
|
||||
const bytes = encodeTerminalStreamFrame({ opcode, slot: 12, payload });
|
||||
const expected = { kind: "terminal", frame: { opcode, slot: 12, payload } };
|
||||
expect(decodeBinaryFrame(bytes)).toEqual(expected);
|
||||
const routed = decodeBinaryFrame(encodeHostRouteFrame("host", bytes));
|
||||
expect(routed?.kind).toBe("host_route");
|
||||
if (routed?.kind !== "host_route") throw new Error("Missing host route");
|
||||
expect(decodeBinaryFrame(routed.frame.payload)).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,9 @@ export function decodeBinaryFrame(bytes: Uint8Array): BinaryFrame | null {
|
||||
case TerminalStreamOpcode.Input:
|
||||
case TerminalStreamOpcode.Resize:
|
||||
case TerminalStreamOpcode.Snapshot:
|
||||
case TerminalStreamOpcode.ReplayBegin:
|
||||
case TerminalStreamOpcode.ReplayData:
|
||||
case TerminalStreamOpcode.ReplayEnd:
|
||||
case TerminalStreamOpcode.Restore: {
|
||||
const frame = decodeTerminalStreamFrame(bytes);
|
||||
return frame ? { kind: "terminal", frame } : null;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { TerminalStateSchema } from "../messages.js";
|
||||
|
||||
export const TerminalReplayMetadataSchema = z.object({
|
||||
rows: z.number().int().positive(),
|
||||
cols: z.number().int().positive(),
|
||||
retainedHistoryLines: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
export const TerminalStreamResizeSchema = z.object({
|
||||
rows: z.number().int().positive(),
|
||||
cols: z.number().int().positive(),
|
||||
@@ -13,6 +19,9 @@ export const TerminalStreamOpcode = {
|
||||
Resize: 0x03,
|
||||
Snapshot: 0x04,
|
||||
Restore: 0x05,
|
||||
ReplayBegin: 0x06,
|
||||
ReplayData: 0x07,
|
||||
ReplayEnd: 0x08,
|
||||
} as const;
|
||||
|
||||
export type TerminalStreamOpcode = (typeof TerminalStreamOpcode)[keyof typeof TerminalStreamOpcode];
|
||||
@@ -58,6 +67,9 @@ function isTerminalStreamOpcode(value: number): value is TerminalStreamOpcode {
|
||||
value === TerminalStreamOpcode.Input ||
|
||||
value === TerminalStreamOpcode.Resize ||
|
||||
value === TerminalStreamOpcode.Snapshot ||
|
||||
value === TerminalStreamOpcode.ReplayBegin ||
|
||||
value === TerminalStreamOpcode.ReplayData ||
|
||||
value === TerminalStreamOpcode.ReplayEnd ||
|
||||
value === TerminalStreamOpcode.Restore
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const CLIENT_CAPS = {
|
||||
terminalChunkedReplay: "terminal_chunked_replay",
|
||||
helloRejection: "hello_rejection",
|
||||
// COMPAT(ownedSubscriptions): added in v0.8.0, remove legacy ownership after 2027-03-09.
|
||||
ownedSubscriptions: "owned_subscriptions",
|
||||
|
||||
@@ -3740,6 +3740,7 @@ export const ServerInfoStatusPayloadSchema = z
|
||||
resumeOnStartupSetting: z.boolean().optional(),
|
||||
// COMPAT(terminalRestoreModes): added in v0.1.81, remove gate after 2026-11-23.
|
||||
"terminal-restore-modes": z.boolean().optional(),
|
||||
terminalChunkedReplay: z.boolean().optional(),
|
||||
// COMPAT(terminalInputModeReplay): added in v0.2.6, remove gate after 2027-02-02.
|
||||
"terminal-input-mode-replay": z.boolean().optional(),
|
||||
// COMPAT(terminalSizeOwnership): added in v0.2.6, remove gate after 2027-02-02.
|
||||
|
||||
Reference in New Issue
Block a user