Skip to content

Commit 4958d68

Browse files
committed
fix(tui): bump @opentui/core packages to 0.6.4
1 parent 282acc8 commit 4958d68

12 files changed

Lines changed: 216 additions & 89 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.17",
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.17",
129-
"@opentui/core-darwin-x64": "0.5.17",
130-
"@opentui/core-linux-arm64": "0.5.17",
131-
"@opentui/core-linux-arm64-musl": "0.5.17",
132-
"@opentui/core-linux-x64": "0.5.17",
133-
"@opentui/core-linux-x64-musl": "0.5.17",
134-
"@opentui/core-win32-arm64": "0.5.17",
135-
"@opentui/core-win32-x64": "0.5.17"
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: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,38 @@ afterEach(() => {
2525
});
2626

2727
/** Live heading colour in the transcript registry. */
28-
function registryHeadingFg(): RGBA | undefined {
29-
return transcriptSyntaxStyle().getAllStyles().get("markup.heading.1")?.fg;
28+
function registryHeadingFg(h: Harness): RGBA | undefined {
29+
return transcriptSyntaxStyle(h.renderer)
30+
.getAllStyles()
31+
.get("markup.heading.1")?.fg;
3032
}
3133

3234
function painted(spans: CapturedSpan[], text: string, fg: RGBA): boolean {
3335
return spans.some((s) => s.text.includes(text) && s.fg.equals(fg));
3436
}
3537

38+
/** 0.6.x leases a session buffer around each presented frame; a capture
39+
* landing inside that window throws OutputBusy. The poll retries on the
40+
* next round anyway, so a losing capture keeps polling instead of
41+
* throwing — the ready predicate, never the trace, decides the outcome. */
42+
function readSpans(h: Harness): CapturedSpan[] {
43+
try {
44+
return h.captureSpans().lines.flatMap((line) => line.spans);
45+
} catch (err) {
46+
if (err instanceof Error && err.message.includes("OutputBusy")) return [];
47+
throw err;
48+
}
49+
}
50+
3651
async function settleSpans(
3752
h: Harness,
3853
ready: (spans: CapturedSpan[]) => boolean,
3954
): Promise<CapturedSpan[]> {
40-
let spans = h.captureSpans().lines.flatMap((line) => line.spans);
55+
let spans = readSpans(h);
4156
for (let i = 0; i < 400 && !ready(spans); i++) {
4257
await h.renderOnce();
4358
await new Promise((resolve) => setTimeout(resolve, 25));
44-
spans = h.captureSpans().lines.flatMap((line) => line.spans);
59+
spans = readSpans(h);
4560
}
4661
return spans;
4762
}
@@ -64,7 +79,7 @@ describe("applyThemePinLive", () => {
6479
painted(spans, "pin cycle", darkHeading),
6580
);
6681
expect(painted(before, "pin cycle", darkHeading)).toBe(true);
67-
expect(registryHeadingFg()?.equals(darkHeading)).toBe(true);
82+
expect(registryHeadingFg(h)?.equals(darkHeading)).toBe(true);
6883

6984
// No cost movement anywhere in this cycle: the meter never moves,
7085
// so only an unconditional repaint repaints anything.
@@ -74,7 +89,7 @@ describe("applyThemePinLive", () => {
7489
expect(applied).toBe("corbits-light");
7590
expect(UI.name).toBe("corbits-light");
7691
expect(UI.ground).not.toBe(darkGround);
77-
expect(registryHeadingFg()?.equals(lightHeading)).toBe(true);
92+
expect(registryHeadingFg(h)?.equals(lightHeading)).toBe(true);
7893

7994
const after = await settleSpans(h, (spans) =>
8095
painted(spans, "pin cycle", lightHeading),
@@ -106,14 +121,14 @@ describe("applyThemePinLive", () => {
106121
painted(spans, "pin cycle", lightHeading),
107122
);
108123
expect(painted(before, "pin cycle", lightHeading)).toBe(true);
109-
expect(registryHeadingFg()?.equals(lightHeading)).toBe(true);
124+
expect(registryHeadingFg(h)?.equals(lightHeading)).toBe(true);
110125

111126
const applied = applyThemePinLive("dark", () =>
112127
repaintShellForTheme(shell),
113128
);
114129
expect(applied).toBe("corbits-dark");
115130
expect(UI.name).toBe("corbits-dark");
116-
expect(registryHeadingFg()?.equals(darkHeading)).toBe(true);
131+
expect(registryHeadingFg(h)?.equals(darkHeading)).toBe(true);
117132

118133
const after = await settleSpans(h, (spans) =>
119134
painted(spans, "pin cycle", darkHeading),
@@ -129,11 +144,13 @@ describe("applyThemePinLive", () => {
129144
);
130145
}, 30000);
131146

132-
test("auto re-resolves through the startup path and rebuilds the registry", () => {
147+
test("auto re-resolves through the startup path and rebuilds the registry", async () => {
133148
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);
149+
await withTestRenderer(async (h) => {
150+
const applied = applyThemePinLive("auto", () => undefined);
151+
expect(["corbits-dark", "corbits-light"]).toContain(applied);
152+
expect(UI.name).toBe(applied);
153+
expect(registryHeadingFg(h)?.equals(RGBA.fromHex(UI.heading))).toBe(true);
154+
});
138155
});
139156
});

‎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 {

‎src/tui/shell/prompt.ts‎

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
shellInternals,
4646
shellPromptImageSource,
4747
shellRecognitionSource,
48+
type ShellRenderer,
4849
} from "./internals.js";
4950
import {
5051
appendStreamRow,
@@ -177,35 +178,40 @@ export function setPromptCostContext(
177178
paintPromptBorder(shell);
178179
}
179180

180-
let cachedPromptSyntaxStyle: SyntaxStyle | null = null;
181-
182-
let cachedPromptRecognizedStyleId: number | null = null;
181+
/** 0.6.x native styles must name their owning Context — the live renderer's
182+
* native scene — so the style is keyed per renderer instead of a module
183+
* global; `promptStyleGeneration` invalidates it like the old cache did. */
184+
const promptStylesByRenderer = new WeakMap<
185+
ShellRenderer,
186+
{ generation: number; style: SyntaxStyle }
187+
>();
183188

184189
let promptStyleGeneration = 0;
185190

186191
const promptHighlightGenerations = new WeakMap<AppShell, number>();
187192

188193
onThemeChange(() => {
189-
cachedPromptSyntaxStyle = null;
190-
cachedPromptRecognizedStyleId = null;
191194
promptStyleGeneration += 1;
192195
});
193196

194197
/**
195198
* Lazy like `transcriptSyntaxStyle`: construction reaches into the native
196199
* render lib.
197200
*/
198-
function promptRecognizedStyleId(): number {
199-
if (cachedPromptSyntaxStyle === null) {
200-
cachedPromptSyntaxStyle = SyntaxStyle.fromStyles({
201-
recognized: { fg: UI.action },
202-
});
203-
}
204-
if (cachedPromptRecognizedStyleId === null) {
205-
cachedPromptRecognizedStyleId =
206-
cachedPromptSyntaxStyle.resolveStyleId("recognized") ?? 0;
201+
function promptStyleFor(renderer: ShellRenderer): SyntaxStyle {
202+
const cached = promptStylesByRenderer.get(renderer);
203+
if (cached !== undefined && cached.generation === promptStyleGeneration) {
204+
return cached.style;
207205
}
208-
return cachedPromptRecognizedStyleId;
206+
const style = SyntaxStyle.fromStyles(
207+
{ recognized: { fg: UI.action } },
208+
renderer.nativeScene,
209+
);
210+
promptStylesByRenderer.set(renderer, {
211+
generation: promptStyleGeneration,
212+
style,
213+
});
214+
return style;
209215
}
210216

211217
const promptHighlightedValue = new WeakMap<AppShell, string>();
@@ -226,8 +232,9 @@ export function syncPromptHighlights(shell: AppShell): void {
226232
promptHighlightedValue.set(shell, value);
227233
promptHighlightGenerations.set(shell, promptStyleGeneration);
228234

229-
const styleId = promptRecognizedStyleId();
230-
shell.prompt.syntaxStyle = cachedPromptSyntaxStyle;
235+
const style = promptStyleFor(shell.renderer);
236+
const styleId = style.resolveStyleId("recognized") ?? 0;
237+
shell.prompt.syntaxStyle = style;
231238
shell.prompt.clearAllHighlights();
232239
const matcher = resolvePromptRecognitionMatcher(source);
233240
for (const span of resolvePromptHighlightSpans(value, matcher)) {

‎src/tui/shell/transcript.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,13 @@ export function buildRowNode(
355355
}
356356

357357
/** Shared construction options for a transcript markdown body's renderer. */
358-
function markdownBodyOptions(gutter: PaintedStreamLine, width: number) {
358+
function markdownBodyOptions(
359+
ctx: CliRenderer,
360+
gutter: PaintedStreamLine,
361+
width: number,
362+
) {
359363
return {
360-
syntaxStyle: transcriptSyntaxStyle(),
364+
syntaxStyle: transcriptSyntaxStyle(ctx),
361365
fg: gutter.fg,
362366
width,
363367
flexShrink: 0,
@@ -430,22 +434,22 @@ function createMarkdownBody(
430434
if (split === null) {
431435
// Native incremental block stability: only the trailing block is unstable.
432436
return new MarkdownRenderable(ctx, {
433-
...markdownBodyOptions(gutter, width),
437+
...markdownBodyOptions(ctx, gutter, width),
434438
content,
435439
streaming: row.streaming === true,
436440
});
437441
}
438442
const column = new BoxRenderable(ctx, { flexDirection: "column", width });
439443
column.add(
440444
new MarkdownRenderable(ctx, {
441-
...markdownBodyOptions(gutter, width),
445+
...markdownBodyOptions(ctx, gutter, width),
442446
content: split.frozen,
443447
streaming: false,
444448
}),
445449
);
446450
column.add(
447451
new MarkdownRenderable(ctx, {
448-
...markdownBodyOptions(gutter, width),
452+
...markdownBodyOptions(ctx, gutter, width),
449453
content: split.live,
450454
streaming: row.streaming === true,
451455
marginTop: split.gapRows,

‎src/tui/slash-popup-gate.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,4 +1072,25 @@ describe("overlay host occupancy and opt-in deferral", () => {
10721072
expect(isOverlayHostIdle(shell)).toBe(true);
10731073
});
10741074
});
1075+
1076+
test("a `/` right after the Ctrl+C arm still opens the popup", async () => {
1077+
// Smoke suspicion (2026-10-14 TUI QA): a `/` typed immediately after the
1078+
// first Ctrl+C press seemed suppressed, letting the slash text submit to
1079+
// the agent. Both path stages share the key wiring, so the arm must not
1080+
// eat the key or leave the popup host unusable — this pins the sequence
1081+
// end to end through the real keybinding dispatch (idle shell, empty
1082+
// prompt, no live workers — the arming press, never the quit).
1083+
await withShell(async ({ shell, press, render }) => {
1084+
press("Ctrl+C");
1085+
await render();
1086+
expect(isSlashPopupOpen(shell)).toBe(false);
1087+
1088+
press("/");
1089+
await render();
1090+
expect(shell.prompt.value).toBe("/");
1091+
expect(isSlashPopupOpen(shell)).toBe(true);
1092+
// The unfiltered catalog stays listed (popup opens; nothing filtered).
1093+
expect(shell.paletteCommands[0]?.id).toBe("help");
1094+
});
1095+
});
10751096
});

‎src/tui/stream.test.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { toolCallRow } from "./diff";
1919
import { toolResultRow } from "./mcp-view";
2020
import { mergeToolRows } from "./tool-rows";
2121
import { UI } from "./theme";
22+
import { withTestRenderer } from "./harness";
2223

2324
const SOLO: RowLayout = { width: 56, multiAgent: false };
2425
const CREW: RowLayout = { width: 56, multiAgent: true };
@@ -577,9 +578,11 @@ describe("sub-agent dispatch row marks", () => {
577578
// underline-absence on the link scopes so a future hover style cannot
578579
// sneak one in.
579580
describe("transcriptSyntaxStyle markdown links", () => {
580-
test("link cells carry no underline", () => {
581-
const styles = transcriptSyntaxStyle().getAllStyles();
582-
expect(styles.get("markup.link")?.underline).not.toBe(true);
583-
expect(styles.get("markup.link.url")?.underline).not.toBe(true);
581+
test("link cells carry no underline", async () => {
582+
await withTestRenderer(async (h) => {
583+
const styles = transcriptSyntaxStyle(h.renderer).getAllStyles();
584+
expect(styles.get("markup.link")?.underline).not.toBe(true);
585+
expect(styles.get("markup.link.url")?.underline).not.toBe(true);
586+
});
584587
});
585588
});

0 commit comments

Comments
 (0)