Skip to content

Commit 0592289

Browse files
committed
fix(splash): wait for real first-frame, not just any output
The inline-mode splash dismissal fired on hasOutputRef + 1500ms, which trips on tiny preambles like "Connecting..." or auth-check spinners (~20 bytes). On slow-starting CLIs this dismissed the splash several seconds before the actual REPL rendered, leaving the user staring at an empty terminal. Refine the inline signal to "substantial output then a brief silence": track total output bytes + last-output timestamp, dismiss only when ≥512 bytes have arrived AND the stream has been quiet for 500 ms (CLI finished writing its first frame and is awaiting input). alt-screen fast-path and process-exit/start-fail paths unchanged. CLIs that print continuously fall through to the existing 15 s maxWait. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9b45556 commit 0592289

1 file changed

Lines changed: 29 additions & 7 deletions

File tree

src-ui/src/components/center/TierTerminal.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,14 @@ function TierTerminalImpl({
277277

278278
// ── Launch failure detection ─────────────────────────────────────────────
279279
const hasOutputRef = useRef(false); // Set to true when PTY emits visible output
280+
// Refined readiness signals for inline-mode CLIs (Claude Code etc. that
281+
// don't enter alt-screen). hasOutputRef alone trips on the first byte —
282+
// a "Connecting..." preamble was enough to dismiss the splash even when
283+
// the actual REPL was 8 s away. Tracking total bytes + last-output time
284+
// lets the splash wait for "substantial output, then a brief silence"
285+
// (CLI finished its first frame and is awaiting input).
286+
const outputBytesRef = useRef(0);
287+
const lastOutputAtRef = useRef(0);
280288
const [processExited, setProcessExited] = useState(false);
281289
const [startFailed, setStartFailed] = useState(false);
282290
// First exit event to arrive (onExit from child-watcher, or onStatus from
@@ -600,6 +608,8 @@ function TierTerminalImpl({
600608
onOutput: (data) => {
601609
if (!mounted) return;
602610
hasOutputRef.current = true;
611+
outputBytesRef.current += data.length;
612+
lastOutputAtRef.current = Date.now();
603613
xtermRef.current?.write(data);
604614

605615
// Handle SSH Auto-login via Password injection
@@ -988,13 +998,25 @@ function TierTerminalImpl({
988998
}
989999
// Inline-mode signal: some tools (current Claude Code builds, simple
9901000
// CLIs) print their banner directly to the regular terminal instead
991-
// of entering alt-screen. Threshold 1500 ms post-splash-start; once
992-
// we've passed the 800 ms branding window AND output is flowing AND
993-
// the process is alive, the tool is clearly running. Tighter than
994-
// the prior 2500 ms because tools usually finish painting their
995-
// banner well within 1 s — anything slower would make the splash
996-
// feel "stuck" over a visibly working REPL.
997-
if (hasOutputRef.current && elapsed > 1500) {
1001+
// of entering alt-screen. We need a "first frame painted" proxy
1002+
// that's stronger than "any output", because CLIs commonly print a
1003+
// tiny preamble ("Connecting...", auth-check spinners, ~20 bytes)
1004+
// and then go silent for several seconds before the real REPL
1005+
// appears — dismissing on the preamble leaves the user staring at
1006+
// an empty terminal. Combined gate:
1007+
// • outputBytes ≥ 512 — filters trivial preambles; a real banner
1008+
// (logo + version + prompt) easily clears this.
1009+
// • silence ≥ 500 ms — output stream has paused, meaning the CLI
1010+
// finished writing its first frame and is awaiting input.
1011+
// • elapsed > 1500 ms — branding window respected.
1012+
// If a CLI prints continuously without pause, we never trip silence
1013+
// and fall through to the maxWait fallback below.
1014+
const sinceLastOutput = Date.now() - lastOutputAtRef.current;
1015+
if (
1016+
outputBytesRef.current >= 512 &&
1017+
sinceLastOutput >= 500 &&
1018+
elapsed > 1500
1019+
) {
9981020
dismiss();
9991021
clearInterval(poll);
10001022
return;

0 commit comments

Comments
 (0)