fix(terminal): refit after fonts load

This commit is contained in:
Gabriel Brown
2026-07-10 17:06:33 -04:00
parent 0b5a2c0411
commit 75d06394ce
4 changed files with 114 additions and 10 deletions
@@ -0,0 +1,21 @@
export const scheduleTerminalFits = (deps: {
fit: () => void;
sendResize: () => void;
fontsReady: Promise<unknown>;
loadNerdFont: () => Promise<unknown>;
raf: (cb: () => void) => void;
isAborted: () => boolean;
}): void => {
const refit = () => {
if (deps.isAborted()) return;
deps.fit();
deps.sendResize();
};
deps.raf(refit);
void deps.fontsReady.then(refit).catch(() => undefined);
void deps
.loadNerdFont()
.then(refit)
.catch(() => undefined);
};
@@ -6,6 +6,8 @@ import { useTheme } from 'next-themes';
import { Button } from '@spoon/ui';
import { scheduleTerminalFits } from './terminal-fit';
import '@xterm/xterm/css/xterm.css';
const TERMINAL_FONT =
@@ -143,18 +145,8 @@ export const WorkspaceTerminal = ({
term.loadAddon(fit);
term.loadAddon(new WebLinksAddon());
term.open(container);
fit.fit();
termRef.current = term;
// Pull in the Nerd Font icon glyphs (loaded lazily by unicode-range) and
// repaint once ready so powerline/oh-my-posh/eza icons render.
void document.fonts
.load("16px 'Symbols Nerd Font Mono'", '\ue0b0')
.then(() => {
if (!isAborted()) term.refresh(0, term.rows - 1);
})
.catch(() => undefined);
const sendResize = () => {
if (ws?.readyState !== WebSocket.OPEN) return;
ws.send(
@@ -162,6 +154,26 @@ export const WorkspaceTerminal = ({
);
};
scheduleTerminalFits({
fit: () => {
try {
fit.fit();
} catch {
// ignore transient layout errors
}
},
sendResize,
fontsReady: document.fonts.ready,
loadNerdFont: () =>
document.fonts
.load("16px 'Symbols Nerd Font Mono'", '\ue0b0')
.then(() => {
if (!isAborted()) term.refresh(0, term.rows - 1);
}),
raf: (cb) => requestAnimationFrame(cb),
isAborted,
});
ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
+47
View File
@@ -0,0 +1,47 @@
import { scheduleTerminalFits } from '@/components/agent-workspace/terminal-fit';
import { describe, expect, test, vi } from 'vitest';
const flush = async () => {
await Promise.resolve();
await Promise.resolve();
};
describe('scheduleTerminalFits', () => {
test('fits in rAF and again after fonts.ready and nerd-font load', async () => {
const fit = vi.fn();
const sendResize = vi.fn();
let rafCb: (() => void) | undefined;
scheduleTerminalFits({
fit,
sendResize,
fontsReady: Promise.resolve(),
loadNerdFont: () => Promise.resolve(),
raf: (cb) => {
rafCb = cb;
},
isAborted: () => false,
});
rafCb?.();
await flush();
expect(fit).toHaveBeenCalledTimes(3);
expect(sendResize).toHaveBeenCalledTimes(3);
});
test('does nothing once aborted', async () => {
const fit = vi.fn();
scheduleTerminalFits({
fit,
sendResize: vi.fn(),
fontsReady: Promise.resolve(),
loadNerdFont: () => Promise.resolve(),
raf: (cb) => cb(),
isAborted: () => true,
});
await flush();
expect(fit).not.toHaveBeenCalled();
});
});