From 0e2db6e51ea9620f7de9febaa4e923496d4ff4cc Mon Sep 17 00:00:00 2001 From: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Date: Sun, 2 Aug 2026 16:43:02 -0400 Subject: [PATCH] fix(desktop): preserve terminal mode while tab attaches Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> --- .../terminal/TerminalBootstrap.test.mjs | 58 ++++++++++++++++++- .../features/terminal/TerminalBootstrap.tsx | 2 +- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/terminal/TerminalBootstrap.test.mjs b/desktop/src/features/terminal/TerminalBootstrap.test.mjs index 77bcabd00..fb660cf56 100644 --- a/desktop/src/features/terminal/TerminalBootstrap.test.mjs +++ b/desktop/src/features/terminal/TerminalBootstrap.test.mjs @@ -130,7 +130,7 @@ test("mounted bootstrap passes GUI context and ACKs only after consuming a frame const { ThemeProvider } = await import("@/shared/theme/ThemeProvider"); const { TerminalBootstrap } = await import("./TerminalBootstrap.tsx"); - render( + const view = render( createElement( StrictMode, null, @@ -205,6 +205,7 @@ test("mounted bootstrap passes GUI context and ACKs only after consuming a frame calls.filter(({ command }) => command === "terminal_ack").length, 1, ); + view.unmount(); }); test("resize during in-flight catch-up keeps the newest viewport ready", async () => { const { createElement } = await import("react"); @@ -262,3 +263,58 @@ test("resize during in-flight catch-up keeps the newest viewport ready", async ( assert.equal(readyCalls.at(-1).args.viewport.columns, 200); view.unmount(); }); + +test("opening a tab keeps terminal ownership while its attachment is pending", async () => { + const { createElement } = await import("react"); + const { act, fireEvent, render, waitFor } = await import( + "@testing-library/react" + ); + const { ThemeProvider } = await import("@/shared/theme/ThemeProvider"); + const { TerminalBootstrap } = await import("./TerminalBootstrap.tsx"); + + const view = render( + createElement( + ThemeProvider, + null, + createElement("div", { + className: "buzz-huddle-app-surface", + tabIndex: -1, + }), + createElement(TerminalBootstrap, { + channelId: "channel-1", + channelName: "general", + npub: "npub1owner", + relayUrl: "wss://relay.example", + threadId: null, + }), + ), + ); + await waitFor(() => + assert.ok(calls.some(({ command }) => command === "terminal_attach")), + ); + await act(async () => { + await Promise.resolve(); + }); + const substrate = view.container.querySelector(".buzz-terminal-substrate"); + const chord = { bubbles: true, code: "KeyJ", metaKey: true }; + act(() => { + window.dispatchEvent(new KeyboardEvent("keydown", chord)); + window.dispatchEvent(new KeyboardEvent("keyup", chord)); + }); + await waitFor(() => + assert.equal(substrate.dataset.terminalOwner, "terminal"), + ); + + attachResolver = () => {}; + fireEvent.click(view.getByLabelText("New Buzz Term tab")); + await waitFor(() => assert.equal(typeof attachResolver, "function")); + assert.equal( + substrate.dataset.terminalOwner, + "terminal", + "an attaching session must not force the substrate back to Buzz", + ); + assert.equal(view.getAllByRole("tab").length, 2); + + await act(async () => attachResolver()); + view.unmount(); +}); diff --git a/desktop/src/features/terminal/TerminalBootstrap.tsx b/desktop/src/features/terminal/TerminalBootstrap.tsx index 1649b067a..b231a278e 100644 --- a/desktop/src/features/terminal/TerminalBootstrap.tsx +++ b/desktop/src/features/terminal/TerminalBootstrap.tsx @@ -209,7 +209,7 @@ export function TerminalBootstrap({