feat(workspace): frameless console that blends into its card

The tab bar and terminal lose their bordered box: the bar keeps one
divider with an accent underline on the active tab, and the terminal is
painted in the card's colour (--surface-3) instead of the page's.

That made a long-standing overflow visible: xterm re-measures its cell
only on a font option change or a resize, so a webfont landing after
open left the fit on the fallback font's cell and the rows/cols spilled
out of the pane. Re-set the family and refit on document.fonts
loadingdone.
This commit is contained in:
2026-10-03 12:37:35 +07:00
parent e5762a59d6
commit 8f6360f993
6 changed files with 31 additions and 17 deletions
+7 -5
View File
@@ -99,20 +99,22 @@ export function ConsoleTabs({
"flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-sm text-fg-secondary hover:bg-surface-4 hover:text-fg-primary";
return (
<div className="flex h-10 shrink-0 items-stretch rounded-t-lg border border-b-0 border-border bg-surface-2">
<div className="flex h-10 shrink-0 items-stretch border-b border-border">
<div role="tablist" aria-label={t("tabs.label")} className="flex min-w-0 items-stretch">
{tabs.map((r) => {
const active = r.id === activeRunId;
return (
<div
key={r.id}
className={`group relative flex min-w-0 max-w-[15rem] items-center border-r border-border ${
className={`group relative flex min-w-0 max-w-[15rem] items-center rounded-t-md ${
active
? "bg-surface-0 text-fg-primary"
: "text-fg-muted hover:bg-surface-3 hover:text-fg-secondary"
? "text-fg-primary"
: "text-fg-muted hover:bg-surface-4 hover:text-fg-secondary"
}`}
>
{active && <span className="absolute inset-x-0 top-0 h-0.5 bg-accent" />}
{active && (
<span className="absolute inset-x-2 -bottom-px h-0.5 rounded-full bg-accent" />
)}
<button
type="button"
role="tab"
+15 -2
View File
@@ -268,18 +268,31 @@ export function TerminalView({ runId, wsBaseUrl, onExit }: TerminalViewProps) {
const reassert = () => sendSize();
term.textarea?.addEventListener("focus", reassert);
const resizeObserver = new ResizeObserver(() => {
const refit = () => {
// Never fit a collapsed console — see sendSize.
const el = containerRef.current;
if (!el || !el.clientWidth || !el.clientHeight) return;
fit.fit(); // a real change fires onResize, which reports it
});
};
const resizeObserver = new ResizeObserver(refit);
if (containerRef.current) resizeObserver.observe(containerRef.current);
// A webfont landing after open changes the cell, not the container, so the
// observer never fires — and xterm re-measures its cell only on a font
// option change or a resize, so the fit kept the fallback font's cell and
// the rows/cols overflowed the pane. Re-setting the family (a trailing
// space keeps it a change) makes xterm measure again before the fit.
const onFontLoaded = () => {
const family = term.options.fontFamily ?? "";
term.options.fontFamily = family.endsWith(" ") ? family.trimEnd() : `${family} `;
refit();
};
document.fonts?.addEventListener("loadingdone", onFontLoaded);
return () => {
disposed = true;
if (retryTimer) clearTimeout(retryTimer);
resizeObserver.disconnect();
document.fonts?.removeEventListener("loadingdone", onFontLoaded);
term.textarea?.removeEventListener("focus", reassert);
resizeDisposable.dispose();
dataDisposable.dispose();