feat(desktop): add typed terminal session client

Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
This commit is contained in:
npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta
2026-08-01 22:12:36 -04:00
parent 48b711cc6a
commit bb7bfba148
2 changed files with 255 additions and 0 deletions
@@ -0,0 +1,114 @@
import assert from "node:assert/strict";
import { afterEach, test } from "node:test";
const callbacks = new Map();
const calls = [];
let nextCallback = 1;
let attachDuringInvoke;
globalThis.isTauri = true;
globalThis.window = {
__TAURI_INTERNALS__: {
invoke(command, args) {
calls.push({ command, args });
if (command === "terminal_attach") attachDuringInvoke?.(args);
return Promise.resolve(
command === "terminal_attach"
? {
sessionId: "session-1",
subscriptionId: "subscription-1",
viewport: { columns: 80, generation: 0, screenLines: 24 },
}
: undefined,
);
},
transformCallback(callback) {
const id = nextCallback++;
callbacks.set(id, callback);
return id;
},
unregisterCallback(id) {
callbacks.delete(id);
},
},
isTauri: true,
};
const { TerminalConnection } = await import("./terminalClient.ts");
function emit(channel, message, index = 0) {
const id = Number(channel.toJSON().slice("__CHANNEL__:".length));
callbacks.get(id)({ index, message });
}
const request = {
channelId: "channel-1",
channelName: "general",
npub: "npub1test",
relayUrl: "wss://relay.example",
columns: 80,
rows: 24,
pixelWidth: 800,
pixelHeight: 408,
};
const frame = {
type: "frame",
payload: {
subscriptionId: "subscription-1",
sequence: 7,
rows: [],
cursor: { column: 0, line: 0, visible: true },
full: true,
viewport: { columns: 80, generation: 0, screenLines: 24 },
bracketedPaste: true,
focusReporting: true,
},
};
afterEach(() => {
calls.length = 0;
attachDuringInvoke = undefined;
});
test("attach retains a bootstrap frame delivered before invoke resolves", async () => {
const deliveries = [];
attachDuringInvoke = ({ onFrame }) => emit(onFrame, frame);
const connection = await TerminalConnection.attach(
request,
() => {},
(delivery) => deliveries.push(delivery),
);
assert.equal(connection.sessionId, "session-1");
assert.equal(deliveries.length, 1);
assert.equal(deliveries[0].frame.sequence, 7);
await deliveries[0].acknowledge();
assert.deepEqual(calls.at(-1), {
command: "terminal_ack",
args: {
sessionId: "session-1",
subscriptionId: "subscription-1",
sequence: 7,
},
});
});
test("stale subscription frames are ignored", async () => {
const deliveries = [];
let channel;
attachDuringInvoke = ({ onFrame }) => {
channel = onFrame;
};
await TerminalConnection.attach(
request,
() => {},
(value) => deliveries.push(value),
);
emit(
channel,
{ ...frame, payload: { ...frame.payload, subscriptionId: "stale" } },
0,
);
assert.deepEqual(deliveries, []);
});
@@ -0,0 +1,141 @@
import { Channel, invoke, isTauri } from "@tauri-apps/api/core";
import type { TerminalFrame } from "./terminalRenderer";
export type TerminalAttachRequest = {
sessionId?: string;
channelId: string;
channelName: string;
threadId?: string;
npub: string;
relayUrl: string;
columns: number;
rows: number;
pixelWidth: number;
pixelHeight: number;
};
export type TerminalViewport = TerminalFrame["viewport"];
export type TerminalFrameMessage = TerminalFrame & {
subscriptionId: string;
sequence: number;
bracketedPaste: boolean;
focusReporting: boolean;
};
export type TerminalMessage =
| { type: "frame"; payload: TerminalFrameMessage }
| { type: "title"; payload: string }
| { type: "resetTitle"; payload?: never }
| { type: "bell"; payload?: never }
| { type: "exit"; payload?: never };
type AttachResponse = {
sessionId: string;
subscriptionId: string;
viewport: TerminalViewport;
};
export type TerminalDelivery = {
frame: TerminalFrameMessage;
acknowledge: () => Promise<void>;
};
export class TerminalConnection {
readonly sessionId: string;
readonly subscriptionId: string;
readonly viewport: TerminalViewport;
private constructor(response: AttachResponse) {
this.sessionId = response.sessionId;
this.subscriptionId = response.subscriptionId;
this.viewport = response.viewport;
}
static async attach(
request: TerminalAttachRequest,
onMessage: (message: Exclude<TerminalMessage, { type: "frame" }>) => void,
onFrame: (delivery: TerminalDelivery) => void,
): Promise<TerminalConnection> {
if (!isTauri()) throw new Error("terminal sessions require Buzz Desktop");
let connection: TerminalConnection | null = null;
const pending: TerminalMessage[] = [];
const deliver = (message: TerminalMessage) => {
if (!connection) {
pending.push(message);
return;
}
if (message.type !== "frame") {
onMessage(message);
return;
}
const frame = message.payload;
if (frame.subscriptionId !== connection.subscriptionId) return;
onFrame({
frame,
acknowledge: () =>
connection?.acknowledge(frame.sequence) ?? Promise.resolve(),
});
};
const channel = new Channel<TerminalMessage>(deliver);
const response = await invoke<AttachResponse>("terminal_attach", {
request,
onFrame: channel,
});
connection = new TerminalConnection(response);
for (const message of pending) deliver(message);
return connection;
}
input(data: string): Promise<void> {
return invoke("terminal_input", { sessionId: this.sessionId, data });
}
async resize(
columns: number,
rows: number,
pixelWidth: number,
pixelHeight: number,
): Promise<TerminalViewport> {
return invoke("terminal_resize", {
sessionId: this.sessionId,
columns,
rows,
pixelWidth,
pixelHeight,
});
}
viewportReady(viewport: TerminalViewport): Promise<void> {
return invoke("terminal_viewport_ready", {
sessionId: this.sessionId,
subscriptionId: this.subscriptionId,
viewport,
});
}
focus(focused: boolean): Promise<void> {
return invoke("terminal_focus", { sessionId: this.sessionId, focused });
}
detach(): Promise<void> {
return invoke("terminal_detach", {
sessionId: this.sessionId,
subscriptionId: this.subscriptionId,
});
}
close(): Promise<void> {
return invoke("terminal_close", { sessionId: this.sessionId });
}
private acknowledge(sequence: number): Promise<void> {
return invoke("terminal_ack", {
sessionId: this.sessionId,
subscriptionId: this.subscriptionId,
sequence,
});
}
}