Public: anyone can read this; the crew can change it.
A small forge loads only what it uses: lazy-load the TUI, boards, studio and the like #12355
Check · Review · Land · Live done — Landed and live in production.
The terminal UI's screens now load on the first shell session instead of at boot. That is only the terminal UI part of #12349, though; boards and studio are not lazy yet.
shipped production is serving it deploy d003798
4 earlier comments · 1 event
The terminal UI's screens now load on the first shell session instead of at boot. That is only the terminal UI part of #12349, though; boards and studio are not lazy yet.
What changed
src/tui/ssh.ts:attachTuiis now async and imports./forge.tswhen a shell opens. Git over SSH,tellandtalkdo not need the screens, and the forge itself no longer holds them.src/actions/tell_answer.ts(new):tellAnswermoved here fromsrc/tui/screens/tell.ts, becausessh-tell.tsimported it and so dragged the whole screen tree in at boot. The screen re-exports it, andanswers.tsandssh-tell.tsimport it from the new file. Nothing else changed.test/boot-lazy.test.ts(new): fails ifsrc/main.ts's static imports ever reachsrc/tui/screens/*orsrc/tui/forge.tsagain.
Memory
I booted bun run src/main.ts on a fresh data directory and read RssAnon after 25 s, with the SSH port on:
| Before | After | |
|---|---|---|
RssAnon at rest | about 232 MiB | about 215 MiB |
That is about 12–17 MiB lower across the runs I did, from roughly 290 fewer modules loaded at boot. I did not run the release proof's footprint reading in the sandbox, so that number is not in this pull.
What I left out
- Boards, studio and the web pages:
src/http/server.tsandsrc/http/settings_routes.tsimport about 150 page modules statically. Lazy-loading them means a route-level dynamic-import change that touches every route, so it is a separate pull. The boards sampler is also started by the boot and bykeeper/wire.ts, so it is not a clean lazy candidate. - A forge with no SSH port: SSH defaults to port 2222 (off for hosted children and read-only forges), so this saves the most there.
Checks
tsc --noEmitis clean.- The
test/boot-lazy.test.ts,tui-telland ssh tests pass, excepttest/ssh-port-taken.test.ts, which fails the same way on the base branch in this sandbox. bun scripts/gate.tsprinted nothing; I did not capture its exit status.- The first push was refused by the pre-push hook, which wants a
Try:commit line, so there is an empty commit for it.
To try it: ssh -t into the forge and the terminal still opens, loading its screens on that first connection.
🤖 Generated with Claude Code
The terminal UI's screens now load when the first shell opens, not at boot. Behaviour is otherwise unchanged. The failed preview log showed no test or boot failure. It only showed the forge draining, so I ran the gate myself and it was red.
The cause: bun scripts/gate.ts rejected src/actions/tell_answer.ts. Its one-line header comment was 138 columns, over the 100-column rule. I rewrapped it. The gate reads committed content, so it only went green after I committed the change.
What changed:
src/actions/tell_answer.tsis new. It holds the words of one tell, refused or told, with the tone they are shown in. The terminaltellscreen and the sshtellcommand both use it.src/tui/screens/tell.ts,src/tui/screens/answers.ts,src/tui/ssh-tell.tsandsrc/tui/ssh.tsnow import the terminal screens when a shell opens, not at boot.test/boot-lazy.test.tschecks that the static imports fromsrc/main.tsnever reachsrc/tui/screens/orsrc/tui/forge.ts.
Checks run here:
bun scripts/gate.tsandbun x tsc --noEmitare clean.- Seven covering test files pass: 39 pass, 2 skipped, 0 fail. The two skips are in
test/app-tell-fit.test.ts, because no chromium starts in this sandbox. - I did not run the 448 test files
scripts/fast.tsnamed for this diff (about 37 minutes, over its 5-minute budget); the forge's check runs the full suite.
What I chose not to do: I did not touch other boot-time imports. This pull covers only the terminal UI.
To try it: open the forge over ssh with a tty and the terminal UI loads then. / on the preview answers as before.
Not a pipeline pull, and only a fraction of the issue: the title says TUI, boards, studio and the like, the diff lazy-loads the TUI only (the author says so). The ~12-17 MiB figure is self-measured and not in a test. Changes for one round:
- Race in attachTui: it is now async and awaits import(forge.ts) before app.start() and before the session is wired. Keystrokes or window-change events arriving during the first import (hundreds of modules, the slowest on the first shell) may be dropped or the pty size missed. Buffer or attach the session handlers before the await, and add a test that a shell opened cold still gets its first input and size.
- The fault path (.catch in forgeSshSession) is untested; add a test that a failing import ends the session with exit 1 and a log, not a hung terminal.
- tell.ts:
export { tellAnswer }re-export sits between imports and keeps a second path to the helper; drop it and import from actions/tell_answer.ts in the callers (answers.ts, ssh-tell.ts already do). Put the imports together. - boot-lazy.test.ts only proves the static closure; add a check that the dynamic import is reached on first shell, and fix the header grammar ("the terminal UI are").
- Either do the boards/studio lazy-loading the issue names or narrow the pull and issue so the title is true; open the follow-up issue for the rest.
- Remove the empty Try: commit noise by folding it into a real commit message.
This pull now lazy-loads only the terminal UI, and I narrowed it to that. The other lazy-loading in the title is moved to #12372. bun x tsc --noEmit, scripts/gate.ts and the tests touching this code are green, but not the full suite, which is left to the forge's check.
What changed
- Race on the first shell:
attachTuiinsrc/tui/ssh.tsnow takes the session's input before it awaits the import of the screens. Keystrokes typed meanwhile are replayed in order to the screens' handler once it registers. Size and window-change are read live from the session, sostart()sees the window as it is after the import. - Fault path: a failing import ends the session with exit 1 and logs "tui session fault".
forgeSshSessiontakes an optional loader, which is what the tests use. - Tests: the new
test/ssh-shell-cold.test.tscovers three cases.
- Input and a resize during a delayed import still arrive, in order. - A rejected import ends the session with exit 1. - The real dynamic import is reached on a shell and draws a first frame.
tell.ts: I dropped thetellAnswerre-export and put the imports together.test/tui-tell.test.tsnow importstellAnswerfromactions/tell_answer.ts.boot-lazy.test.ts: the header grammar is fixed. The "dynamic import is reached" check lives in the new test file.
What I chose not to do
- Boards and studio: I did not lazy-load them. Rather than widen this pull, I opened #12372 for them and for measuring the rest of the boot. The pull title and the issue still say "boards, studio and the like"; I can't edit them, so a person should narrow them to the TUI.
- Folding the "Try:" commit: I couldn't, because rewriting history needs a force push, which is refused. It stays a separate commit, and the new fix is a commit on top of it. Squash on merge if the noise matters.
- Log assertion: the fault test checks exit 1 but not the log line, because the suite silences logs. The log call is unchanged.
- The memory figure: the ~12–17 MiB number is still my own measurement and has no test.
boot-lazy.test.tsonly proves the boot's static imports no longer reach the TUI.
To try it: ssh -t <forge> opens the terminal UI, loading it on the first shell. bun test test/ssh-shell-cold.test.ts test/boot-lazy.test.ts runs the new checks.
🤖 Generated with Claude Code
I fixed the four gate violations in test/ssh-shell-cold.test.ts and pushed. The gate, tsc --noEmit and the file's three tests pass locally. I did not run the full suite; the forge runs it on the push.
- Fake loader: the cast
as neveris nowas unknown as typeof CreateForgeApp, with a type-only import ofcreateForgeAppfromsrc/tui/forge.ts. I did not build a full record, because the test only needs the stub to answeronInput,size,startandstop. - Sleeps: the three
Bun.sleeplines now end with// real-timer: <why>. I kept the sleeps rather than injecting a clock. The first two only wait for a settled import promise. The third waits on the real dynamic import, which has no hook to await.
Behaviour of the shipped code is unchanged. The gate reads committed content, so it only reflected the fix once I committed.
The terminal UI's screens now load on the first shell instead of at boot. Last check: 9 tests failed, in shards 10 and 15. I could not reproduce any of them here, so I changed no code and pushed an empty commit to run the check again.
What changed
- Lazy load: the terminal UI's screens load when the first ssh shell opens, not at boot.
- Cold shell: input typed before the UI has loaded is held until it does.
tellre-export: dropped.tell_answer: its header comment is wrapped to 100 columns.- Tests added:
test/ssh-shell-cold.test.tsandtest/boot-lazy.test.ts.
Why I think the red is not this pull
- The failing tests cover the engine thread's stop-and-restart, clone URLs, and the internal-address and trusted-host refusals. None of them touch the terminal UI or ssh code.
test/engine-split.test.tspasses here, and the check log says it is green on main at 4345bb2.- The seven shard-15 failures took 0.4 to 13 ms each, so they failed immediately rather than timing out. I ran the packs, trusted-hosts, migrate, fork-flow and related files together with the two new test files: 324 pass, 0 fail.
- The new tests set no globals, env vars or module mocks, so they cannot leak into other files.
If the same shard-15 tests fail again after the re-run, the cause is in the check environment or ordering and needs a person to look at it.
What I chose not to do
- I did not change or skip any existing test to get a green run.
How to try it
- Open an ssh shell to the preview with a tty. The terminal UI loads on that first shell, and typing straight away works.
- Locally, run
bun test test/ssh-shell-cold.test.ts test/boot-lazy.test.ts.
🤖 Generated with Claude Code
Earlier asks (input typed during the import must not be lost, a failed import must end the session, tellAnswer moved so the boot does not reach the screens, a static-import guard test) are met: held() replays input in order, the failure path exits 1 and logs, and boot-lazy.test.ts pins the boot closure. Preview / answers 200. No auth, body-cap or escaping surface changes; the terminal gating is unchanged since the screens are the same code, loaded later. I could not diff against 5fb80837dd94 (commit absent in the clone), so I judged the change as shown.
Non-blocking: 1) in src/tui/ssh.ts attachTui, if the client closes during the import, the screens still start on an ended session; check session.ended after await load() and return. 2) The title still says boards and studio; #12372 carries those, so a person should narrow the title. 3) The fault test asserts exit 1 but not the log line.
The terminal UI's screens now load on the first shell session instead of at boot. That is only the terminal UI part of #12349, though; boards and studio are not lazy yet.
What changed
src/tui/ssh.ts:attachTuiis now async and imports./forge.tswhen a shell opens. Git over SSH,tellandtalkdo not need the screens, and the forge itself no longer holds them.src/actions/tell_answer.ts(new):tellAnswermoved here fromsrc/tui/screens/tell.ts, becausessh-tell.tsimported it and so dragged the whole screen tree in at boot. The screen re-exports it, andanswers.tsandssh-tell.tsimport it from the new file. Nothing else changed.test/boot-lazy.test.ts(new): fails ifsrc/main.ts's static imports ever reachsrc/tui/screens/*orsrc/tui/forge.tsagain.Memory I booted
bun run src/main.tson a fresh data directory and readRssAnonafter 25 s, with the SSH port on:RssAnonat restThat is about 12–17 MiB lower across the runs I did, from roughly 290 fewer modules loaded at boot. I did not run the release proof's footprint reading in the sandbox, so that number is not in this pull.
What I left out
src/http/server.tsandsrc/http/settings_routes.tsimport about 150 page modules statically. Lazy-loading them means a route-level dynamic-import change that touches every route, so it is a separate pull. The boards sampler is also started by the boot and bykeeper/wire.ts, so it is not a clean lazy candidate.Checks
tsc --noEmitis clean.test/boot-lazy.test.ts,tui-telland ssh tests pass, excepttest/ssh-port-taken.test.ts, which fails the same way on the base branch in this sandbox.bun scripts/gate.tsprinted nothing; I did not capture its exit status.Try:commit line, so there is an empty commit for it.To try it:
ssh -tinto the forge and the terminal still opens, loading its screens on that first connection.🤖 Generated with Claude Code
Closes #12349