fix(cli): patch @opentui-ui/dialog for opentui 0.4.x remove() contract (#12516)

@opentui-ui/dialog@0.1.2 is built against @opentui/core ^0.1.69, whose
Renderable.remove(id) took a string id. Core 0.4.x renamed it to
remove(child) and throws when handed anything but a renderable, so the
dialog package's removeDialog()/provider teardown aborted before
detaching the panel: the React portal content unmounted but the
imperative grey box stayed on screen over the chat after every dialog
close (model picker, help, command palette, ...).

The upstream package is abandoned at 0.1.2, so pin the fix with a bun
patch that passes the renderable object on all three bindings (react,
solid, core container). A tui-test opens and dismisses the help dialog
and asserts the panel's #262626 background is fully gone, not just its
text.

Fixes #12506

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
This commit is contained in:
Dominic Cooney
2026-07-24 15:18:15 +09:00
committed by GitHub
parent 359e771ab3
commit 98024c4243
4 changed files with 104 additions and 3 deletions
+58 -2
View File
@@ -17,14 +17,19 @@
// - Auto-approve all (Shift+Tab)
// ---------------------------------------------------------------------------
import { test } from "@microsoft/tui-test";
import { expect, test } from "@microsoft/tui-test";
import type { Terminal } from "@microsoft/tui-test/lib/terminal/term";
import { CLINE_BIN, TERMINAL_WIDE } from "../helpers/constants.js";
import { clineEnv } from "../helpers/env.js";
import {
toggleAutoApproveAll,
waitForChatReady,
} from "../helpers/page-objects/chat.js";
import { expectVisible } from "../helpers/terminal.js";
import {
expectNotVisible,
expectVisible,
typeAndSubmit,
} from "../helpers/terminal.js";
test.describe("cline (authenticated) - shows chat view", () => {
test.use({
@@ -53,3 +58,54 @@ test.describe("Auto-approve all - Shift+Tab toggle", () => {
await toggleAutoApproveAll(terminal);
});
});
test.describe("Dialog dismissal - panel is fully removed", () => {
test.use({
program: { file: CLINE_BIN, args: [] },
...TERMINAL_WIDE,
env: clineEnv("default"),
});
// The dialog panel's background is @opentui-ui/dialog's DEFAULT_STYLE
// (#262626), which xterm reports as this packed 24-bit color.
const DIALOG_PANEL_BG = 0x262626;
const countPanelCells = (terminal: Terminal): number => {
let count = 0;
for (const shift of terminal.serialize().shifts.values()) {
if (shift.bgColor === DIALOG_PANEL_BG) {
count++;
}
}
return count;
};
// @opentui-ui/dialog is built against @opentui/core ^0.1.69, whose
// Renderable.remove(id) took an id. Core 0.4.x renamed it to
// remove(child) and throws on a non-renderable argument, so the
// package's removeDialog() aborted before detaching its panel — the React
// portal content unmounted, but the imperative grey box stayed on screen
// over the chat. Asserting on the panel's background (not its text) is what
// distinguishes a leaked box from a clean teardown.
test("closing the help dialog removes its grey panel", async ({
terminal,
}) => {
await waitForChatReady(terminal);
await typeAndSubmit(terminal, "/help");
await expectVisible(terminal, "Keyboard Shortcuts");
expect(countPanelCells(terminal)).toBeGreaterThan(0);
terminal.keyEscape();
await expectNotVisible(terminal, "Keyboard Shortcuts");
// The panel unmounts a frame after its content; poll until the
// dialog's imperative box is detached rather than sampling once.
const deadline = Date.now() + 10_000;
let remaining = countPanelCells(terminal);
while (remaining > 0 && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 100));
remaining = countPanelCells(terminal);
}
expect(remaining).toBe(0);
});
});
+3
View File
@@ -772,6 +772,9 @@
"better-sqlite3",
"grpc-tools",
],
"patchedDependencies": {
"@opentui-ui/dialog@0.1.2": "patches/@opentui-ui%2Fdialog@0.1.2.patch",
},
"overrides": {
"@opentui/core": "0.4.3",
"@opentui/react": "0.4.3",
+4 -1
View File
@@ -86,5 +86,8 @@
"better-sqlite3",
"grpc-tools"
],
"packageManager": "bun@1.3.13"
"packageManager": "bun@1.3.13",
"patchedDependencies": {
"@opentui-ui/dialog@0.1.2": "patches/@opentui-ui%2Fdialog@0.1.2.patch"
}
}
+39
View File
@@ -0,0 +1,39 @@
diff --git a/dist/dialog-container-Btgzkwy7.mjs b/dist/dialog-container-Btgzkwy7.mjs
index fa1f29023479d5df2daf399c42f1d492365365aa..9a576b394c551269e6944bbb08fb43f7a7d3a8ca 100644
--- a/dist/dialog-container-Btgzkwy7.mjs
+++ b/dist/dialog-container-Btgzkwy7.mjs
@@ -677,7 +677,7 @@ var DialogContainerRenderable = class extends BoxRenderable {
const renderable = this._dialogRenderables.get(id);
if (renderable) {
this._dialogRenderables.delete(id);
- this.remove(renderable.id);
+ this.remove(renderable);
renderable.destroyRecursively();
this.updateBackdropVisibility();
this.updateBackdropStyle();
diff --git a/dist/react.mjs b/dist/react.mjs
index 157bd9c3ba3101810e02da720b8763425fd7b0fd..70bb55028ff815a82fea84205c61fb85d2aac03a 100644
--- a/dist/react.mjs
+++ b/dist/react.mjs
@@ -169,7 +169,7 @@ function DialogProvider(props) {
renderer.root.add(container);
return () => {
container.destroyRecursively();
- renderer.root.remove(container.id);
+ renderer.root.remove(container);
manager.destroy();
};
}, [
diff --git a/dist/solid.mjs b/dist/solid.mjs
index 2aea0ede350d79b5a8e533323fe70480bbd7f3ce..f075087a57c68875f341baa9ada67231fcc9cb10 100644
--- a/dist/solid.mjs
+++ b/dist/solid.mjs
@@ -192,7 +192,7 @@ function DialogProvider(props) {
unsubscribe();
portalItemCache.clear();
container.destroyRecursively();
- renderer.root.remove(container.id);
+ renderer.root.remove(container);
manager.destroy();
});
createEffect(() => {