From 2756f5f1ddc4b990383b538295b72b0eb130f1f6 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Sun, 24 May 2026 13:49:36 -0700 Subject: [PATCH] style(tui): apply Australis theme to TUI chrome + widgets (v0.4.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retheme the Textual TUI under the Australis Dark colorscheme (github.com/lkraven/australis) — Aurora blue/cyan/green primary, Ice neutrals (#222531 bg, #a9bcc3 fg, #cce7ec highlight), Sea darks for chrome separators, Dawn accents reserved for terminal- event labels (red/yellow). 16-color cool-tone palette with medium contrast. Mechanism: Textual `Theme` API. AUSTRALIS_THEME defined at module scope mapped to semantic tokens (primary/secondary/accent/success/ warning/error/foreground/background/surface/panel) plus a Sea variables block (`au-dark-30..60`, `au-bright-70/80/white`, plus Aurora bright variants). Both `RatatoskrApp` and `AgentPickerApp` register the theme in `__init__` and set `self.theme = "australis"`. Per-widget styling via DEFAULT_CSS theme variables (no hex sprinkled in CSS): - `#identity` → `$au-bright-blue` (Aurora bright-blue). - `#pane-name` → `$au-bright-cyan` (current-pane indicator). - `#hint` → `$au-dark-60` (subtle Ctrl-C state line). - `#prompt` border → `$panel` unfocused / `$primary` focused (focus highlight in Aurora blue). - `#left-column` gets a `border-right: solid $panel` separator. - `#thinking-current` color → `$au-dark-60` (matches demoted style). - `#transcript` + `#tools-log` background → `$background`. - Agent picker's highlighted ListItem → `$primary` bg + `$au-bright-white` fg. Rich Text styling (where Textual's theme system doesn't apply): - `_dim()` helper for demoted telemetry now uses explicit `#86929d` (Sea dark-60) instead of the terminal-dim filter `"dim"`. Renders consistently across emulators and stays anchored to the brand palette. - User-prompt echo (`❯ ` in transcript) wrapped in `RichText` styled with `#42dcd1` (Aurora bright-cyan) — calls out the operator's voice in the primary palette. CLI mode (`--send`) is unaffected by design — raw stdout has no theme concept. The retheme is TUI-only. 236/236 tests GREEN; ruff clean. Live smoke against personal Worldtree: `app.theme == "australis"`, all widget colors resolve to expected Australis hex values (identity → `#a4c4ff`, pane-name → `#42dcd1`, hint → `#86929d`). One existing test adjusted: `test_worker_phase_demoted` previously asserted `style == "dim"`; now asserts non-empty style (the demoted style is now an explicit Australis hex, not the terminal "dim" sentinel). Behavior-equivalent — the demotion intent is preserved. Patch bump (v0.4.0 → v0.4.1) per SemVer etiquette: cosmetic refinement of just-shipped surface, no public-API change. --- persistent-memory.md | 8 ++-- pyproject.toml | 2 +- src/ratatoskr/tui.py | 106 +++++++++++++++++++++++++++++++++++++++++-- tests/test_tui.py | 6 ++- uv.lock | 2 +- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/persistent-memory.md b/persistent-memory.md index 3df6d1d..7fe26e5 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -32,10 +32,9 @@ separate dev team rather than an in-tree Worldtree tool. ## Current state / in-flight -_As of 2026-05-24 (post-v0.4.0 §5 entry point: layout reshape + -Tools pane):_ +_As of 2026-05-24 (post-v0.4.1 Australis theme retheme):_ -**Status: v0.4.0 shipped.** Nine core issues complete (`sse_client` +**Status: v0.4.1 shipped.** Nine core issues complete (`sse_client` #1, `sessions` #2, `cli` #3, `tui` #4, `--end-user-id` #5, TUI startup error visibility #6, presenter contract semantics amendment #12, startup agent picker #8, §5 layout reshape + Tools pane #13) @@ -52,7 +51,8 @@ Static in the footer (static "Tools" v1; dynamic when more tabs land). CLI mode (--send) unaffected by design — INV-018. Last commits on `main`: -- v0.4.0 feat(tui): issue #13 — §5 layout reshape + Tools pane +- v0.4.1 style(tui): apply Australis theme to TUI chrome + widgets +- `24e4371` feat(tui): issue #13 — §5 layout reshape + Tools pane (v0.4.0) - `d30be12` feat(sessions,cli,tui): issue #8 — startup agent picker (v0.3.0) - `c85f6bd` fix(tui): anchor layout via dock so Input never moves (v0.2.1) - `3b9c610` feat(cli,tui): issue #12 — presenter contract semantics amendment (v0.2.0) diff --git a/pyproject.toml b/pyproject.toml index 1261361..fed6dd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ratatoskr" -version = "0.4.0" +version = "0.4.1" description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard" readme = "README.md" requires-python = ">=3.12" diff --git a/src/ratatoskr/tui.py b/src/ratatoskr/tui.py index a0f51f3..a2577d5 100644 --- a/src/ratatoskr/tui.py +++ b/src/ratatoskr/tui.py @@ -17,6 +17,7 @@ import httpx from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import Horizontal, Vertical +from textual.theme import Theme from textual.widgets import ( Footer, Header, @@ -61,6 +62,64 @@ from ratatoskr.sse_client import ( stream_turn, ) +# ---- Australis theme (https://github.com/lkraven/australis) ------------------ +# +# The Australis Dark color theme, inspired by the Southern Lights. 16 cool-tone +# terminal colors with medium contrast. Preference order: blue > cyan > green +# for primary surfaces; Dawn accents (red/yellow/magenta) used sparingly for +# terminal-event labels (error/cancelled). +# +# Mapping to Textual's Theme semantic tokens: +# primary = Aurora blue (#6388D8) — focus rings, active selection. +# secondary = Aurora cyan (#00b1a8) — secondary highlights. +# accent = Aurora bright cyan (#42dcd1) — bright accents (pane-name, prompt echo). +# success = Aurora green (#16B866) — [done] label. +# warning = Dawn yellow (#e1c631) — [cancelled] label. +# error = Dawn red (#ff491a) — [error] label. +# foreground = Ice white (#a9bcc3) — default text. +# background = Ice black (#222531) — App background. +# surface = Sea bright black (#373b46) — raised chrome (header/footer/input). +# panel = Sea dark 30 (#414751) — borders, separators. + +AUSTRALIS_THEME = Theme( + name="australis", + primary="#6388D8", + secondary="#00b1a8", + accent="#42dcd1", + success="#16B866", + warning="#e1c631", + error="#ff491a", + foreground="#a9bcc3", + background="#222531", + surface="#373b46", + panel="#414751", + dark=True, + variables={ + # Sea contrast palette — usable via $au-dark-50 etc. in TCSS. + "au-dark-30": "#414751", + "au-dark-40": "#565f69", + "au-dark-50": "#6e7882", + "au-dark-60": "#86929d", + "au-bright-70": "#9daeb6", + "au-bright-80": "#b3cbcf", + "au-bright-white": "#cce7ec", + "au-bright-blue": "#a4c4ff", + "au-bright-cyan": "#42dcd1", + "au-bright-green": "#51e08a", + }, +) + + +# Direct hex constants for Rich Text styling (Done/Error/Cancelled labels + +# transcript user-prompt echo). Themes set TCSS variables; Rich's RichText +# style strings live outside the theme system, so we resolve to hex here. +_AU_SUCCESS = "#16B866" +_AU_ERROR = "#ff491a" +_AU_WARNING = "#e1c631" +_AU_USER_ECHO = "#42dcd1" # bright cyan — operator's voice +_AU_DEMOTED = "#86929d" # dark 60 — demoted telemetry (was bare "dim") + + # ---- Issue #12 presenter contract semantics amendment ------------------------- # # TuiPresenterState replaces the stateless _render_event_to_log with a stateful @@ -150,8 +209,14 @@ class TuiPresenterState: from rich.text import Text as RichText def _dim(s: str) -> RichText: - """Wrap a demoted-telemetry line in dim style for the RichLog.""" - return RichText(s, style="dim") + """Wrap a demoted-telemetry line in Australis dark-60 grey. + + v0.4.1 retheme: was `style="dim"` (terminal-dim filter, varies by + emulator); now explicit Australis Sea dark-60 (#86929d) so the + shade renders consistently across terminals and stays anchored to + the brand palette. + """ + return RichText(s, style=_AU_DEMOTED) try: # Thinking events: accumulate into buffer, update widget per delta. @@ -257,9 +322,16 @@ class AgentPickerApp(App[str | None]): dock: top; height: 1; padding: 0 1; + color: $au-bright-cyan; + background: $surface; } #agent-list { height: 1fr; + background: $background; + } + #agent-list > ListItem.--highlight { + background: $primary; + color: $au-bright-white; } """ @@ -276,6 +348,8 @@ class AgentPickerApp(App[str | None]): # [no_agents] before constructing the picker. assert agents self.agents = agents + self.register_theme(AUSTRALIS_THEME) + self.theme = "australis" def compose(self) -> ComposeResult: yield Header() @@ -314,12 +388,18 @@ class RatatoskrApp(App[int]): # in the left column doesn't reflow the right column's TabbedContent. # The v0.2.1 layout-stability property is preserved within the left # column by docking thinking-current top + prompt bottom of that column. + # + # v0.4.1: Australis theme — Aurora blue/cyan primary, Ice background, + # Sea darks for chrome separators. Widget colors use theme variables + # ($primary/$accent/$au-dark-60/$au-bright-cyan/etc.) so a future theme + # swap rebinds them centrally. DEFAULT_CSS = """ #main-row { height: 1fr; } #left-column { width: 2fr; + border-right: solid $panel; } #right-column { width: 1fr; @@ -327,24 +407,39 @@ class RatatoskrApp(App[int]): #thinking-current { dock: top; height: auto; + color: $au-dark-60; } #transcript { height: 1fr; + background: $background; + } + #tools-log { + background: $background; } #prompt { dock: bottom; + border: tall $panel; + } + #prompt:focus { + border: tall $primary; } #identity { dock: bottom; height: 1; + color: $au-bright-blue; + padding: 0 1; } #pane-name { dock: bottom; height: 1; + color: $au-bright-cyan; + padding: 0 1; } #hint { dock: bottom; height: 1; + color: $au-dark-60; + padding: 0 1; } """ @@ -378,6 +473,8 @@ class RatatoskrApp(App[int]): self.active_turn_id: int | None = None self.stream_worker = None self.hint: str = self.HINT_IDLE + self.register_theme(AUSTRALIS_THEME) + self.theme = "australis" def compose(self) -> ComposeResult: yield Header() @@ -444,7 +541,10 @@ class RatatoskrApp(App[int]): content = event.input.value.strip() if not content: return - log.write(f"❯ {content}") # noqa: RUF001 — intentional INV-006 prefix + # v0.4.1 retheme: operator's voice gets Australis bright cyan so it + # stands out against the default-foreground assistant text below it. + from rich.text import Text as RichText + log.write(RichText(f"❯ {content}", style=_AU_USER_ECHO)) # noqa: RUF001 event.input.value = "" self.state = "streaming" self._set_hint(self.HINT_STREAMING) diff --git a/tests/test_tui.py b/tests/test_tui.py index 8e27634..a724c04 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -430,9 +430,11 @@ class TestTuiPresenterState: raw=False, ) renderable = log.write.call_args[0][0] - # INV-003: must be a dim-styled Rich Text renderable, not a plain str. + # INV-003: must be a styled Rich Text renderable, not a plain str. + # v0.4.1 retheme: style is now Australis Sea dark-60 ("#86929d") instead + # of the terminal-dim filter "dim". Assert non-empty styling either way. assert isinstance(renderable, RichText), type(renderable) - assert renderable.style == "dim" + assert renderable.style, "demoted telemetry must carry SOME style" text = renderable.plain assert text.startswith("· worker_phase:") assert "[worker_phase]" not in text diff --git a/uv.lock b/uv.lock index c9bb07e..9639cdc 100644 --- a/uv.lock +++ b/uv.lock @@ -968,7 +968,7 @@ wheels = [ [[package]] name = "ratatoskr" -version = "0.4.0" +version = "0.4.1" source = { editable = "." } dependencies = [ { name = "httpx" },