feat: unify workspace routing through tab-based URLs and remove legacy per-type routes

This commit is contained in:
Mohamed Boudra
2026-03-03 21:14:15 +07:00
parent 74ac17f910
commit f3796e2ba5
25 changed files with 204 additions and 569 deletions
@@ -128,7 +128,10 @@ function createControllerHarness(input?: {
client,
getPreferredSize: () => ({ rows: 24, cols: 80 }),
onChunk: (chunk) => {
chunks.push(chunk);
chunks.push({
terminalId: chunk.terminalId,
text: chunk.text,
});
},
onStatusChange: (status) => {
statuses.push(status);
@@ -212,6 +215,7 @@ describe("terminal-stream-controller", () => {
expect(harness.client.attachCalls.length).toBe(2);
expect(harness.client.attachCalls[1]?.options).toEqual({
resumeOffset: 0,
rows: 24,
cols: 80,
});
@@ -268,6 +272,38 @@ describe("terminal-stream-controller", () => {
});
});
it("recovers when stream exits before initial attach completes", async () => {
const harness = createControllerHarness();
harness.client.nextAttachResponses.push({
streamId: 1,
currentOffset: 1200,
reset: false,
error: null,
});
harness.client.nextAttachResponses.push({
streamId: 2,
currentOffset: 1200,
reset: false,
error: null,
});
harness.controller.setTerminal({ terminalId: "term-attach-exit" });
harness.controller.handleStreamExit({
terminalId: "term-attach-exit",
streamId: 1,
});
await flushAsyncWork();
expect(harness.client.attachCalls).toHaveLength(2);
expect(harness.client.attachCalls[1]?.options).toEqual({
resumeOffset: 1200,
rows: 24,
cols: 80,
});
expect(harness.client.detachCalls).toContain(1);
expect(harness.controller.getActiveStreamId()).toBe(2);
});
it("emits reset callback when attach indicates output reset", async () => {
const harness = createControllerHarness();
harness.client.nextAttachResponses.push({
@@ -85,6 +85,7 @@ const DEFAULT_RECONNECT_ERROR_MESSAGE = "Terminal stream ended. Reconnecting…"
export class TerminalStreamController {
private readonly resumeOffsetByTerminalId = new Map<string, number>();
private readonly exitedStreamsWhileAttaching = new Set<number>();
private selectedTerminalId: string | null = null;
private activeStream: TerminalStreamControllerActiveStream | null = null;
private attachGeneration = 0;
@@ -167,31 +168,36 @@ export class TerminalStreamController {
},
});
const activeStream = this.activeStream;
if (!activeStream) {
return;
}
if (activeStream.terminalId !== input.terminalId || activeStream.streamId !== input.streamId) {
return;
}
if (this.selectedTerminalId !== input.terminalId) {
return;
}
this.attachGeneration += 1;
const generation = this.attachGeneration;
void this.detachActiveStream({ shouldDetach: false });
this.updateStatus({
terminalId: input.terminalId,
streamId: null,
isAttaching: true,
error:
this.options.reconnectErrorMessage ?? DEFAULT_RECONNECT_ERROR_MESSAGE,
});
void this.attachTerminal({
terminalId: input.terminalId,
generation,
});
const activeStream = this.activeStream;
const activeMatches =
Boolean(activeStream) &&
activeStream!.terminalId === input.terminalId &&
activeStream!.streamId === input.streamId;
if (activeMatches) {
this.attachGeneration += 1;
const generation = this.attachGeneration;
void this.detachActiveStream({ shouldDetach: false });
this.updateStatus({
terminalId: input.terminalId,
streamId: null,
isAttaching: true,
error:
this.options.reconnectErrorMessage ?? DEFAULT_RECONNECT_ERROR_MESSAGE,
});
void this.attachTerminal({
terminalId: input.terminalId,
generation,
});
return;
}
if (this.status.isAttaching && this.status.terminalId === input.terminalId) {
this.exitedStreamsWhileAttaching.add(input.streamId);
}
}
pruneResumeOffsets(input: { terminalIds: string[] }): void {
@@ -267,6 +273,12 @@ export class TerminalStreamController {
timeoutMessage: "Timed out attaching terminal stream",
});
updateTerminalResumeOffset({
terminalId: input.terminalId,
offset: attachPayload.currentOffset,
resumeOffsetByTerminalId: this.resumeOffsetByTerminalId,
});
if (!this.isAttachGenerationCurrent({ generation: input.generation, terminalId: input.terminalId })) {
if (typeof attachPayload.streamId === "number") {
void this.options.client.detachTerminalStream(attachPayload.streamId).catch(() => {});
@@ -300,17 +312,22 @@ export class TerminalStreamController {
return;
}
if (this.exitedStreamsWhileAttaching.has(attachPayload.streamId)) {
this.exitedStreamsWhileAttaching.delete(attachPayload.streamId);
void this.options.client.detachTerminalStream(attachPayload.streamId).catch(() => {});
this.attachGeneration += 1;
void this.attachTerminal({
terminalId: input.terminalId,
generation: this.attachGeneration,
});
return;
}
if (attachPayload.reset) {
this.resumeOffsetByTerminalId.delete(input.terminalId);
this.options.onReset?.({ terminalId: input.terminalId });
}
updateTerminalResumeOffset({
terminalId: input.terminalId,
offset: attachPayload.currentOffset,
resumeOffsetByTerminalId: this.resumeOffsetByTerminalId,
});
const decoder = new TextDecoder();
const streamId = attachPayload.streamId;
const replayedFromOffset =