From 75d06394cef5f28cb33c912fa8873b98f1d4047c Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Fri, 10 Jul 2026 16:53:18 -0400 Subject: [PATCH] fix(terminal): refit after fonts load --- .superpowers/sdd/task-11-report.md | 24 ++++++++++ .../agent-workspace/terminal-fit.ts | 21 +++++++++ .../agent-workspace/workspace-terminal.tsx | 32 +++++++++---- apps/next/tests/unit/terminal-fit.test.ts | 47 +++++++++++++++++++ 4 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 .superpowers/sdd/task-11-report.md create mode 100644 apps/next/src/components/agent-workspace/terminal-fit.ts create mode 100644 apps/next/tests/unit/terminal-fit.test.ts diff --git a/.superpowers/sdd/task-11-report.md b/.superpowers/sdd/task-11-report.md new file mode 100644 index 0000000..57b55d3 --- /dev/null +++ b/.superpowers/sdd/task-11-report.md @@ -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. diff --git a/apps/next/src/components/agent-workspace/terminal-fit.ts b/apps/next/src/components/agent-workspace/terminal-fit.ts new file mode 100644 index 0000000..da1d777 --- /dev/null +++ b/apps/next/src/components/agent-workspace/terminal-fit.ts @@ -0,0 +1,21 @@ +export const scheduleTerminalFits = (deps: { + fit: () => void; + sendResize: () => void; + fontsReady: Promise; + loadNerdFont: () => Promise; + 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); +}; diff --git a/apps/next/src/components/agent-workspace/workspace-terminal.tsx b/apps/next/src/components/agent-workspace/workspace-terminal.tsx index aee45bf..59d9923 100644 --- a/apps/next/src/components/agent-workspace/workspace-terminal.tsx +++ b/apps/next/src/components/agent-workspace/workspace-terminal.tsx @@ -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 = () => { diff --git a/apps/next/tests/unit/terminal-fit.test.ts b/apps/next/tests/unit/terminal-fit.test.ts new file mode 100644 index 0000000..5abc592 --- /dev/null +++ b/apps/next/tests/unit/terminal-fit.test.ts @@ -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(); + }); +});