Merge terminal close completion

* commit '477f7721a':
  fix(desktop): complete terminal tab close

Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
This commit is contained in:
npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta
2026-08-02 21:57:22 -04:00
3 changed files with 47 additions and 5 deletions
+6 -4
View File
@@ -277,12 +277,11 @@ impl Session {
}
fn shutdown(mut self) {
if let Ok(mut channel) = self.channel.lock() {
*channel = None;
}
// Publication is detached before the reader enters close mode; the
// lifecycle helper then abandons parser work and keeps raw-draining
// through child termination and reap.
// through child termination and reap. Keep the renderer channel alive
// until the reader has reported Exit; close() removes the session from
// the runtime before shutdown begins, so this is its final message.
if let Ok(mut publisher) = self.publisher.lock() {
publisher.close();
}
@@ -300,6 +299,9 @@ impl Session {
reader.join();
}
}
if let Ok(mut channel) = self.channel.lock() {
*channel = None;
}
}
}
@@ -319,6 +319,43 @@ test("opening a tab keeps terminal ownership while its attachment is pending", a
view.unmount();
});
test("a successful close removes the tab even if the exit event is lost", async () => {
const { createElement } = await import("react");
const { 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 waitFor(() => assert.ok(view.queryByRole("tab", { name: /SHELL/ })));
fireEvent.click(view.getByLabelText("Close SHELL"));
await waitFor(() =>
assert.ok(calls.some(({ command }) => command === "terminal_close")),
);
await waitFor(() => assert.equal(view.queryByRole("tab"), null));
view.unmount();
});
// The wheel-to-IPC path end to end. `TerminalSubstrate` already proves it
// accumulates pixels into whole cells and `buzz-terminal` already proves which
// way the engine goes; the seam between them is this file's business, and the
@@ -228,7 +228,10 @@ export function TerminalBootstrap({
removeSession(key);
return;
}
void connection.close().catch(fail);
void connection
.close()
.then(() => removeSession(key))
.catch(fail);
}}
onFrameConsumed={(frame) => {
const delivery = sessionsRef.current.find(