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" },