Problem
Probe/validation spinners in the TUI are hand-rolled per surface and have drifted, producing inconsistent — and sometimes broken — animation:
SkillFeedsStepView ("Discovering skills at {url}") uses a view-local _spinnerTick++ incremented per render — works, but mutates state during render.
ProviderStepView (provider validation/probe) indexes the glyph on ProbeElapsedSeconds (1 Hz) → the spinner crawls at ~1 fps even though InitWizardPage already redraws it every 120 ms.
ModelManagerPage (model discovery) has no fast tick at all (1 s timer, redraw only on state change) → the spinner is effectively frozen for the whole probe.
ProviderManagerPage (eager + per-provider validation) has the same elapsed-indexed lazy pattern.
OAuthFlowViews carries yet another copy of SpinnerFrames + a GetSpinnerFrame(tick) helper.
Every surface re-implements the SpinnerFrames array, a tick source, and (separately) a redraw subscription. This is the root of the lazy/frozen probe spinners noted in #1292.
Footgun
The naive fix — subscribing to a SpinnerTick reactive property inside a rebuilt view method — creates a re-entrant redraw loop: R3 ReactiveProperty.Subscribe fires the current value immediately → InvalidateAndRedraw() → rebuild → re-subscribe → … It's easy to write, and the interactive smoke harness wouldn't catch it (the local-Ollama probe is near-instant, so the loop has almost no window to run). We nearly shipped exactly this while working on #1292.
What already exists
Termina has a self-animating SpinnerSegment (Termina.Components.Streaming, used by the chat streaming view with an intervalMs), but it lives in the streaming-segment model and isn't usable in the ILayoutNode/TextNode views the wizard and managers build.
Proposal
A reusable, self-animating spinner node for the layout-node model that owns its own animation timer + redraw — so a view drops in something like new SpinnerNode(style, color, intervalMs) (composed with a text node) with no per-surface tick fields, no copied SpinnerFrames, and no hand-wired redraw subscription. Then migrate the existing hand-rolled spinners (SkillFeeds, provider step, model manager, provider manager, OAuth flows) onto it.
Benefit
- Eliminates the frozen/lazy/inconsistent-spinner class of bug and the re-entrancy footgun.
- One tested component; consistent look and cadence across the TUI.
- Removes the duplicated
SpinnerFrames arrays and per-surface tick/redraw plumbing.
Context
Surfaced while fixing #1292 (PR #1311). The cosmetic spinner improvements were intentionally dropped from that PR in favor of this standardized approach.
Problem
Probe/validation spinners in the TUI are hand-rolled per surface and have drifted, producing inconsistent — and sometimes broken — animation:
SkillFeedsStepView("Discovering skills at {url}") uses a view-local_spinnerTick++incremented per render — works, but mutates state during render.ProviderStepView(provider validation/probe) indexes the glyph onProbeElapsedSeconds(1 Hz) → the spinner crawls at ~1 fps even thoughInitWizardPagealready redraws it every 120 ms.ModelManagerPage(model discovery) has no fast tick at all (1 s timer, redraw only on state change) → the spinner is effectively frozen for the whole probe.ProviderManagerPage(eager + per-provider validation) has the sameelapsed-indexed lazy pattern.OAuthFlowViewscarries yet another copy ofSpinnerFrames+ aGetSpinnerFrame(tick)helper.Every surface re-implements the
SpinnerFramesarray, a tick source, and (separately) a redraw subscription. This is the root of the lazy/frozen probe spinners noted in #1292.Footgun
The naive fix — subscribing to a
SpinnerTickreactive property inside a rebuilt view method — creates a re-entrant redraw loop: R3ReactiveProperty.Subscribefires the current value immediately →InvalidateAndRedraw()→ rebuild → re-subscribe → … It's easy to write, and the interactive smoke harness wouldn't catch it (the local-Ollama probe is near-instant, so the loop has almost no window to run). We nearly shipped exactly this while working on #1292.What already exists
Termina has a self-animating
SpinnerSegment(Termina.Components.Streaming, used by the chat streaming view with anintervalMs), but it lives in the streaming-segment model and isn't usable in theILayoutNode/TextNodeviews the wizard and managers build.Proposal
A reusable, self-animating spinner node for the layout-node model that owns its own animation timer + redraw — so a view drops in something like
new SpinnerNode(style, color, intervalMs)(composed with a text node) with no per-surface tick fields, no copiedSpinnerFrames, and no hand-wired redraw subscription. Then migrate the existing hand-rolled spinners (SkillFeeds, provider step, model manager, provider manager, OAuth flows) onto it.Benefit
SpinnerFramesarrays and per-surface tick/redraw plumbing.Context
Surfaced while fixing #1292 (PR #1311). The cosmetic spinner improvements were intentionally dropped from that PR in favor of this standardized approach.