Skip to content

Commit ad86ed7

Browse files
Merge pull request #1442 from corbitsdev/cl-10647-tui-staged-bump-opentuicore-0512-0517-064
fix(tui): bump @opentui/core packages to 0.6.4
2 parents 92b3dc4 + 0c8b281 commit ad86ed7

15 files changed

Lines changed: 437 additions & 111 deletions

‎bun.lock‎

Lines changed: 18 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
"@intx/types": "workspace:*",
106106
"@intx/workflow-host": "0.3.0",
107107
"@modelcontextprotocol/sdk": "^1.29.0",
108-
"@opentui/core": "0.5.12",
108+
"@opentui/core": "0.6.4",
109109
"arktype": "catalog:",
110110
"isomorphic-git": "catalog:",
111111
"@corbits/code-agent-designer": "workspace:*",
@@ -125,13 +125,13 @@
125125
"ws": "^8.21.0"
126126
},
127127
"optionalDependencies": {
128-
"@opentui/core-darwin-arm64": "0.5.12",
129-
"@opentui/core-darwin-x64": "0.5.12",
130-
"@opentui/core-linux-arm64": "0.5.12",
131-
"@opentui/core-linux-arm64-musl": "0.5.12",
132-
"@opentui/core-linux-x64": "0.5.12",
133-
"@opentui/core-linux-x64-musl": "0.5.12",
134-
"@opentui/core-win32-arm64": "0.5.12",
135-
"@opentui/core-win32-x64": "0.5.12"
128+
"@opentui/core-darwin-arm64": "0.6.4",
129+
"@opentui/core-darwin-x64": "0.6.4",
130+
"@opentui/core-linux-arm64": "0.6.4",
131+
"@opentui/core-linux-arm64-musl": "0.6.4",
132+
"@opentui/core-linux-x64": "0.6.4",
133+
"@opentui/core-linux-x64-musl": "0.6.4",
134+
"@opentui/core-win32-arm64": "0.6.4",
135+
"@opentui/core-win32-x64": "0.6.4"
136136
}
137137
}

‎src/tui/runner/settings-theme.test.ts‎

Lines changed: 136 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,43 @@
1111
*/
1212

1313
import { afterEach, describe, expect, test } from "bun:test";
14-
import { RGBA, type CapturedSpan } from "@opentui/core";
14+
import { RGBA, SyntaxStyle, type CapturedSpan } from "@opentui/core";
1515

1616
import { corbitsDark, corbitsLight, setTheme, UI } from "../theme.js";
1717
import { transcriptSyntaxStyle } from "../stream.js";
1818
import { withTestRenderer, type Harness } from "../harness.js";
19+
import { settleSpans } from "../test-helpers.js";
1920
import { appendStreamRow } from "../shell/chrome.js";
2021
import { createAppShell } from "../shell/index.js";
22+
import { setPromptRecognitionSource } from "../shell/internals.js";
23+
import { syncPromptHighlights } from "../shell/prompt.js";
2124
import { applyThemePinLive, repaintShellForTheme } from "./settings.js";
2225

2326
afterEach(() => {
2427
setTheme("corbits-dark");
2528
});
2629

