Skip to content

Commit 10a112d

Browse files
committed
⚡️ perf(cli): render startup header with Textual
Replace the Rich title panel with a Textual Header wired to model/base URL, keep output inline, and make CLI tests stub the header render. Tighten MCP spec typing in CLI commands and ensure tool output formatting remains type-safe. Update CLI guidance to reflect the new header behavior.
1 parent 13ef9cf commit 10a112d

4 files changed

Lines changed: 45 additions & 17 deletions

File tree

meeseeks-cli/AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ Scope: this file applies to the `meeseeks-cli/` package only. It covers the term
1010

1111
## Rendering Pipeline (How we produce output)
1212
- Entry point: `meeseeks-cli/cli_master.py` (`run_cli`).
13-
- Rendering is **Rich**-based and printed via a single `Console` instance.
13+
- Rendering is primarily **Rich**-based and printed via a single `Console` instance.
1414
- High-level sections:
15-
- Ready panel with model/base URL (within the same border).
15+
- Startup header via Textual `Header` plus a ready line with session info.
1616
- Action plan checklist (Panel + Text + Group).
1717
- Tool results as cards (Panel + Columns).
1818
- Response panel (Markdown in a bold border).
@@ -28,7 +28,7 @@ Scope: this file applies to the `meeseeks-cli/` package only. It covers the term
2828
If you change any of these, update this file.
2929

3030
## Dialogs / Prompts (Textual)
31-
We use **Textual** only for interactive dialogs, not for overall output.
31+
We use **Textual** for the startup header and interactive dialogs, not the full CLI output.
3232

3333
Location: `meeseeks-cli/cli_dialogs.py`
3434

meeseeks-cli/cli_commands.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from core.task_master import orchestrate_session # noqa: E402
2424
from core.token_budget import get_token_budget # noqa: E402
25-
from core.tool_registry import ToolRegistry, load_registry # noqa: E402
25+
from core.tool_registry import ToolRegistry, ToolSpec, load_registry # noqa: E402
2626

2727

2828
@dataclass(frozen=True)
@@ -275,8 +275,8 @@ def _refresh_mcp_registry(context: CommandContext) -> None:
275275

276276
def _maybe_select_mcp_specs(
277277
context: CommandContext,
278-
mcp_specs: list[object],
279-
) -> list[object] | None:
278+
mcp_specs: list[ToolSpec],
279+
) -> list[ToolSpec] | None:
280280
if context.prompt_func is None:
281281
return None
282282
dialogs = DialogFactory(console=context.console, prompt_func=context.prompt_func)
@@ -471,7 +471,7 @@ def _handle_model_wizard(
471471
def _render_mcp(
472472
console: Console,
473473
tool_registry: ToolRegistry,
474-
mcp_specs: list[object] | None = None,
474+
mcp_specs: list[ToolSpec] | None = None,
475475
) -> None:
476476
config_path = os.getenv("MESEEKS_MCP_CONFIG")
477477
if config_path and os.path.exists(config_path):

meeseeks-cli/cli_master.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
from rich.console import Console, Group
1515
from rich.markdown import Markdown
1616
from rich.panel import Panel
17+
from rich.status import Status
1718
from rich.syntax import Syntax
1819
from rich.text import Text
20+
from textual.app import App, ComposeResult
21+
from textual.widgets import Header
1922

2023

2124
def _verbosity_to_level(verbosity: int) -> str:
@@ -139,6 +142,27 @@ def _resolve_display_model(model_name: str | None) -> str:
139142
)
140143

141144

