@@ -505,6 +505,11 @@ export interface AppShellOptions {
505505 * notice has been shown, so it is not permanent chrome.
506506 */
507507 readonly telemetryNotice ?: string ;
508+ /**
509+ * Suppress landing snow and mountain motion. The idle timer is not
510+ * armed, and `paintLanding` holds a still mountain with no flakes.
511+ */
512+ readonly reducedMotion ?: boolean ;
508513 /**
509514 * Clipboard port for Alt+C and drag-select auto-copy. Defaults to an
510515 * in-memory recorder so tests and demos never shell out; the product host
@@ -1811,7 +1816,7 @@ export function applyLayout(shell: AppShell, layout: GeometryLayout): void {
18111816 // A new zone can seat a different tier, and a tier is a different grid, so
18121817 // the mark is redrawn rather than left showing the previous size's frame.
18131818 fitLandingMark ( landing . above , resolveMarkGrid ( split . above , layout . contentWidth ) ) ;
1814- paintLandingMark ( landing . above , bag . landingNowMs , ! bag . landingAnimating ) ;
1819+ paintLandingMark ( landing . above , bag . landingNowMs , ! bag . landingAnimating , bag . reducedMotion ) ;
18151820 landing . below . height = Math . max ( 0 , split . below ) ;
18161821 landing . below . visible = split . below > 0 ;
18171822 }
@@ -2109,6 +2114,12 @@ interface ShellInternals {
21092114 landingAnimating : boolean ;
21102115 /** Clock of the last painted mark frame, so a resize can redraw in place. */
21112116 landingNowMs : number ;
2117+ /**
2118+ * Mount-time reduced-motion flag. When true, the idle snow timer is
2119+ * never armed and every landing paint holds a still mountain with no
2120+ * flakes. Set once at `createAppShell`; not a per-paint argument.
2121+ */
2122+ reducedMotion : boolean ;
21122123 /**
21132124 * Cancels the mount-scoped idle repaint timer armed in `createAppShell`,
21142125 * or null while none is armed. Cleared by whichever teardown happens
@@ -2813,19 +2824,19 @@ const LANDING_IDLE_REPAINT_INTERVAL_MS = 125;
28132824 * needs to drift across a frozen mountain. Driven by the mount-scoped timer
28142825 * armed in `createAppShell` rather than a render event, so the repaint
28152826 * cadence is independent of however often the renderer happens to paint.
2827+ *
2828+ * Reduced motion is a mount-time flag on the shell, not a per-paint
2829+ * argument: it freezes the mountain and drops snow even when a caller
2830+ * asks for `animating`.
28162831 */
2817- export function paintLanding (
2818- shell : AppShell ,
2819- nowMs : number ,
2820- animating : boolean ,
2821- reducedMotion = false ,
2822- ) : void {
2832+ export function paintLanding ( shell : AppShell , nowMs : number , animating : boolean ) : void {
28232833 const bag = internals . get ( shell ) ;
28242834 const landing = bag ?. landing ;
28252835 if ( bag === undefined || landing === null || landing === undefined ) return ;
2826- bag . landingAnimating = animating ;
2836+ const motion = bag . reducedMotion ? false : animating ;
2837+ bag . landingAnimating = motion ;
28272838 bag . landingNowMs = nowMs ;
2828- paintLandingMark ( landing . above , nowMs , ! animating , reducedMotion ) ;
2839+ paintLandingMark ( landing . above , nowMs , ! motion , bag . reducedMotion ) ;
28292840}
28302841
28312842/** True while the landing composition is still mounted. */
@@ -5749,6 +5760,7 @@ export function createAppShell(renderer: ShellRenderer, options?: AppShellOption
57495760 const paletteCatalogOpt = options ?. paletteCatalog ?? null ;
57505761 const onCommandOpt = options ?. onCommand ;
57515762 const onObserveRequestOpt = options ?. onObserveRequest ;
5763+ const reducedMotion = options ?. reducedMotion === true ;
57525764
57535765 const terminal = terminalOf ( renderer , options ?. terminal ) ;
57545766 const layout = resolveGeometry ( {
@@ -5865,7 +5877,7 @@ export function createAppShell(renderer: ShellRenderer, options?: AppShellOption
58655877 } ) ;
58665878 transcript . add ( transcriptSpacer ) ;
58675879
5868- const landingAbove = createLandingAbove ( ctx ) ;
5880+ const landingAbove = createLandingAbove ( ctx , reducedMotion ) ;
58695881 const landingBelowState = landingBelowContent ( {
58705882 rows : splitLandingRows ( layout . heights . transcript ) . below ,
58715883 columns : layout . contentWidth ,
@@ -6650,6 +6662,7 @@ export function createAppShell(renderer: ShellRenderer, options?: AppShellOption
66506662 landingSuggestionsVisible : true ,
66516663 landingAnimating : false ,
66526664 landingNowMs : 0 ,
6665+ reducedMotion,
66536666 landingIdleTimerCancel : null ,
66546667 chrome : { task : [ ] , tasksRaw : [ ] , agents : [ ] } ,
66556668 // CL-5847: the manage_tasks checklist panel is hidden by default. The
@@ -6679,22 +6692,27 @@ export function createAppShell(renderer: ShellRenderer, options?: AppShellOption
66796692 // the renderer directly (`withTestRenderer`'s cleanup) without ever
66806693 // calling `shell.dispose()`. Without this check the timer would keep
66816694 // firing against renderables the harness already tore down.
6682- const landingIdleHandle = setInterval ( ( ) => {
6683- if ( renderer . isDestroyed ) {
6684- clearInterval ( landingIdleHandle ) ;
6685- return ;
6686- }
6687- const bag = internals . get ( shell ) ;
6688- if ( bag ?. landing == null || bag . landingAnimating ) return ;
6689- paintLanding ( shell , Date . now ( ) , false ) ;
6690- } , LANDING_IDLE_REPAINT_INTERVAL_MS ) ;
6691- landingIdleHandle . unref ?.( ) ;
6692- {
6693- const bag = internals . get ( shell ) ;
6694- if ( bag !== undefined ) {
6695- bag . landingIdleTimerCancel = ( ) => clearInterval ( landingIdleHandle ) ;
6696- } else {
6697- clearInterval ( landingIdleHandle ) ;
6695+ //
6696+ // Reduced motion never starts the timer: there is no snow to advance
6697+ // and the mountain stays on its filled frame.
6698+ if ( ! reducedMotion ) {
6699+ const landingIdleHandle = setInterval ( ( ) => {
6700+ if ( renderer . isDestroyed ) {
6701+ clearInterval ( landingIdleHandle ) ;
6702+ return ;
6703+ }
6704+ const bag = internals . get ( shell ) ;
6705+ if ( bag ?. landing == null || bag . landingAnimating ) return ;
6706+ paintLanding ( shell , Date . now ( ) , false ) ;
6707+ } , LANDING_IDLE_REPAINT_INTERVAL_MS ) ;
6708+ landingIdleHandle . unref ?.( ) ;
6709+ {
6710+ const bag = internals . get ( shell ) ;
6711+ if ( bag !== undefined ) {
6712+ bag . landingIdleTimerCancel = ( ) => clearInterval ( landingIdleHandle ) ;
6713+ } else {
6714+ clearInterval ( landingIdleHandle ) ;
6715+ }
66986716 }
66996717 }
67006718 transcriptSpacers . set ( shell , transcriptSpacer ) ;
0 commit comments