2730
/** Live heading colour in the transcript registry. */
28-
function registryHeadingFg(): RGBA | undefined {
29-
return transcriptSyntaxStyle().getAllStyles().get("markup.heading.1")?.fg;
31+
function registryHeadingFg(h: Harness): RGBA | undefined {
32+
return transcriptSyntaxStyle(h.renderer)
33+
.getAllStyles()
34+
.get("markup.heading.1")?.fg;
3035
}
3136

32-
function painted(spans: CapturedSpan[], text: string, fg: RGBA): boolean {
33-
return spans.some((s) => s.text.includes(text) && s.fg.equals(fg));
37+
/** `destroy()` frees the native style and there is no finalizer; a later
38+
* read throws instead of returning a palette. */
39+
function syntaxStyleAlive(style: SyntaxStyle): boolean {
40+
try {
41+
style.getStyleCount();
42+
return true;
43+
} catch (err) {
44+
if (err instanceof Error && err.message.includes("destroyed")) return false;
45+
throw err;
46+
}
3447
}
3548

36-
async function settleSpans(
37-
h: Harness,
38-
ready: (spans: CapturedSpan[]) => boolean,
39-
): Promise<CapturedSpan[]> {
40-
let spans = h.captureSpans().lines.flatMap((line) => line.spans);
41-
for (let i = 0; i < 400 && !ready(spans); i++) {
42-
await h.renderOnce();
43-
await new Promise((resolve) => setTimeout(resolve, 25));
44-
spans = h.captureSpans().lines.flatMap((line) => line.spans);
45-
}
46-
return spans;
49+
function painted(spans: CapturedSpan[], text: string, fg: RGBA): boolean {
50+
return spans.some((s) => s.text.includes(text) && s.fg.equals(fg));
4751
}
4852

4953
describe("applyThemePinLive", () => {
@@ -64,7 +68,7 @@ describe("applyThemePinLive", () => {
6468
painted(spans, "pin cycle", darkHeading),
6569
);
6670
expect(painted(before, "pin cycle", darkHeading)).toBe(true);
67-
expect(registryHeadingFg()?.equals(darkHeading)).toBe(true);
71+
expect(registryHeadingFg(h)?.equals(darkHeading)).toBe(true);
6872

6973
// No cost movement anywhere in this cycle: the meter never moves,
7074
// so only an unconditional repaint repaints anything.
@@ -74,7 +78,7 @@ describe("applyThemePinLive", () => {
7478
expect(applied).toBe("corbits-light");
7579
expect(UI.name).toBe("corbits-light");
7680
expect(UI.ground).not.toBe(darkGround);
77-
expect(registryHeadingFg()?.equals(lightHeading)).toBe(true);
81+
expect(registryHeadingFg(h)?.equals(lightHeading)).toBe(true);
7882

7983
const after = await settleSpans(h, (spans) =>
8084
painted(spans, "pin cycle", lightHeading),
@@ -106,14 +110,14 @@ describe("applyThemePinLive", () => {
106110
painted(spans, "pin cycle", lightHeading),
107111
);
108112
expect(painted(before, "pin cycle", lightHeading)).toBe(true);
109-
expect(registryHeadingFg()?.equals(lightHeading)).toBe(true);
113+
expect(registryHeadingFg(h)?.equals(lightHeading)).toBe(true);
110114

111115
const applied = applyThemePinLive("dark", () =>
112116
repaintShellForTheme(shell),
113117
);
114118
expect(applied).toBe("corbits-dark");
115119
expect(UI.name).toBe("corbits-dark");
116-
expect(registryHeadingFg()?.equals(darkHeading)).toBe(true);
120+
expect(registryHeadingFg(h)?.equals(darkHeading)).toBe(true);
117121

118122
const after = await settleSpans(h, (spans) =>
119123
painted(spans, "pin cycle", darkHeading),
@@ -129,11 +133,118 @@ describe("applyThemePinLive", () => {
129133
);
130134
}, 30000);
131135

132-
test("auto re-resolves through the startup path and rebuilds the registry", () => {
136+
test("auto re-resolves through the startup path and rebuilds the registry", async () => {
133137
setTheme("corbits-dark");
134-
const applied = applyThemePinLive("auto", () => undefined);
135-
expect(["corbits-dark", "corbits-light"]).toContain(applied);
136-
expect(UI.name).toBe(applied);
137-
expect(registryHeadingFg()?.equals(RGBA.fromHex(UI.heading))).toBe(true);
138+
await withTestRenderer(async (h) => {
139+
const applied = applyThemePinLive("auto", () => undefined);
140+
expect(["corbits-dark", "corbits-light"]).toContain(applied);
141+
expect(UI.name).toBe(applied);
142+
expect(registryHeadingFg(h)?.equals(RGBA.fromHex(UI.heading))).toBe(true);
143+
});
144+
});
145+
146+
test("a second pin destroys the transcript style the rows dropped", async () => {
147+
await withTestRenderer(
148+
async (h) => {
149+
setTheme("corbits-dark");
150+
const shell = createAppShell(h.renderer, {
151+
terminal: { columns: 80, rows: 40 },
152+
wireKeys: false,
153+
});
154+
try {
155+
appendStreamRow(shell, { role: "assistant", text: "# pin cycle" });
156+
const first = transcriptSyntaxStyle(h.renderer);
157+
applyThemePinLive("light", () => repaintShellForTheme(shell));
158+
const second = transcriptSyntaxStyle(h.renderer);
159+
expect(second).not.toBe(first);
160+
expect(syntaxStyleAlive(first)).toBe(false);
161+
expect(syntaxStyleAlive(second)).toBe(true);
162+
163+
applyThemePinLive("dark", () => repaintShellForTheme(shell));
164+
const third = transcriptSyntaxStyle(h.renderer);
165+
expect(third).not.toBe(second);
166+
expect(syntaxStyleAlive(second)).toBe(false);
167+
expect(syntaxStyleAlive(third)).toBe(true);
168+
} finally {
169+
shell.dispose();
170+
setTheme("corbits-dark");
171+
}
172+
},
173+
{ width: 80, height: 40 },
174+
);
175+
});
176+
177+
test("refreshing the transcript cache leaves a style rows still paint", async () => {
178+
await withTestRenderer(
179+
async (h) => {
180+
setTheme("corbits-dark");
181+
const shell = createAppShell(h.renderer, {
182+
terminal: { columns: 80, rows: 40 },
183+
wireKeys: false,
184+
});
185+
try {
186+
appendStreamRow(shell, { role: "assistant", text: "# pin cycle" });
187+
const painted = transcriptSyntaxStyle(h.renderer);
188+
setTheme("corbits-light");
189+
const rebuilt = transcriptSyntaxStyle(h.renderer);
190+
expect(rebuilt).not.toBe(painted);
191+
expect(syntaxStyleAlive(painted)).toBe(true);
192+
repaintShellForTheme(shell);
193+
expect(syntaxStyleAlive(painted)).toBe(false);
194+
expect(syntaxStyleAlive(transcriptSyntaxStyle(h.renderer))).toBe(
195+
true,
196+
);
197+
} finally {
198+
shell.dispose();
199+
setTheme("corbits-dark");
200+
}
201+
},
202+
{ width: 80, height: 40 },
203+
);
204+
});
205+
206+
test("a pin destroys the prompt style only after the prompt drops it", async () => {
207+
await withTestRenderer(
208+
async (h) => {
209+
setTheme("corbits-dark");
210+
const shell = createAppShell(h.renderer, {
211+
terminal: { columns: 80, rows: 40 },
212+
wireKeys: false,
213+
});
214+
setPromptRecognitionSource(shell, () => ({
215+
commandNames: ["implement"],
216+
}));
217+
const originalDestroy = SyntaxStyle.prototype.destroy;
218+
let destroyedWhileHeld = false;
219+
try {
220+
shell.prompt.value = "/implement";
221+
syncPromptHighlights(shell);
222+
const painted = shell.prompt.syntaxStyle;
223+
expect(painted).not.toBeNull();
224+
if (painted === null) return;
225+
SyntaxStyle.prototype.destroy = function destroy(this: SyntaxStyle) {
226+
if (this === painted && shell.prompt.syntaxStyle === painted) {
227+
destroyedWhileHeld = true;
228+
}
229+
return originalDestroy.call(this);
230+
};
231+
applyThemePinLive("light", () => repaintShellForTheme(shell));
232+
expect(shell.prompt.syntaxStyle).toBe(painted);
233+
expect(syntaxStyleAlive(painted)).toBe(true);
234+
syncPromptHighlights(shell);
235+
expect(destroyedWhileHeld).toBe(false);
236+
expect(shell.prompt.syntaxStyle).not.toBe(painted);
237+
expect(syntaxStyleAlive(painted)).toBe(false);
238+
const current = shell.prompt.syntaxStyle;
239+
expect(current).not.toBeNull();
240+
if (current !== null) expect(syntaxStyleAlive(current)).toBe(true);
241+
} finally {
242+
SyntaxStyle.prototype.destroy = originalDestroy;
243+
shell.dispose();
244+
setTheme("corbits-dark");
245+
}
246+
},
247+
{ width: 80, height: 40 },
248+
);
138249
});
139250
});

‎src/tui/shell/chrome.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ import {
143143
transcriptRowChildren,
144144
transcriptRowLayout,
145145
transcriptRowOffset,
146+
releaseRetiredTranscriptSyntaxStyles,
146147
} from "./transcript.js";
147148

148149
function syncPending(shell: AppShell): void {
@@ -1411,6 +1412,7 @@ export function repaintTranscriptWindow(shell: AppShell): void {
14111412
),
14121413
);
14131414
});
1415+
releaseRetiredTranscriptSyntaxStyles(shell);
14141416
}
14151417

14161418
/** Tear the landing down on the first transcript row. The prompt box jumps

‎src/tui/shell/internals.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ export type ShellRenderer = Pick<
284284
| "off"
285285
| "isDestroyed"
286286
| "clearSelection"
287+
// 0.6.x native style construction needs the owning Context: the renderer's
288+
// native scene.
289+
| "nativeScene"
287290
>;
288291

289292
export interface AppShellOptions {

0 commit comments

Comments
 (0)