@@ -1797,6 +1797,13 @@ type ShellInternals = {
17971797 * rather than a screen the first prompt wipes.
17981798 */
17991799 landingNotice : string | null
1800+ /**
1801+ * System/runtime notices that arrived while the landing was still up (MCP
1802+ * load failures, width-contract warnings, hook failures). Held here and
1803+ * painted on the notice strip so they never call `clearLandingMark`; flushed
1804+ * into the transcript when the first real session row ends the landing.
1805+ */
1806+ landingDeferredRows : StreamRow [ ]
18001807 /** What the rows below the box are painting, so they can be repainted. */
18011808 landingBelow : LandingBelowContent | null
18021809 /** Starters are offered only while the prompt is empty. */
@@ -2031,6 +2038,28 @@ function evictedRowsNotice(evicted: number): string {
20312038 return ` … ${ evicted } earlier row${ evicted === 1 ? "" : "s" } dropped (past the retention limit)`
20322039}
20332040
2041+ /**
2042+ * Surface a runtime/load notice without stealing the landing hero.
2043+ *
2044+ * MCP connection failures, hook failures and similar startup chatter used to
2045+ * call `appendStreamRow` → `clearLandingMark`, wiping the mountain the moment
2046+ * anything went wrong on load (CL-5618 / CL-5600). While the landing is still
2047+ * mounted the wording rides the notice strip and the row is held for flush
2048+ * once a real session row ends the landing; after that it is a normal system
2049+ * row.
2050+ */
2051+ export function surfaceStartupNotice ( shell : AppShell , text : string ) : void {
2052+ if ( isLanding ( shell ) ) {
2053+ const bag = internals . get ( shell )
2054+ if ( bag !== undefined ) {
2055+ bag . landingDeferredRows . push ( { role : "system" , text } )
2056+ }
2057+ setStatusFlash ( shell , text )
2058+ return
2059+ }
2060+ appendStreamRow ( shell , { role : "system" , text } )
2061+ }
2062+
20342063/**
20352064 * Paint + push onto the visible streamLog (child while observing, parent
20362065 * otherwise). The paint tree stays 1:1 with the (retention-capped) log —
@@ -2356,6 +2385,10 @@ export function repaintTranscriptWindow(shell: AppShell): void {
23562385 * The prompt box travels from the middle of the screen to the bottom, which is
23572386 * a jump; it happens on the same frame as the operator's own first row so it
23582387 * reads as the screen answering them rather than as the layout twitching.
2388+ *
2389+ * System/runtime notices deferred while the hero was up are flushed into the
2390+ * transcript here so they stay durable once the session has content, without
2391+ * ever having stolen the mountain on the way in.
23592392 */
23602393function clearLandingMark ( shell : AppShell ) : void {
23612394 const bag = internals . get ( shell )
@@ -2373,6 +2406,15 @@ function clearLandingMark(shell: AppShell): void {
23732406 bag . landingNotice = null
23742407 appendStreamRow ( shell , { role : "system" , text : notice } )
23752408 }
2409+
2410+ const deferred = bag . landingDeferredRows
2411+ if ( deferred . length > 0 ) {
2412+ bag . landingDeferredRows = [ ]
2413+ // The notice strip held the latest wording while the mark was up; the
2414+ // rows themselves are durable now, so drop the flash rather than double-paint.
2415+ setStatusFlash ( shell , null )
2416+ for ( const row of deferred ) appendStreamRow ( shell , row )
2417+ }
23762418}
23772419
23782420/**
@@ -5599,6 +5641,7 @@ export function createAppShell(
55995641 paletteFilter : null ,
56005642 landing : { above : landingAbove , below : landingBelow } ,
56015643 landingNotice : options ?. telemetryNotice ?? null ,
5644+ landingDeferredRows : [ ] ,
56025645 landingBelow : landingBelowState ,
56035646 landingSuggestionsVisible : true ,
56045647 landingAnimating : false ,
0 commit comments