Skip to content

Commit 8e18cd7

Browse files
Merge pull request #433 from math-inc/codex/public-rendering-compat-20260328
fix(cli): improve plain-terminal rendering fallbacks
2 parents abda96d + c487b03 commit 8e18cd7

7 files changed

Lines changed: 507 additions & 63 deletions

File tree

cli.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ def load_cli_config() -> Dict[str, Any]:
502502
VERSION, RELEASE_DATE, GAUSS_AGENT_LOGO, GAUSS_CADUCEUS, COMPACT_BANNER,
503503
build_welcome_banner,
504504
)
505+
from gauss_cli.colors import render_terminal_text, spinner_frames, supports_ansi, supports_unicode
505506
from gauss_cli.commands import COMMANDS, SlashCommandCompleter
506507
from gauss_cli import callbacks as _callbacks
507508
from toolsets import get_all_toolsets, get_toolset_info, resolve_toolset, validate_toolset
@@ -819,7 +820,13 @@ def _rich_text_from_ansi(text: str) -> _RichText:
819820
Using Rich Text.from_ansi preserves literal bracketed text like
820821
``[not markup]`` while still interpreting real ANSI color codes.
821822
"""
822-
return _RichText.from_ansi(text or "")
823+
return _RichText.from_ansi(
824+
render_terminal_text(
825+
text or "",
826+
allow_ansi=supports_ansi(),
827+
allow_unicode=supports_unicode(),
828+
)
829+
)
823830

824831

825832
def _cprint(text: str):
@@ -829,7 +836,16 @@ def _cprint(text: str):
829836
StdoutProxy. Routing through print_formatted_text(ANSI(...)) lets
830837
prompt_toolkit parse the escapes and render real colors.
831838
"""
832-
_pt_print(_PT_ANSI(text))
839+
ansi_enabled = supports_ansi()
840+
rendered = render_terminal_text(
841+
text,
842+
allow_ansi=ansi_enabled,
843+
allow_unicode=supports_unicode(),
844+
)
845+
if ansi_enabled:
846+
_pt_print(_PT_ANSI(rendered))
847+
else:
848+
_pt_print(rendered)
833849

834850

835851
class ChatConsole:
@@ -843,12 +859,15 @@ class ChatConsole:
843859

844860
def __init__(self):
845861
from io import StringIO
862+
846863
self._buffer = StringIO()
864+
self._ansi_enabled = supports_ansi()
847865
self._inner = Console(
848866
file=self._buffer,
849-
force_terminal=True,
850-
color_system="truecolor",
867+
force_terminal=self._ansi_enabled,
868+
color_system="truecolor" if self._ansi_enabled else None,
851869
highlight=False,
870+
no_color=not self._ansi_enabled,
852871
)
853872

