Files
tracer-cloud--opensre/surfaces/interactive_shell/ui/banner/banner.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

412 lines
14 KiB
Python

"""Splash screen, agent ready-state box, and REPL launch banner.
Three exported entry points
---------------------------
render_splash(console, first_run=False)
Full branded startup screen with ASCII art and optional security gate.
Called once when the CLI starts.
render_ready_box(console, session=None)
DIM-bordered two-column welcome panel:
left → ◉ OpenSRE · provider · model · mode · cwd
right → "Tips for getting started" + "What's new"
Called after the splash and on /clear, /welcome, and greeting aliases.
render_banner(console)
Backward-compatible shim: render_splash + render_ready_box in one call.
Existing callers continue to work unchanged.
Rendered output legend (colour roles)
--------------------------------------
# [HIGHLIGHT] ASCII art lines · ◉ glyph · OpenSRE brand name
# [BRAND] version string · model name · section headers
# [SECONDARY] "opensre" product name label · cwd · tip / note body
# [DIM] subtitle description · rule lines · box chrome · dividers
# [TEXT] provider/model values · greeting
# [WARNING] read-only or trust-mode notice · incomplete-integration marker
"""
from __future__ import annotations
import getpass
import math
import os
import sys
from rich import box
from rich.console import Console, Group
from rich.panel import Panel
from rich.rule import Rule
from rich.table import Table
from rich.text import Text
from config.repl_config import WHATS_NEW
from config.version import get_opensre_version
from platform.terminal.theme import (
BRAND,
DIM,
HIGHLIGHT,
SECONDARY,
TEXT,
WARNING,
_parse_hex_color,
get_active_theme,
)
from surfaces.interactive_shell.ui.banner.banner_state import _build_ambient_right_column
from surfaces.interactive_shell.ui.components.banner_art import _render_art
from surfaces.interactive_shell.ui.tables.provider import detect_provider_model
def _is_first_run() -> bool:
"""True when the wizard has never been completed on this machine."""
try:
from surfaces.cli.wizard.store import get_store_path
return not get_store_path().exists()
except Exception:
return False
# ── Splash screen ─────────────────────────────────────────────────────────────
def _interpolate_hex_color(start: str, end: str, t: float) -> str:
"""Return a hex color linearly interpolated between ``start`` and ``end``."""
start_rgb = _parse_hex_color(start)
end_rgb = _parse_hex_color(end)
clamped = max(0.0, min(1.0, t))
channels = tuple(
int(round(start_rgb[idx] + (end_rgb[idx] - start_rgb[idx]) * clamped)) for idx in range(3)
)
return f"#{channels[0]:02X}{channels[1]:02X}{channels[2]:02X}"
def _splash_block_style(block_index: int, block_total: int) -> str:
"""Return the Rich style for one splash block character."""
theme = get_active_theme()
start = theme.SPLASH_GRADIENT_START
end = theme.SPLASH_GRADIENT_END
if not start or not end:
return f"bold {HIGHLIGHT}"
if block_total <= 1:
return f"bold {_interpolate_hex_color(start, end, 0.0)}"
ratio = block_index / (block_total - 1)
return f"bold {_interpolate_hex_color(start, end, ratio)}"
def render_splash(console: Console | None = None, *, first_run: bool | None = None) -> None:
"""Print the branded startup splash.
Rendered output (with colour roles):
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ [DIM divider]
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋ [HIGHLIGHT art]
...
opensre [SECONDARY] · v<version> [BRAND]
open-source SRE agent for automated incident
investigation and root cause analysis [DIM]
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ [DIM divider]
If first_run (or not set and wizard has never run):
⚠ This tool runs AI-powered commands … [WARNING]
Press Enter to continue… [SECONDARY]
"""
console = console or Console(
highlight=False,
force_terminal=True,
color_system="truecolor",
legacy_windows=False,
)
if first_run is None:
first_run = _is_first_run()
version = get_opensre_version()
art = _render_art(console.width)
console.print()
console.print(Rule(style=DIM))
console.print()
for line in art.splitlines():
t = Text()
t.append(" ")
block_total = line.count("█")
block_index = 0
for ch in line:
if ch == "█":
t.append(ch, style=_splash_block_style(block_index, block_total))
block_index += 1
else:
t.append(ch, style=f"bold {BRAND}")
console.print(t)
console.print()
subtitle = Text()
subtitle.append(" ")
subtitle.append("opensre", style=SECONDARY)
subtitle.append(" · ", style=DIM)
subtitle.append(f"v{version}", style=BRAND)
console.print(subtitle)
desc = Text()
desc.append(
" open-source SRE agent for automated incident investigation and root cause analysis",
style=DIM,
)
console.print(desc)
console.print()
console.print(Rule(style=DIM))
if first_run:
console.print()
notice = Text()
notice.append(" ")
notice.append("⚠ ", style=f"bold {WARNING}")
notice.append(
"This tool executes AI-powered commands against your infrastructure.\n"
" Review the documentation before connecting production systems.\n"
" Source: https://github.com/opensre-dev/opensre",
style=SECONDARY,
)
console.print(notice)
console.print()
if sys.stdin.isatty():
try:
console.print(f" [{SECONDARY}]Press Enter to continue…[/]", end="")
sys.stdin.readline()
except (EOFError, KeyboardInterrupt, OSError):
# Non-interactive stdin or user abort — skip blocking and continue startup.
pass
console.print()
# ── Agent ready-state box ─────────────────────────────────────────────────────
# Static copy for the right column (first-run only). Keep entries terse.
_TIPS: tuple[str, ...] = (
"Paste alert JSON or describe an incident",
"Type /help to list slash commands",
"Run /doctor for environment diagnostics",
"Use /investigate for runnable demos/templates",
)
# Panel geometry. The body switches to a stacked layout on narrow terminals,
# and otherwise expands to fill the full console width while keeping the left
# identity column readable and the right notes column roomy.
_MIN_LEFT_COL_WIDTH = 34
_MAX_LEFT_COL_WIDTH = 48
_MIN_RIGHT_COL_WIDTH = 40
_DIVIDER_WIDTH = 3
_PANEL_PADDING_X = 2
_PANEL_FRAME_WIDTH = 2 + (_PANEL_PADDING_X * 2)
_MIN_TWO_COLUMN_CONTENT_WIDTH = _MIN_LEFT_COL_WIDTH + _DIVIDER_WIDTH + _MIN_RIGHT_COL_WIDTH
# OpenSRE brand mark — single "O" from oh-my-logo tiny font (half-block chars).
_LOGO_MARK_ROWS: tuple[tuple[str, str], ...] = (
("█▀█", ""),
("█▄█", ""),
)
def _github_username() -> str:
"""Return the saved GitHub login for the configured GitHub integration, or "".
Best-effort and never raises: the welcome greeting must render even when the
integration store is unreadable or GitHub is not configured.
"""
try:
from integrations.github.identity import saved_github_username
return saved_github_username()
except Exception:
return ""
def _get_username() -> str:
# Prefer the authenticated GitHub handle once it is known, so the greeting
# reflects the user's GitHub identity rather than the local system account.
github = _github_username()
if github:
return github
try:
return getpass.getuser()
except Exception:
return "there"
def _build_logo_mark() -> Text:
"""Return the brand mark left-aligned (flush with the column's 2-space indent)."""
logo = Text(no_wrap=True)
for index, (body, _echo) in enumerate(_LOGO_MARK_ROWS):
if index:
logo.append("\n")
logo.append(body, style=f"bold {HIGHLIGHT}")
return logo
def _format_cwd(path: str) -> str:
"""Collapse the user's home directory to ~ for a tidier identity line."""
home = os.path.expanduser("~")
if home and (path == home or path.startswith(home + os.sep)):
return "~" + path[len(home) :]
return path
def _build_identity_block(provider: str, model: str, *, trust_mode: bool) -> Text:
"""Left column: mascot · blank · greeting · blank · identity line (all left-aligned)."""
logo = _build_logo_mark()
greeting = Text()
greeting.append(f"Welcome back {_get_username()}!", style=f"bold {TEXT}")
# Single flowing line: model · tier · workspace
cwd = _format_cwd(os.getcwd())
tier = "trust mode" if trust_mode else provider
identity = Text(overflow="fold")
identity.append(model, style=f"bold {BRAND}")
identity.append(" · ", style=DIM)
if trust_mode:
identity.append(tier, style=f"bold {WARNING}")
identity.append(" · ", style=DIM)
else:
identity.append(tier, style=SECONDARY)
identity.append(" · ", style=DIM)
identity.append(cwd, style=SECONDARY)
return Text("\n").join([logo, Text(), Text(), greeting, Text(), Text(), identity])
def _build_notes_block(header_text: str, items: tuple[str, ...]) -> Text:
"""Right column section: bold header followed by dim list items."""
parts: list[Text] = [Text(header_text, style=f"bold {BRAND}")]
for item in items:
parts.append(Text(item, style=SECONDARY, overflow="fold"))
return Text("\n").join(parts)
def _visual_line_count(block: Text, width: int) -> int:
"""Estimate how many terminal lines a Text block will occupy at ``width``."""
safe_width = max(width, 1)
total = 0
for raw_line in block.plain.split("\n"):
total += max(1, math.ceil(max(len(raw_line), 1) / safe_width))
return total
def _vertical_divider(height: int) -> Text:
"""Build a padded vertical rule with ``height`` lines."""
return Text("\n".join(" │ " for _ in range(max(height, 1))), style=DIM, no_wrap=True)
def _two_column_widths(console_width: int) -> tuple[int, int]:
"""Return responsive left/right widths for the ready panel body."""
content_width = max(console_width - _PANEL_FRAME_WIDTH, _MIN_TWO_COLUMN_CONTENT_WIDTH)
left_width = int((content_width - _DIVIDER_WIDTH) * 0.42)
left_width = max(_MIN_LEFT_COL_WIDTH, min(left_width, _MAX_LEFT_COL_WIDTH))
right_width = content_width - _DIVIDER_WIDTH - left_width
if right_width < _MIN_RIGHT_COL_WIDTH:
right_width = _MIN_RIGHT_COL_WIDTH
left_width = content_width - _DIVIDER_WIDTH - right_width
return left_width, right_width
def build_ready_panel(
console: Console | None = None,
*,
session: object = None,
) -> Panel:
"""Build the responsive welcome panel shared by startup and CLI help."""
console = console or Console(
highlight=False,
force_terminal=True,
color_system="truecolor",
legacy_windows=False,
)
provider, model = detect_provider_model()
version = get_opensre_version()
trust_mode: bool = bool(getattr(session, "trust_mode", False))
panel_title = Text()
panel_title.append(" OpenSRE", style=f"bold {HIGHLIGHT}")
panel_title.append(" · ", style=DIM)
panel_title.append(f"v{version} ", style=BRAND)
left = _build_identity_block(provider, model, trust_mode=trust_mode)
if _is_first_run():
right = Text("\n").join(
[
_build_notes_block("Tips for getting started", _TIPS),
Text("───", style=DIM),
_build_notes_block("What's new", WHATS_NEW),
]
)
else:
right = _build_ambient_right_column(session=session)
body: Group | Table
if console.width - _PANEL_FRAME_WIDTH >= _MIN_TWO_COLUMN_CONTENT_WIDTH:
left_width, right_width = _two_column_widths(console.width)
height = max(
_visual_line_count(left, left_width),
_visual_line_count(right, right_width),
)
divider = _vertical_divider(height)
grid = Table.grid(padding=0, expand=False)
grid.add_column(justify="left", vertical="top", width=left_width)
grid.add_column(justify="center", vertical="top", width=_DIVIDER_WIDTH)
grid.add_column(justify="left", vertical="top", width=right_width)
grid.add_row(left, divider, right)
body = grid
else:
body = Group(
left,
Rule(style=DIM),
right,
)
return Panel(
body,
title=panel_title,
title_align="left",
border_style=DIM,
padding=(1, _PANEL_PADDING_X),
expand=True,
box=box.ROUNDED,
)
def render_ready_box(
console: Console | None = None,
*,
session: object = None,
) -> None:
"""Print the two-column welcome panel with an embedded title bar."""
console = console or Console(
highlight=False,
force_terminal=True,
color_system="truecolor",
legacy_windows=False,
)
console.print()
console.print(build_ready_panel(console, session=session))
console.print()
# ── Backward-compatible shim ──────────────────────────────────────────────────
def render_banner(console: Console | None = None) -> None:
"""Render splash + ready-state box in one call (legacy entry point).
Existing callers (main.run_repl) continue to work unchanged.
"""
_console = console or Console(
highlight=False,
force_terminal=True,
color_system="truecolor",
legacy_windows=False,
)
render_splash(_console)
render_ready_box(_console)