Display Architecture

This maintainer-facing page explains the internal display architecture built around DisplayContext.

If you only need to understand what appears in the terminal during a run, start with the [Streaming Blocks and Long-Content Display](developer-internals.md#streaming-blocks-and-long-content-display) section in developer-internals.md instead.

Overview

Every renderer receives a DisplayContext instead of constructing its own Console or reading environment variables directly. This keeps rendering testable, predictable, and easier to audit.

The DI invariant

The following rules are enforced by tests/display/test_di_invariants.py, which scans every *.py under ralph/display/ at test time:

  • Console( may only appear in ralph/display/theme.py.

  • Theme( may only appear in ralph/display/theme.py.

  • os.environ and os.getenv may only appear in ralph/display/context.py and ralph/display/content_condenser.py.

To opt a line out of the invariant scan, append # noqa: di-allow to it and document why in the same commit.

Single display owner

ParallelDisplay is the only display class in Ralph Workflow. Every public display helper lives in exactly one module (ralph/display/parallel_display.py or ralph/display/context.py) and is re-exported through ralph.display. The complete public surface is:

Symbol

Owner

ParallelDisplay

ralph/display/parallel_display.py

emit_activity_line()

ralph/display/parallel_display.py

resolve_active_display()

ralph/display/parallel_display.py

get_display_context()

ralph/display/parallel_display.py

phase_style_for_phase()

ralph/display/parallel_display.py

status_text()

ralph/display/parallel_display.py

subscriber_for_display()

ralph/display/parallel_display.py

strip_markup()

ralph/display/parallel_display.py

The 42 consolidated emit_* methods on ParallelDisplay (41 instance methods + the module-level emit_activity_line) own every user-facing banner, table, panel, and one-shot status surface. The persistent bottom Status Bar is intentionally outside the emit_* surface: it is composed via the ralph.display.status_bar module (StatusBar, StatusBarModel, and the pure free function render_status_bar), reachable through ParallelDisplay.status_bar, and pushed to via ParallelDisplay.update_status_bar(model). The persistent footer renders on the _STATUS_BAR_REFRESH_PER_SECOND cadence (4.0 Hz / 250 ms) and is gated on a real-TTY run, so non-interactive output stays clean. DisplayContext resolves the terminal background once from its environment; event identities and the footer use the matching dark/light identity palette without each renderer probing the terminal. When background detection is unavailable, they use the dedicated unknown-background palette, which remains legible on both black and white. Simultaneously visible identities are deterministically collision-nudged against the active set, including under the supported color-vision-deficiency simulations. They are grouped by surface below.

Run lifecycle

  • emit_run_start — start-of-run banner with title and project root.

  • emit_run_end — end-of-run recap line with status symbol.

  • emit_parsed_event — turn one parsed transcript event into a log line and (optionally) a banner.

  • emit_analysis_result — render the analysis-cycle result.

Phase banners

  • emit_phase_start — show a phase-start banner from explicit parameters.

  • emit_phase_start_from_entry — show a phase-start banner from a lifecycle entry model.

  • emit_phase_transition — show a phase-transition banner between two phases.

  • emit_phase_close — show a phase-close banner from explicit parameters.

  • emit_phase_close_from_exit — show a phase-close banner from a lifecycle exit model.

  • emit_phase_close_banner — show the rich, model-based phase-close banner.

Artifact renderers

  • emit_plan_artifact — render the plan artifact.

  • emit_development_artifact — render the development artifact.

  • emit_review_artifact — render the review artifact.

  • emit_fix_artifact — render the fix artifact.

  • emit_analysis_decision — render the analysis-decision artifact.

  • emit_commit_message — render the generated commit message.

  • emit_missing_plan_hint — emit the missing-plan hint.

Tables and panels

  • emit_agents_table — render the agents table.

  • emit_providers_table — render the providers table.

  • emit_config_table — render the config table.

  • emit_metrics_table — render the pipeline-metrics table.

  • emit_checkpoint_summary_table — render the checkpoint-summary table.

  • emit_diagnose_inventory_table — render the diagnose inventory table.

  • emit_diagnose_probe_table — render the diagnose probe table.

  • emit_diagnose_servers_table — render the diagnose servers table.

  • emit_capability_summary — render the skill capability summary.

  • emit_info_panel — render a titled info panel.

Status and warnings

  • emit_status — emit a one-line status message.

  • emit_warning — emit a one-line warning (also the error path; uses theme.status.error styling for error text).

  • emit_skill_failure_warning — emit the skills-auto-install failure hint.

  • emit_fallback_next_steps — emit a numbered fallback next-steps list.

First-run and welcome

  • emit_welcome_banner — emit the welcome ASCII banner.

  • emit_first_run_panel — emit the first-run panel.

Helpers

  • emit_blank_line — emit a single blank line.

  • emit_dry_run_summary — emit the dry-run-mode recap block.

This contract is enforced by two test classes:

  • tests.display.test_di_invariants.TestDisplayIsOnlyParallelDisplay in tests/display/test_di_invariants.py (DI seam contract).

  • tests.test_no_anti_drift_regression.TestParallelDisplayOwnsAllDisplayHelpers in tests/test_no_anti_drift_regression.py (anti-drift regression pin).

Single Status Bar owner

The persistent bottom Status Bar is composed by ParallelDisplay and reachable only through pd.status_bar. The lifecycle has exactly one owner:

  • One constructor. StatusBar is instantiated in exactly one site — ralph.display.parallel_display.ParallelDisplay.__init__ (self._status_bar: StatusBar = StatusBar(self)). No other module under ralph/display/, ralph/pipeline/, or ralph/cli/ constructs a StatusBar.

  • One start site. start() is called from exactly one site — ralph.display.parallel_display.ParallelDisplay.start. The pipeline reaches the bar through the production context manager with loop_ctx.active_display: in ralph/pipeline/run_loop.py, which invokes ParallelDisplay.start (and therefore self._status_bar.start()) exactly once per run.

  • One stop site. stop() is called from exactly one site — ralph.display.parallel_display.ParallelDisplay.stop. ParallelDisplay.__exit__ invokes ParallelDisplay.stop, so the Live region is torn down exactly once per run.

  • One push surface. The pipeline pushes models through update_status_bar(), which validates the StatusBarModel and delegates to self._status_bar.update(model). The Live region reads the latest model on each refresh tick (4 Hz by default).

  • One CLI / runtime consumer surface. ralph/cli/**/*.py and ralph/runtime/**/*.py are forbidden from constructing StatusBar or calling _status_bar.start() / _status_bar.stop(); consumers reach the bar through pd.status_bar (the composed accessor on ParallelDisplay) or via active.update_status_bar(...).

This single-owner contract is enforced by tests/display/test_status_bar_single_owner.py (4 AST-based tests covering the constructor, the start() call site, the stop() call site, and the CLI / runtime prohibition).

Verifying the Status Bar runtime

The persistent Status Bar runtime contract is provable through the production entry point. The integration test tests/integration/test_status_bar_runtime_visibility.py enters with pd as active:, pushes a StatusBarModel through the production context manager, and asserts both the observable is_active / last_model slots on pd.status_bar and the captured buffer contents.

Focused regression commands:

cd ralph-workflow
uv run python -m pytest tests/display/test_status_bar.py tests/display/test_single_mode_anti_drift.py tests/display/test_status_bar_single_owner.py tests/integration/test_status_bar_runtime_visibility.py -q -p no:cacheprovider --no-header

uv run python -m pytest tests/pipeline/test_run_loop_status_bar_wiring.py -q -p no:cacheprovider --no-header

Authoritative verification (combined 60-second test budget):

cd ralph-workflow
make verify

No drift in CLI/pipeline display

CLI command modules under ralph/cli/commands/ and pipeline modules under ralph/pipeline/ are forbidden from constructing their own Console instances or from reading environment variables directly once a DisplayContext is in scope. The anti-drift invariant test tests.display.test_di_invariants.TestNoInlineConsoleConstructor walks every *.py under ralph/ (excluding tests/, docs/, and the legitimate ralph/display/theme.py source) and asserts zero inline Console( constructions and zero module-level DisplayContext(...) calls. The companion test tests.display.test_di_invariants.TestNoModuleLevelDisplayContext in tests/test_no_anti_drift_regression.py performs the same scan specifically for DisplayContext materialisation at import time.

The tests.test_no_anti_drift_regression.TestPublicSurfaceImports test pins the public surface by importing all nine canonical symbols from ralph.display and asserting they are all callable or class objects — this catches accidental re-export drift before users notice.

Visual hierarchy

ParallelDisplay emits distinct visual section breaks (a ─── rule in Unicode mode, an ASCII --- fallback otherwise) between run-start, phase-close, and run-end blocks. The rule glyph is sourced from ralph/display/theme.py via glyph_for() so it is substitutable per the existing Okabe-Ito discipline. Quiet mode (is_quiet=True) short-circuits every emit method that owns a banner so no banner or log line leaks when resolve_active_display() is called with is_quiet=True.

The section-rule contract is enforced by tests/display/test_parallel_display_visual_hierarchy.py:

  • Every emit method that opens a section calls self._emit_section_rule(tag) (single default-mode layout always emits section rules).

  • Headers use the theme.banner.title style; body cells use theme.text.muted.

  • Output is markup-free: callers do not need to escape [brackets] or rich markup.

  • The single default-mode layout emits a trailing Rule for visual symmetry around the section block.

Environment variables

The following environment variables influence display behaviour. All are resolved once during make_display_context(); no renderer reads the environment after that.

Width

Variable

Effect

COLUMNS

Positive integer overrides the console’s auto-detected width.

Color

Variable

Effect

NO_COLOR

Any value disables color. Takes precedence over FORCE_COLOR.

FORCE_COLOR

Any value forces color on non-TTY streams.

Glyphs

Variable

Effect

RALPH_FORCE_ASCII

Any truthy value disables Unicode glyphs; ASCII fallbacks are used (e.g. -> instead of , [OK] instead of ).

TERM=dumb

Disables Unicode glyphs via the same fallback path as RALPH_FORCE_ASCII.

Streaming

Variable

Effect

RALPH_STREAMING_DEDUP

Set to 0/false/no/off to disable consecutive-fragment deduplication in streaming blocks.

RALPH_STREAMING_CHECKPOINTS

Set to 0/false/no/off to disable periodic checkpoint lines during long streaming blocks.

Long content

Variable

Effect

RALPH_LONG_CONTENT_SUMMARY

Set to 0/false/no/off to disable fallback-headline generation for long content blocks.

RALPH_LONG_CONTENT_AI_SUMMARY

Set to 0/false/no/off to disable AI-based headline generation for long content blocks.

Responsive Status Bar

Ralph Workflow exposes one display mode. The persistent one-row Status Bar uses a single responsive layout rather than separate narrow and wide modes. Its segment priority is attention, phase, liveness, elapsed time, run position, agent identity, then working directory. Attention reserves its slot while healthy, so a waiting, stalled, retrying, completed, failed, or cancelled state does not shift neighbouring fields.

At 120 columns every segment is shown. At 80 columns the directory is left-elided; at 60 it drops, the phase abbreviates, and the agent remains; at the supported 40-column floor attention, phase, liveness, elapsed time, and position remain. The footer never wraps. Below that floor it uses a plain minimal form until the terminal recovers. Resizing reflows the footer immediately in both width and height; it remains one row on a 12-row viewport.

The liveness cell advances from the injected monotonic clock during quiet work, while the watchdog remains the sole authority for the STALLED state. The live footer refreshes at a bounded cadence so elapsed time can advance; unchanged direct renders remain byte-stable. NO_COLOR and RALPH_FORCE_ASCII preserve the same labels and hierarchy.

The single layout keeps phase, cycle/iter (or round), elapsed time, and identity vocabulary consistent with the live activity feed and rendered record.

Rendered record hierarchy

The text-first .agent/raw/<id>.rendered.log record groups event rows under a readable phase header. The header carries the phase label, cycle/iteration position, and agent identity once. Indented event rows carry a timestamp, body, and role=... marker; healthy info severity is omitted while warnings and errors remain explicit. Tool results name their tool, target, and terminal outcome once; identity remains on the enclosing phase header and previews remain with the corresponding tool call. This keeps the record greppable without repeating chrome on every event. The verbatim .log capture remains the unabridged target for condensation markers. Every supported agent and the generic fallback use this same production path; malformed input becomes an unknown entry with the same hierarchy rather than raw output.

Command-path guard

The main run, commit plumbing, policy check, diagnose, explain, init, prompt-helper, cleanup, star, contribute, smoke, and conflict resolution use ParallelDisplay.emit_* methods for operator-facing output. The smoke command’s literal EXIT_CODE=N line is the single machine-readable exception. tests/display/test_parallel_display_drift_prevention.py and scripts/wt028-drift-check.sh reject new private command output paths.

Iteration context labels

When the pipeline renders phase-start banners, [phase-close] lines, and the final completion panel, it uses a set of canonical iteration labels that appear consistently across all three display surfaces.

Label format

Style

Meaning

Cycle N/cap or Cycle #N

Bold sky-blue (theme.outer_dev)

Outer development cycle number (1-indexed). Shows N/cap when the total budget is known, #N otherwise.

iter N/cap or iter #N

Purple (theme.inner_analysis)

Inner analysis loop iteration. Shows N/cap when the loop cap is known, #N otherwise.

Budget: N left

Bold orange (theme.level.warn)

Remaining invocations allowed by the active budget counter.

These labels are produced by helpers in ralph.display.phase_status (format_dev_cycle, format_analysis_cycle) and consumed via PhaseIterationContext when rendering [phase-close] lines.

Lifecycle view-model

The ralph.display.phase_lifecycle module defines the single source of truth for data flowing through phase-start banners, phase-close after-banners, and the final run summary. Three frozen dataclasses capture the lifecycle:

Class

Used by

PhaseEntryModel

Phase-start banners (show_phase_start family).

PhaseExitModel

Phase-close after-banners (emit_phase_close).

RunCompletionModel

Final run-completion panel and [run-end] transcript block.

All three share the same canonical iteration fields (outer_dev_iteration, outer_dev_cap, inner_analysis, inner_analysis_cap) so every surface expresses iteration context in the same vocabulary derived from ralph.display.phase_status.

Phase-close line format

After each phase ends, a structured [phase-close] line is written to the transcript:

<ISO-TS> INFO META [phase-close] <glyph> phase=<name> [Cycle N/cap] [iter N/cap] <produced> exit=<trigger> (elapsed=Ns, content_blocks=N, thinking_blocks=N, tool_calls=N, errors=N)
  • The <glyph> prefix ( Unicode, * ASCII) appears only for milestone-role phases (execution, review, fix).

  • Canonical iteration labels ([Cycle N/cap] or [Cycle #N], [iter N/cap] or [iter #N], etc.) appear between the phase name and the produced-artifact summary when a PhaseIterationContext is provided.

  • exit=<trigger> (e.g. exit=produced) appears after the artifact summary when an exit_trigger string is supplied to emit_phase_close. Runner code passes exit_trigger="produced" for all artifact-success paths.

  • The trailing counter tuple always appears so every [phase-close] line carries phase-level activity metrics.

Phase-close rich banner

In addition to the [phase-close] transcript line, a rich visual banner is emitted to the console at the start of each phase transition. In the single default-mode layout the banner includes:

  • A artifact: line showing what was produced (e.g. plan: 5 step(s), 2 risk(s)), sourced from artifact_outcome. This line is omitted when the artifact outcome is empty.

  • A stats: line showing per-phase activity counters (content, thinking, tool calls, errors), omitted when all counters are zero.

  • A debug: line showing the last waiting-status breadcrumb and failure category when either is set, to surface failure context without requiring the completion summary to be read.

The runner populates waiting_status_line from the display subscriber and last_failure_category from pipeline state so these breadcrumbs appear even when the phase exits unexpectedly.

See also

The full API reference for all display modules is available in the Python API Reference page, generated from docstrings.