854873
def print(self, *args, **kwargs):
@@ -909,7 +928,7 @@ def _build_compact_banner() -> str:
909928
compact_logo = (
910929
skin.banner_logo_compact
911930
if skin and getattr(skin, "banner_logo_compact", "")
912-
else f"[bold {accent}]GAUSS[/] [dim {dim}]· Lean Autoformalization[/]"
931+
else f"[bold {accent}]GAUSS[/] [dim {dim}]{'·' if supports_unicode() else '-'} Lean Autoformalization[/]"
913932
)
914933
return (
915934
f"\n{compact_logo}\n"
@@ -1586,8 +1605,9 @@ def _command_spinner_frame(self) -> str:
15861605
"""Return the current spinner frame for slow slash commands."""
15871606
import time as _time
15881607

1589-
frame_idx = int(_time.monotonic() * 10) % len(_COMMAND_SPINNER_FRAMES)
1590-
return _COMMAND_SPINNER_FRAMES[frame_idx]
1608+
frames = spinner_frames()
1609+
frame_idx = int(_time.monotonic() * 10) % len(frames)
1610+
return frames[frame_idx]
15911611

15921612
@contextmanager
15931613
def _busy_command(self, status: str):
@@ -1596,7 +1616,11 @@ def _busy_command(self, status: str):
15961616
self._command_status = status
15971617
self._invalidate(min_interval=0.0)
15981618
try:
1599-
print(f"⏳ {status}")
1619+
status_line = f"{'⏳' if supports_unicode() else '...'} {status}"
1620+
if self._app:
1621+
_cprint(f"{_DIM}{status_line}{_RST}")
1622+
else:
1623+
print(render_terminal_text(status_line, allow_ansi=False, allow_unicode=supports_unicode()))
16001624
yield
16011625
finally:
16021626
self._command_running = False

gauss_cli/banner.py

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,22 @@
66
import json
77
import logging
88
import os
9+
import re
910
import shutil
1011
import subprocess
1112
import threading
1213
import time
1314
from pathlib import Path
1415
from typing import Dict, List, Any, Optional, Tuple
1516

17+
from rich import box as rich_box
1618
from rich.console import Console
1719
from rich.panel import Panel
1820
from rich.table import Table
1921

2022
from prompt_toolkit import print_formatted_text as _pt_print
2123
from prompt_toolkit.formatted_text import ANSI as _PT_ANSI
24+
from gauss_cli.colors import render_terminal_text, supports_ansi, supports_unicode
2225

2326
logger = logging.getLogger(__name__)
2427

@@ -35,7 +38,16 @@
3538

3639
def cprint(text: str):
3740
"""Print ANSI-colored text through prompt_toolkit's renderer."""
38-
_pt_print(_PT_ANSI(text))
41+
ansi_enabled = supports_ansi()
42+
rendered = render_terminal_text(
43+
text,
44+
allow_ansi=ansi_enabled,
45+
allow_unicode=supports_unicode(),
46+
)
47+
if ansi_enabled:
48+
_pt_print(_PT_ANSI(rendered))
49+
else:
50+
_pt_print(rendered)
3951

4052

4153
# =========================================================================
@@ -73,6 +85,12 @@ def _skin_branding(key: str, fallback: str) -> str:
7385
[#CD7F32]╚██████╔╝██║ ██║╚██████╔╝███████║███████║[/]
7486
[#CD7F32] ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝[/]"""
7587

88+
GAUSS_AGENT_LOGO_ASCII = """[bold #FFD700] ____ _ _ _ ____ ____[/]
89+
[bold #FFD700] / ___| / \\ | | | / ___/ ___|[/]
90+
[#FFBF00]| | _ / _ \\ | | | \\___ \\___ \\[/]
91+
[#FFBF00]| |_| |/ ___ \\| |_| |___) |__) |[/]
92+
[#CD7F32] \\____/_/ \\_\\\\___/|____/____/[/]"""
93+
7694
GAUSS_CADUCEUS = """[#CD7F32]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡀⠀⣀⣀⠀⢀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
7795
[#CD7F32]⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⣿⣿⣇⠸⣿⣿⠇⣸⣿⣿⣷⣦⣄⡀⠀⠀⠀⠀⠀⠀[/]
7896
[#FFBF00]⠀⢀⣠⣴⣶⠿⠋⣩⡿⣿⡿⠻⣿⡇⢠⡄⢸⣿⠟⢿⣿⢿⣍⠙⠿⣶⣦⣄⡀⠀[/]
@@ -89,28 +107,74 @@ def _skin_branding(key: str, fallback: str) -> str:
89107
[#B8860B]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⠈⣡⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
90108
[#B8860B]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]"""
91109

110+
GAUSS_CADUCEUS_ASCII = """[dim #B8860B]Lean workspace and managed proof workflows[/]"""
111+
92112
COMPACT_BANNER = """[bold #FFD700]GAUSS[/] [dim #B8860B]· Lean Autoformalization[/]"""
113+
COMPACT_BANNER_ASCII = """[bold #FFD700]GAUSS[/] [dim #B8860B]- Lean Autoformalization[/]"""
93114

94115
FULL_BANNER_MIN_WIDTH = 124
95116
FULL_STACK_BANNER_MIN_WIDTH = 88
96117
MINI_BANNER_MIN_WIDTH = 34
97118
SIMPLIFIED_BANNER_WIDTH = 72
98119

99120

100-
def _select_banner_art(term_width: int, skin: Optional[Any]) -> Tuple[str, str, str]:
121+
_MARKUP_TAG_RE = re.compile(r"\[[^\]]+\]")
122+
123+
124+
def _markup_is_ascii(markup: str) -> bool:
125+
plain = _MARKUP_TAG_RE.sub("", markup or "")
126+
try:
127+
plain.encode("ascii")
128+
except UnicodeEncodeError:
129+
return False
130+
return True
131+
132+
133+
def _select_banner_art(
134+
term_width: int,
135+
skin: Optional[Any],
136+
unicode_enabled: Optional[bool] = None,
137+
) -> Tuple[str, str, str]:
101138
"""Return ``(layout_mode, logo_markup, hero_markup)`` for the current width."""
102-
full_logo = skin.banner_logo if skin and getattr(skin, "banner_logo", "") else GAUSS_AGENT_LOGO
103-
compact_logo = (
104-
skin.banner_logo_compact
105-
if skin and getattr(skin, "banner_logo_compact", "")
106-
else COMPACT_BANNER
107-
)
108-
full_hero = skin.banner_hero if skin and getattr(skin, "banner_hero", "") else GAUSS_CADUCEUS
109-
compact_hero = (
110-
skin.banner_hero_compact
111-
if skin and getattr(skin, "banner_hero_compact", "")
112-
else ""
113-
)
139+
unicode_enabled = supports_unicode() if unicode_enabled is None else unicode_enabled
140+
if unicode_enabled:
141+
full_logo = skin.banner_logo if skin and getattr(skin, "banner_logo", "") else GAUSS_AGENT_LOGO
142+
compact_logo = (
143+
skin.banner_logo_compact
144+
if skin and getattr(skin, "banner_logo_compact", "")
145+
else COMPACT_BANNER
146+
)
147+
full_hero = skin.banner_hero if skin and getattr(skin, "banner_hero", "") else GAUSS_CADUCEUS
148+
compact_hero = (
149+
skin.banner_hero_compact
150+
if skin and getattr(skin, "banner_hero_compact", "")
151+
else ""
152+
)
153+
else:
154+
full_logo = (
155+
skin.banner_logo
156+
if skin and getattr(skin, "banner_logo", "") and _markup_is_ascii(skin.banner_logo)
157+
else GAUSS_AGENT_LOGO_ASCII
158+
)
159+
compact_logo = (
160+
skin.banner_logo_compact
161+
if skin
162+
and getattr(skin, "banner_logo_compact", "")
163+
and _markup_is_ascii(skin.banner_logo_compact)
164+
else COMPACT_BANNER_ASCII
165+
)
166+
full_hero = (
167+
skin.banner_hero
168+
if skin and getattr(skin, "banner_hero", "") and _markup_is_ascii(skin.banner_hero)
169+
else GAUSS_CADUCEUS_ASCII
170+
)
171+
compact_hero = (
172+
skin.banner_hero_compact
173+
if skin
174+
and getattr(skin, "banner_hero_compact", "")
175+
and _markup_is_ascii(skin.banner_hero_compact)
176+
else ""
177+
)
114178

115179
if term_width >= FULL_BANNER_MIN_WIDTH:
116180
return "split", full_logo, full_hero
@@ -318,6 +382,10 @@ def build_welcome_banner(console: Console, model: str, cwd: str,
318382
"""
319383
term_width = shutil.get_terminal_size().columns
320384
simplified = term_width < SIMPLIFIED_BANNER_WIDTH
385+
unicode_enabled = supports_unicode()
386+
separator = "·" if unicode_enabled else "|"
387+
long_dash = "—" if unicode_enabled else "-"
388+
warn_mark = "⚠" if unicode_enabled else "!"
321389

322390
# Resolve skin colors once for the entire banner
323391
accent = _skin_color("banner_accent", "#FFBF00")
@@ -332,7 +400,7 @@ def build_welcome_banner(console: Console, model: str, cwd: str,
332400
except Exception:
333401
_bskin = None
334402

335-
layout_mode, _logo, _hero = _select_banner_art(term_width, _bskin)
403+
layout_mode, _logo, _hero = _select_banner_art(term_width, _bskin, unicode_enabled=unicode_enabled)
336404

337405
layout_table = Table.grid(padding=(0, 1 if layout_mode == "stack" else 2))
338406
if layout_mode == "split":
@@ -350,12 +418,12 @@ def build_welcome_banner(console: Console, model: str, cwd: str,
350418
model_short = model_short[: model_max - 3] + "..."
351419
ctx_str = ""
352420
if context_length and not simplified:
353-
ctx_str = f" [dim {dim}]·[/] [dim {dim}]{_format_context_length(context_length)} context[/]"
421+
ctx_str = f" [dim {dim}]{separator}[/] [dim {dim}]{_format_context_length(context_length)} context[/]"
354422
if simplified:
355-
left_lines.append(f"[{accent}]{model_short}[/] [dim {dim}]·[/] [dim {dim}]Math Inc.[/]")
423+
left_lines.append(f"[{accent}]{model_short}[/] [dim {dim}]{separator}[/] [dim {dim}]Math Inc.[/]")
356424
left_lines.append(f"[dim {dim}]{_shorten_middle(cwd, max(18, term_width - 10))}[/]")
357425
else:
358-
left_lines.append(f"[{accent}]{model_short}[/]{ctx_str} [dim {dim}]·[/] [dim {dim}]Math Inc.[/]")
426+
left_lines.append(f"[{accent}]{model_short}[/]{ctx_str} [dim {dim}]{separator}[/] [dim {dim}]Math Inc.[/]")
359427
left_lines.append(f"[dim {dim}]{cwd}[/]")
360428
if project_label:
361429
left_lines.append(f"[dim {dim}]Project: {project_label}[/]")
@@ -376,13 +444,13 @@ def build_welcome_banner(console: Console, model: str, cwd: str,
376444
right_lines.append(f"[{text}]`/help`[/] [dim {dim}]commands and diagnostics[/]")
377445
else:
378446
right_lines.append(f"[bold {accent}]Primary Workflow[/]")
379-
right_lines.append(f"[{text}]`/project`[/] [dim {dim}] create, convert, inspect, or switch the active project[/]")
380-
right_lines.append(f"[{text}]`/prove`[/] [dim {dim}] spawn a guided managed proving agent[/]")
381-
right_lines.append(f"[{text}]`/draft`[/] [dim {dim}] draft Lean declaration skeletons[/]")
382-
right_lines.append(f"[{text}]`/autoprove`[/] [dim {dim}] spawn an autonomous managed proving agent[/]")
383-
right_lines.append(f"[{text}]`/formalize`[/] [dim {dim}] spawn an interactive managed formalization agent[/]")
384-
right_lines.append(f"[{text}]`/autoformalize`[/] [dim {dim}] spawn an autonomous managed formalization agent[/]")
385-
right_lines.append(f"[{text}]`/help`[/] [dim {dim}] session and diagnostics commands[/]")
447+
right_lines.append(f"[{text}]`/project`[/] [dim {dim}]{long_dash} create, convert, inspect, or switch the active project[/]")
448+
right_lines.append(f"[{text}]`/prove`[/] [dim {dim}]{long_dash} spawn a guided managed proving agent[/]")
449+
right_lines.append(f"[{text}]`/draft`[/] [dim {dim}]{long_dash} draft Lean declaration skeletons[/]")
450+
right_lines.append(f"[{text}]`/autoprove`[/] [dim {dim}]{long_dash} spawn an autonomous managed proving agent[/]")
451+
right_lines.append(f"[{text}]`/formalize`[/] [dim {dim}]{long_dash} spawn an interactive managed formalization agent[/]")
452+
right_lines.append(f"[{text}]`/autoformalize`[/] [dim {dim}]{long_dash} spawn an autonomous managed formalization agent[/]")
453+
right_lines.append(f"[{text}]`/help`[/] [dim {dim}]{long_dash} session and diagnostics commands[/]")
386454
right_lines.append(f"[dim {dim}]Bundled skills and user-managed MCP are off by default.[/]")
387455

388456
try:
@@ -405,16 +473,16 @@ def build_welcome_banner(console: Console, model: str, cwd: str,
405473
"/help",
406474
]
407475
summary_parts.append("/help for commands")
408-
right_lines.append(f"[dim {dim}]{' · '.join(summary_parts)}[/]")
476+
right_lines.append(f"[dim {dim}]{f' {separator} '.join(summary_parts)}[/]")
409477

410478
# Update check — use prefetched result if available
411479
try:
412480
behind = get_update_result(timeout=0.5)
413481
if behind and behind > 0:
414482
commits_word = "commit" if behind == 1 else "commits"
415483
right_lines.append(
416-
f"[bold yellow] {behind} {commits_word} behind[/]"
417-
f"[dim yellow] run [bold]gauss update[/bold] to update[/]"
484+
f"[bold yellow]{warn_mark} {behind} {commits_word} behind[/]"
485+
f"[dim yellow] {long_dash} run [bold]gauss update[/bold] to update[/]"
418486
)
419487
except Exception:
420488
pass # Never break the banner over an update check
@@ -435,6 +503,7 @@ def build_welcome_banner(console: Console, model: str, cwd: str,
435503
title=f"[bold {title_color}]{agent_name} v{VERSION} ({RELEASE_DATE})[/]",
436504
border_style=border_color,
437505
padding=(0, 2),
506+
box=rich_box.ROUNDED if unicode_enabled else rich_box.ASCII,
438507
)
439508

440509
console.print()

0 commit comments

Comments
 (0)