Skip to content

Keep the landing hero when a startup diagnostic arrives - #415

Merged
TheGreatAxios merged 6 commits into
mainfrom
cl-5718-landing-hero-impl
Aug 8, 2026
Merged

Keep the landing hero when a startup diagnostic arrives#415
TheGreatAxios merged 6 commits into
mainfrom
cl-5718-landing-hero-impl

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Collaborator

The decision worth reviewing: the wiring layer, not the call site

The reported bug is that a startup plugin warning wipes the entire landing hero — mark, guidance, centred composition. The obvious fix is to change the one flush site in runner.ts to use surfaceStartupNotice. This PR deliberately does not do that.

systemRow turned out to be the only appendStreamRow caller in runner.ts, feeding all ten of its call sites. So the helper itself is the layer that owns "what happens when the runner says something," and that is where the fix belongs. Routing that one helper through the shell's notice path fixes every producer at once — including the ones nobody has written yet.

This is the second time this exact symptom has been fixed. The previous fix routed the MCP and hook producers away from the transcript individually, and the plugin producer kept the defect. A cross-cutting constraint applied at call sites is one forgotten call site away from being false, which is precisely how this bug survived its own fix.

So the constraint gets an owner: src/tui-opentui/startup-notices.ts exposes flushStartupNotices, and the invariant it holds is a startup diagnostic is never a transcript row. It is small on purpose — the value is that the rule is stated once and named, not that it is complicated.

Reviewers should pressure-test exactly this: is the seam load-bearing, or ceremony over a one-line fix? That is the live question, not whether the bug is fixed.

What else is here

No transcript row can be labelled with the wiring that produced it. meta: "command" (runner.ts, runner-host.ts) and the three meta: "palette" (shell.ts) painted verbatim into a twelve-column gutter the operator reads. A system row's text already says what it is; the three palette rows literally opened with the word palette: beside a column repeating it. Audited across src/tui/ and src/tui-opentui/: there is no literal meta: "overlay" on any rendered row, so that half of the rule was already satisfied. meta: "observe" is kept — observe is a real product mode an operator would name, not plumbing.

Each missing plugin skill is counted once. formatPluginWarningsSummary counted raw warnings, so three plugins each referencing style, philosophy and brand-identity produced 7 skills missing: above a list of nine names. A skill referenced by three plugins is one missing skill — the operator installs it once and all three resolve. The count is now taken from the deduplicated list, so the number and the names cannot disagree again.

Snow over the mark. Absorbs #380 rather than reimplementing it: that branch's implementation and its tests are taken directly. The sky above the ridgeline was dead space; a sparse field of pixel snow drifts through it on the clock the mark already runs on. Density and fall rate stay low deliberately — a storm would turn the one legible thing on the screen into texture. Flakes never land on mountain coverage, and stop entirely when the mark is held still or fading, so the decoration never outlives the thing it drifts over. #380 can be closed on merge.

The landing's two doors read as a pair. Key and description were joined by one space, so the two lines started their descriptions on different columns. A fixed key column sets them. The version moves one row away for the same reason: flush under the two keys it read as a third door, when it only states what is running.

Testing

RED first on both defects. The dedupe test reproduced the operator's exact string before the fix. The landing test asserts a session emitting a startup plugin warning still renders the mark, and a companion asserts no flushed notice ever carries a plumbing gutter label.

Full suite via the repo's mutex-wrapped runner: 4269 pass, 1 fail. The failure is src/agent/lsp-availability.test.ts — it probes for an installed language-server binary, is environmental, and is untouched by this diff. Typecheck is clean across every file changed here; the only tsc errors are pre-existing vendor type drift in src/agent/director.ts, which this diff does not touch.

Verified PR #410's animated phase indicator and narrowed side gutter are intact and not reintroduced-over.

Still to come on this branch

Composition work continues as follow-up commits: the changelog swap for fresh installs, and the notice register (notices show only when true, at most two at a time — a permanent stack of four is the failure mode being designed out). The bug fix is not held for it.

Notes

  • Branch name: this is on cl-5718-landing-hero-impl rather than cl-5718-landing-hero because the canonical name was locked by another worktree and could not be released. Not a stray branch.
  • Diagnostic worth recording for whoever hits that next: when a branch appears locked by another worktree, the useful question is whose worktree. A parent agent's own lock and a genuinely competing session look identical from outside, and the difference decides whether you stand down or proceed.
  • Mockups for the full redesign are on the Linear ticket rather than in the repo, because docs/plans/ became git-ignored on main today.

A skill referenced by three plugins and absent from the search path is one
missing skill, not three: the operator installs it once and all three
resolve. Counting raw warnings made the summary both wrong and
self-contradicting, reading "7 skills missing" above a list of nine names.
Anything the runner says before the first turn lands while the landing still
owns the screen, and a transcript row there reaches clearLandingMark and takes
the whole composition with it — the mark, the guidance beside it, and the
centred prompt box.

The previous fix routed the MCP and hook producers away from the transcript one
at a time and the plugin producer kept the defect. Routing the runner's own
notice helper through the shell's notice path instead fixes every producer at
once, including the ones nobody has written yet, and gives the constraint a
single owner rather than a rule each call site has to remember.

The gutter label goes with it. A system row's text already says what it is, so
stamping it "command" only leaked wiring into a column the operator reads.
The sky above the ridgeline was dead space. A sparse field of pixel snow drifts
through it on the clock the mark already runs on, so the hero has motion even
while the silhouette is held.

Density and fall rate stay low deliberately: the mark has to keep reading as a
mark, and a storm would turn the one legible thing on the screen into texture.
Flakes never land on mountain coverage, and they stop entirely when the mark is
held still or is fading out, so the decoration never outlives the thing it
drifts over.

Absorbed from the standalone snow branch rather than reimplemented; the taste
question it was open on is settled by making snow part of the default landing.
The meta column is the operator's: it says what a row is about, and it is read
at a glance beside every row in the transcript. A row labelled "palette" says
only which part of the code emitted it, which is a fact about us and not about
their session — and the three rows carrying it already open with "palette:" in
their own text, so the column was repeating a word it sat next to.
The key and its description were joined by a single space, so the two lines
started their descriptions on different columns and read as two unrelated
notes rather than as the set they are. A fixed key column lines them up.

The version moves a row away from them for the same reason: sitting flush
under the two keys it read as a third door, when it is only a statement of
what is running.
@linear-code

linear-code Bot commented Aug 8, 2026

Copy link
Copy Markdown

CL-5718

The wrapper module added nothing the shell function did not already do. It
introduced no type and narrowed no export, so a producer reaching for
appendStreamRow directly was exactly as easy with it as without, and the
history it documented reads better on the function itself.

The name was also wrong. The path carries unknown commands, unavailable
modals and provider failures, none of which happen at startup, and a name
that lies to the next reader is how this constraint got lost twice.
@TheGreatAxios
TheGreatAxios merged commit 703d823 into main Aug 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant