fix(desktop): keep the Buzz Term splash until the terminal is revealed

The welcome banner was dismissed by the first visible output from the
active PTY, but TerminalBootstrap spawns the shell when AppShell mounts
and a real shell prints its prompt immediately. The banner was therefore
always gone before the user first opened the terminal.

Dismiss on the first keystroke sent to the PTY, or on visible output that
arrives after the terminal has been revealed at least once. Keystroke
alone would leave the banner painted over live scrollback when a
background job prints into a revealed-but-idle terminal.

Also stop comparing a mounted DOM node against null in the welcome test
helper: building that AssertionError message inspects the element, and
the React fiber keys make the walk take ~2 minutes. A failure in these
rows now reports in about a second.

Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
This commit is contained in:
npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc
2026-08-02 17:06:59 -04:00
parent 6d159d1248
commit e3deaffbe6
2 changed files with 50 additions and 8 deletions
@@ -179,21 +179,55 @@ const VISIBLE_FRAME = {
};
async function expectWelcome(view, present) {
// Compare a boolean, never the node: an AssertionError carrying a mounted
// element inspects it to build its message, and `__reactFiber$` makes that
// walk the whole fiber graph — a failure takes ~2min to report instead of ms.
await waitFor(() =>
present
? assert.ok(view.container.querySelector(".buzz-terminal-welcome"))
: assert.equal(
view.container.querySelector(".buzz-terminal-welcome"),
null,
),
assert.equal(
view.container.querySelector(".buzz-terminal-welcome") !== null,
present,
),
);
}
test("non-empty output from the active PTY dismisses the welcome overlay", async () => {
async function reveal(view) {
const substrate = view.container.querySelector(".buzz-terminal-substrate");
toggleChord();
await waitFor(() =>
assert.equal(substrate.dataset.terminalOwner, "terminal"),
);
}
test("spawn-time output before the first reveal keeps the welcome overlay", async () => {
const subject = fixture({ frame: EMPTY_FRAME });
await ready(subject.view);
await expectWelcome(subject.view, true);
subject.rerender({ frame: VISIBLE_FRAME });
await expectWelcome(subject.view, true);
await reveal(subject.view);
await expectWelcome(subject.view, true);
});
test("the first keystroke dismisses the welcome overlay", async () => {
const subject = fixture({ frame: VISIBLE_FRAME });
await ready(subject.view);
await reveal(subject.view);
await expectWelcome(subject.view, true);
fireEvent.input(subject.view.getByLabelText("Terminal input"), {
target: { value: "l" },
});
await expectWelcome(subject.view, false);
});
test("non-empty output after the reveal dismisses the welcome overlay", async () => {
const subject = fixture({ frame: EMPTY_FRAME });
await ready(subject.view);
await reveal(subject.view);
await expectWelcome(subject.view, true);
subject.rerender({ frame: VISIBLE_FRAME });
await expectWelcome(subject.view, false);
});
@@ -201,6 +235,7 @@ test("non-empty output from the active PTY dismisses the welcome overlay", async
test("empty active output keeps the welcome overlay", async () => {
const subject = fixture({ frame: EMPTY_FRAME });
await ready(subject.view);
await reveal(subject.view);
await expectWelcome(subject.view, true);
subject.rerender({
@@ -221,6 +256,7 @@ test("non-empty output from an inactive PTY keeps the welcome overlay", async ()
],
});
await ready(subject.view);
await reveal(subject.view);
await expectWelcome(subject.view, true);
subject.rerender({
@@ -101,6 +101,7 @@ export function TerminalSubstrate({
const previousFocusRef = React.useRef<HTMLElement | null>(null);
const reportedFocusRef = React.useRef<boolean | null>(null);
const scrollBySessionRef = React.useRef(new Map<string, number>());
const revealedRef = React.useRef(false);
const activeSession = sessions.find((session) => session.active);
const activeSessionId = activeSession?.id ?? null;
const frames = React.useMemo(
@@ -141,6 +142,7 @@ export function TerminalSubstrate({
const appSurface = getAppSurface();
if (!appSurface) return;
if (next === "terminal") {
revealedRef.current = true;
previousFocusRef.current =
document.activeElement instanceof HTMLElement
? document.activeElement
@@ -312,9 +314,13 @@ export function TerminalSubstrate({
consumeFrame(delivered.frame);
if (
delivered.sessionId === activeSessionId &&
revealedRef.current &&
hasVisibleOutput(delivered.frame)
) {
// Policy: first visible output from the active PTY removes the overlay.
// Policy: the banner is a splash for the reveal, so spawn-time shell
// output must not dismiss it. Only visible output from the active PTY
// that arrives after the terminal has been revealed (or the first
// keystroke, see sendInput) removes the overlay.
setWelcomeVisible(false);
}
}