Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c41114faf8 | |||
| c850f7603a |
@@ -762,13 +762,32 @@ export const TerminalView = memo(function TerminalView({
|
|||||||
refit();
|
refit();
|
||||||
};
|
};
|
||||||
document.fonts?.addEventListener("loadingdone", onFontLoaded);
|
document.fonts?.addEventListener("loadingdone", onFontLoaded);
|
||||||
// The browser may drop or reuse the WebGL glyph atlas's GPU memory without
|
// @xterm/addon-webgl 0.19 shares ONE glyph atlas between every terminal
|
||||||
// a context-loss event (sleep/resume, a long-backgrounded tab, a display or
|
// with the same font, and when it fills up it merges four pages into one
|
||||||
// zoom change on macOS): xterm keeps slicing the old coordinates and every
|
// larger page and shifts the rest down. A GPU texture is re-uploaded only
|
||||||
// cell shows a clipped or foreign glyph while the buffer stays correct.
|
// when its page's `version` differs from the uploaded one, and after a
|
||||||
// Rebuilding the atlas when the page comes back is cheap and redraws clean.
|
// shift those counters can coincide, so a slot keeps a stale page of
|
||||||
|
// another size: every glyph is sampled squeezed or foreign while the
|
||||||
|
// buffer stays correct (fixed upstream only in 0.20 betas). Any option
|
||||||
|
// change makes each renderer re-acquire its atlas and re-upload all pages,
|
||||||
|
// so this toggles the family's trailing space (as onFontLoaded does) on
|
||||||
|
// a merge. `clearTextureAtlas` is no fix: it empties the SHARED atlas
|
||||||
|
// but redraws only this pane, leaving its siblings slicing reused space.
|
||||||
|
let atlasFrame = 0;
|
||||||
|
const rebuildAtlas = () => {
|
||||||
|
if (atlasFrame) return;
|
||||||
|
// Never inside the render that is rasterizing the merging glyph.
|
||||||
|
atlasFrame = requestAnimationFrame(() => {
|
||||||
|
atlasFrame = 0;
|
||||||
|
const family = term.options.fontFamily ?? "";
|
||||||
|
term.options.fontFamily = family.endsWith(" ") ? family.trimEnd() : `${family} `;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const atlasMerged = gpu?.onRemoveTextureAtlasCanvas(rebuildAtlas);
|
||||||
|
// The browser may also drop the textures without a context-loss event
|
||||||
|
// (sleep/resume, a long-backgrounded tab): rebuild when the page is back.
|
||||||
const onPageShown = () => {
|
const onPageShown = () => {
|
||||||
if (document.visibilityState === "visible") term.clearTextureAtlas();
|
if (document.visibilityState === "visible") rebuildAtlas();
|
||||||
};
|
};
|
||||||
document.addEventListener("visibilitychange", onPageShown);
|
document.addEventListener("visibilitychange", onPageShown);
|
||||||
window.addEventListener("focus", onPageShown);
|
window.addEventListener("focus", onPageShown);
|
||||||
@@ -790,6 +809,8 @@ export const TerminalView = memo(function TerminalView({
|
|||||||
searchRef.current = null;
|
searchRef.current = null;
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
document.fonts?.removeEventListener("loadingdone", onFontLoaded);
|
document.fonts?.removeEventListener("loadingdone", onFontLoaded);
|
||||||
|
if (atlasFrame) cancelAnimationFrame(atlasFrame);
|
||||||
|
atlasMerged?.dispose();
|
||||||
document.removeEventListener("visibilitychange", onPageShown);
|
document.removeEventListener("visibilitychange", onPageShown);
|
||||||
window.removeEventListener("focus", onPageShown);
|
window.removeEventListener("focus", onPageShown);
|
||||||
window.removeEventListener(FONT_SIZE_EVENT, onFontPref);
|
window.removeEventListener(FONT_SIZE_EVENT, onFontPref);
|
||||||
|
|||||||
@@ -112,12 +112,15 @@ vi.mock("@xterm/addon-fit", () => ({
|
|||||||
FitAddon: vi.fn().mockImplementation(() => ({ fit: vi.fn() })),
|
FitAddon: vi.fn().mockImplementation(() => ({ fit: vi.fn() })),
|
||||||
}));
|
}));
|
||||||
// The newest WebGL addon, with the context-loss listener the view gave it.
|
// The newest WebGL addon, with the context-loss listener the view gave it.
|
||||||
const gpu: { last: { dispose: ReturnType<typeof vi.fn>; lose: () => void } | null } = {
|
const gpu: {
|
||||||
|
last: { dispose: ReturnType<typeof vi.fn>; lose: () => void; merge: () => void } | null;
|
||||||
|
} = {
|
||||||
last: null,
|
last: null,
|
||||||
};
|
};
|
||||||
vi.mock("@xterm/addon-webgl", () => ({
|
vi.mock("@xterm/addon-webgl", () => ({
|
||||||
WebglAddon: vi.fn().mockImplementation(() => {
|
WebglAddon: vi.fn().mockImplementation(() => {
|
||||||
let onLoss = () => {};
|
let onLoss = () => {};
|
||||||
|
let onMerge = () => {};
|
||||||
const addon = {
|
const addon = {
|
||||||
__webgl: true,
|
__webgl: true,
|
||||||
dispose: vi.fn(),
|
dispose: vi.fn(),
|
||||||
@@ -125,8 +128,12 @@ vi.mock("@xterm/addon-webgl", () => ({
|
|||||||
onLoss = fn;
|
onLoss = fn;
|
||||||
return { dispose: vi.fn() };
|
return { dispose: vi.fn() };
|
||||||
},
|
},
|
||||||
|
onRemoveTextureAtlasCanvas: (fn: () => void) => {
|
||||||
|
onMerge = fn;
|
||||||
|
return { dispose: vi.fn() };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
gpu.last = { dispose: addon.dispose, lose: () => onLoss() };
|
gpu.last = { dispose: addon.dispose, lose: () => onLoss(), merge: () => onMerge() };
|
||||||
return addon;
|
return addon;
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
@@ -488,6 +495,22 @@ describe("TerminalView", () => {
|
|||||||
expect(disposeMock).not.toHaveBeenCalled(); // the terminal itself lives on
|
expect(disposeMock).not.toHaveBeenCalled(); // the terminal itself lives on
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("re-acquires the glyph atlas on the next frame after a page merge", () => {
|
||||||
|
// addon-webgl 0.19 may keep a stale page in a GPU slot after it merges
|
||||||
|
// atlas pages; any option change makes it re-upload every page.
|
||||||
|
const raf = vi.spyOn(window, "requestAnimationFrame").mockImplementation(() => 1);
|
||||||
|
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||||
|
const opts = termOptions as { fontFamily?: string };
|
||||||
|
opts.fontFamily = "JetBrains Mono";
|
||||||
|
gpu.last!.merge();
|
||||||
|
gpu.last!.merge(); // coalesced into one frame
|
||||||
|
expect(raf).toHaveBeenCalledTimes(1);
|
||||||
|
expect(opts.fontFamily).toBe("JetBrains Mono"); // never inside the render that merged
|
||||||
|
(raf.mock.calls[0]![0] as FrameRequestCallback)(0);
|
||||||
|
expect(opts.fontFamily).toBe("JetBrains Mono ");
|
||||||
|
raf.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
// xterm 6 dropped its own Alt+arrow → word-move mapping; the pane keeps it.
|
// xterm 6 dropped its own Alt+arrow → word-move mapping; the pane keeps it.
|
||||||
it("sends Alt+arrow as the word-move keys xterm 5 sent", () => {
|
it("sends Alt+arrow as the word-move keys xterm 5 sent", () => {
|
||||||
expect(altArrowSeq("ArrowLeft", false)).toBe("\x1b[1;5D");
|
expect(altArrowSeq("ArrowLeft", false)).toBe("\x1b[1;5D");
|
||||||
|
|||||||
@@ -7316,7 +7316,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-status-warning/90"
|
class="text-status-warning/90"
|
||||||
>
|
>
|
||||||
UI build v2.8.41 (rebuild client to match)
|
UI build v2.8.42 (rebuild client to match)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.41",
|
"version": "2.8.42",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.41",
|
"version": "2.8.42",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.41",
|
"version": "2.8.42",
|
||||||
"bin": {
|
"bin": {
|
||||||
"agenthub": "bin/agenthub.js"
|
"agenthub": "bin/agenthub.js"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user