145+
class _HeaderApp(App[None]):
146+
"""Minimal Textual app for rendering the CLI header."""
147+
148+
def __init__(self, title: str, subtitle: str) -> None:
149+
super().__init__()
150+
self.title = title
151+
self.sub_title = subtitle
152+
153+
def compose(self) -> ComposeResult:
154+
yield Header(show_clock=False)
155+
156+
def on_mount(self) -> None:
157+
self.set_timer(0, self.exit)
158+
159+
160+
def _render_startup_header(title: str, subtitle: str) -> None:
161+
app = _HeaderApp(title, subtitle)
162+
headless = not (sys.stdin.isatty() and sys.stdout.isatty())
163+
app.run(inline=True, inline_no_clear=True, headless=headless)
164+
165+
142166
def run_cli(args: argparse.Namespace) -> int:
143167
"""Run the CLI application loop.
144168
@@ -161,12 +185,10 @@ def run_cli(args: argparse.Namespace) -> int:
161185
registry = get_registry()
162186

163187
base_url = os.getenv("OPENAI_API_BASE") or os.getenv("OPENAI_BASE_URL")
164-
ready_lines = [
165-
Text("Meeseeks CLI ready"),
166-
Text(f"Model: {_resolve_display_model(state.model_name)}", style="dim"),
167-
Text(f"Base URL: {base_url or '(not set)'}", style="dim"),
168-
]
169-
console.print(Panel(Group(*ready_lines), title="Meeseeks"))
188+
model_name = _resolve_display_model(state.model_name)
189+
subtitle = f"Model: {model_name} • Base URL: {base_url or '(not set)'}"
190+
_render_startup_header("Meeseeks", subtitle)
191+
console.print("Meeseeks CLI ready")
170192
console.print(f"Session: {state.session_id}")
171193
console.print("Type /help for commands.\n")
172194

@@ -255,7 +277,7 @@ def _run_query(
255277
task_queue,
256278
tool_registry,
257279
highlight_latest=not bool(task_queue.task_result),
258-
verbose=args.verbose > 0,
280+
verbose=getattr(args, "verbose", 0) > 0,
259281
)
260282
if task_queue.task_result:
261283
console.print(
@@ -401,6 +423,7 @@ def _render_results_with_registry(
401423

402424

403425
def _format_tool_output(result: object, content_style: str | None) -> Text | Syntax:
426+
style = content_style or ""
404427
if isinstance(result, dict | list):
405428
return Syntax(
406429
json.dumps(result, indent=2, ensure_ascii=True),
@@ -422,15 +445,15 @@ def _format_tool_output(result: object, content_style: str | None) -> Text | Syn
422445
theme="ansi_dark",
423446
word_wrap=True,
424447
)
425-
return Text(result, style=content_style)
426-
return Text(str(result), style=content_style)
448+
return Text(result, style=style)
449+
return Text(str(result), style=style)
427450

428451

429452
def _build_cli_hook_manager(
430453
console: Console,
431454
tool_registry: ToolRegistry,
432455
) -> HookManager:
433-
status_holder: dict[str, object] = {}
456+
status_holder: dict[str, Status] = {}
434457
specs = _tool_specs_by_id(tool_registry)
435458

436459
def _start_spinner(action_step: ActionStep) -> ActionStep:

meeseeks-cli/tests/test_cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ def test_run_cli_single_query(monkeypatch, tmp_path):
221221
auto_approve=False,
222222
)
223223

224+
def fake_header(*args, **kwargs):
225+
return None
226+
224227
def fake_orchestrate(*args, **kwargs):
225228
step = ActionStep(
226229
action_consumer="home_assistant_tool",
@@ -231,6 +234,7 @@ def fake_orchestrate(*args, **kwargs):
231234
task_queue.task_result = "ok"
232235
return task_queue
233236

237+
monkeypatch.setattr("cli_master._render_startup_header", fake_header)
234238
monkeypatch.setattr("cli_master.orchestrate_session", fake_orchestrate)
235239
assert run_cli(args) == 0
236240

@@ -265,6 +269,7 @@ def prompt(self, *args, **kwargs):
265269
self.calls += 1
266270
return "/quit"
267271

272+
monkeypatch.setattr("cli_master._render_startup_header", lambda *args, **kwargs: None)
268273
monkeypatch.setattr("cli_master.FileHistory", DummyHistory)
269274
monkeypatch.setattr("cli_master.PromptSession", lambda *args, **kwargs: DummySession())
270275
assert run_cli(args) == 0

0 commit comments

Comments
 (0)