fix(terminal): refit after fonts load
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
# Task 11 Report
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Implemented terminal fit scheduling for the first animation frame, `document.fonts.ready`, and Nerd Font loading. Each successful, non-aborted fit also sends the current terminal dimensions to the worker. Nerd Font completion still refreshes the terminal glyph rows.
|
||||||
|
|
||||||
|
## TDD evidence
|
||||||
|
|
||||||
|
- RED: `cd apps/next && bun run test:unit -- terminal-fit` failed with `Cannot find package '@/components/agent-workspace/terminal-fit'` before the helper existed.
|
||||||
|
- GREEN: the same focused command passed 1 file and 2 tests after adding `scheduleTerminalFits`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `cd apps/next && bun run test:unit -- terminal-fit`: PASS, 1 file / 2 tests.
|
||||||
|
- `cd apps/next && bun run test:unit`: PASS, 4 files / 10 tests.
|
||||||
|
- `cd apps/next && bun run typecheck`: PASS.
|
||||||
|
- `cd apps/next && bun run format`: PASS.
|
||||||
|
- `cd apps/next && bun run lint`: PASS, with the existing stale `baseline-browser-mapping` data warning.
|
||||||
|
- `cd apps/next && bun run test:component`: BLOCKED before test collection. This isolated worktree's root `node_modules` symlink resolves to `/home/gib/Documents/Code/Spoon/node_modules`, causing Vitest to look for `/home/gib/Documents/Code/Spoon/tools/vitest/setup-jsdom.ts` instead of the worktree setup file.
|
||||||
|
|
||||||
|
## Concerns
|
||||||
|
|
||||||
|
- No implementation concerns found.
|
||||||
|
- The component-suite worktree dependency-resolution issue remains external to this task.
|
||||||
@@ -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 { Button } from '@spoon/ui';
|
||||||
|
|
||||||
|
import { scheduleTerminalFits } from './terminal-fit';
|
||||||
|
|
||||||
import '@xterm/xterm/css/xterm.css';
|
import '@xterm/xterm/css/xterm.css';
|
||||||
|
|
||||||
const TERMINAL_FONT =
|
const TERMINAL_FONT =
|
||||||
@@ -143,18 +145,8 @@ export const WorkspaceTerminal = ({
|
|||||||
term.loadAddon(fit);
|
term.loadAddon(fit);
|
||||||
term.loadAddon(new WebLinksAddon());
|
term.loadAddon(new WebLinksAddon());
|
||||||
term.open(container);
|
term.open(container);
|
||||||
fit.fit();
|
|
||||||
termRef.current = term;
|
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 = () => {
|
const sendResize = () => {
|
||||||
if (ws?.readyState !== WebSocket.OPEN) return;
|
if (ws?.readyState !== WebSocket.OPEN) return;
|
||||||
ws.send(
|
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 = new WebSocket(url);
|
||||||
ws.binaryType = 'arraybuffer';
|
ws.binaryType = 'arraybuffer';
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user