Compare commits

...

4 Commits

Author SHA1 Message Date
Dhruv Gupta 5f6f7a0f75 refactor(cli): drop the per-command ✦ brandmark; banner on landing only
Remove the compact `✦ omnigent` line from version/upgrade/server status/
host status/config list — those commands print unbranded again so the CLI
stays quiet and scriptable. The full Otto + wordmark lockup stays on the
landing surfaces only: `omnigent --help`, `omnigent setup`, and the
installer. The print_brandmark helper remains available for opt-in use but
is no longer wired onto any command. Contract doc updated to match.
2026-06-17 20:46:09 +00:00
Dhruv Gupta 7880dec565 fix(cli): grow wordmark to 5 rows, aligned 1:1 with Otto
Use the full-height ANSI-Shadow font (the canonical figlet form, as used
by NeonX) with just one duplicate body row dropped — 5 rows, matching
Otto's height so the lockup pairs 1:1 with no unpaired rows. Taller and
fully legible. Updates the installer banner and tests to match.
2026-06-17 20:32:04 +00:00
Dhruv Gupta ff71530b1e fix(cli): make wordmark 4 rows so g/e are legible
The 3-row ANSI-Shadow squash dropped the middle bars that distinguish
'g' and 'e', leaving them unreadable. Keep each letter's identity row
(rows 0,2,4,5 of the source font) for a 4-row wordmark — slightly taller,
fully legible — and sit it on Otto rows 1-4 so its drop-shadow grounds
on Otto's feet. Updates the installer banner and tests to match.
2026-06-17 20:21:48 +00:00
Dhruv Gupta 073eac5d92 feat(cli): unify terminal output contract + add omnigent wordmark
Adds a single, documented styling layer so every `omnigent` command
reads as one coherent, branded product, and a bold "ANSI-Shadow"
omnigent wordmark paired with the Otto mascot.

New modules:
- omnigent/inner/wordmark.py — the brand art: a regenerable per-letter
  glyph map for the 3-row ANSI-Shadow wordmark, the Otto lockup
  (gradient/tagline/epilogue), and the one-line `✦ omnigent` brandmark.
- omnigent/inner/ui.py — shared consoles, brand Theme/palette, status
  helpers (step/success/info → stdout, warn/error → stderr), structure
  helpers (header/kv/rule/table/panel), and TTY-gated banner helpers
  (OMNIGENT_NO_BANNER honored).
- designs/CLI_CONTRACT.md — the contract: palette, helper API, the
  stdout-is-data / stderr-is-decoration rule, gating, new-command checklist.

Wiring:
- Full lockup on `omnigent --help` and `omnigent setup`; compact brandmark
  on upgrade / server status / host status / config list (text mode only).
- Runner-startup spinner recolored to the brand accent.
- Installer (scripts/install_oss.sh): magenta Otto+wordmark banner, palette
  unified from cyan to brand magenta, TTY-gated.

All decoration is on stderr and TTY-gated, so piped/`--json`/`| cat`
output stays byte-clean; protocol/IPC stdout is untouched. Colored
click.secho call sites routed through the shared helpers.

Tests: tests/inner/test_wordmark.py, tests/inner/test_ui.py.

Co-authored-by: Isaac
2026-06-17 18:47:59 +00:00
12 changed files with 849 additions and 31 deletions
+127
View File
@@ -0,0 +1,127 @@
# Omnigent CLI output contract
This is the contract every `omnigent` command follows when it writes to a
terminal, so the whole CLI reads as one coherent, branded product. The
runtime lives in two small modules:
- **`omnigent/inner/ui.py`** — the styling layer: shared consoles, the
brand palette/theme, and the status/structure helpers. This is the
module command code should import.
- **`omnigent/inner/wordmark.py`** — the brand art: the Otto + "omnigent"
wordmark lockup and the compact one-line brandmark. Imported by `ui`.
The interactive REPL header keeps its own builder
(`omnigent/inner/banner.py`) — that's the live-session box, not part of
this non-interactive contract.
## The one rule: stdout is data, stderr is decoration
Everything else follows from this:
- **stdout** carries machine-readable output — IDs, paths, config dumps,
the `version` string, anything a script might parse. Use `ui.console`
(or `click.echo`) for it.
- **stderr** carries decoration and diagnostics — warnings, errors, the
brand banner, spinners, progress. Use `ui.err_console` / `ui.warn` /
`ui.error` / the banner helpers.
So `omnigent version | cat`, `omnigent config list | jq`, and piped
one-shot output stay byte-clean, while the human at a terminal still gets
color and branding on stderr.
**Never** hand-roll raw ANSI escapes or call `click.secho(fg=...)` in new
code. **Never** print the banner or status decoration to stdout.
## Palette
One brand accent; semantic colors stay conventional. Defined as a
`rich.theme.Theme` in `ui.py`:
| Token | Color | Use |
| -------------- | ------------------ | ------------------------------------ |
| `omni.accent` | `#F43BA6` magenta | Brand — wordmark, headers, `==>`, spinner |
| `omni.success` | green | Success / done |
| `omni.warning` | yellow | Warnings (stderr) |
| `omni.error` | red | Errors (stderr) |
| `omni.info` | cyan | Informational |
| `omni.muted` | dim | Metadata, secondary text |
`#F43BA6` is Otto's magenta — the single source is
`omnigent.inner.mascots.MASCOT_ART_COLOR`, re-exported as `ui.ACCENT`.
The `scripts/install_oss.sh` installer mirrors the same accent
(`\033[38;2;244;59;166m`) so the installer and the tool agree.
## Helper API (`omnigent.inner.ui`)
Status lines — consistent glyph + color, correct stream:
```python
ui.step("Installing Omnigent") # ==> accent (stdout)
ui.success("Verified omnigent") # ✓ green (stdout)
ui.info("Using ~/.omnigent") # · dim (stdout)
ui.warn("tmux not found") # ! yellow (stderr)
ui.error("uv is required") # ✗ red (stderr)
```
Messages are emitted verbatim (never reparsed as rich markup), so a
message containing `[...]` is safe.
Structure:
```python
ui.header("Configured credentials") # bold accent section header
ui.kv("Session", "New session") # aligned label / value row
ui.rule("Setup") # horizontal accent rule
tbl = ui.table(title="Hosts"); ...; ui.console.print(tbl) # branded Table
ui.console.print(ui.panel(body, title="Note")) # branded Panel
```
Raw streams when you need them: `ui.console` (stdout), `ui.err_console`
(stderr).
## When to show the banner
Banner output is drawn on stderr and TTY-gated by `ui.show_banner()` (a
no-op off a TTY or when `OMNIGENT_NO_BANNER` is set):
- **Full lockup** — `ui.print_landing(...)` — Otto + wordmark, optional
gradient / tagline / epilogue. The hero moment, reserved for the few
landing surfaces:
- `omnigent --help` (the top-level group, via `_OmnigentCLI.format_help`)
- `omnigent setup` (first-run experience)
- the installer banner
- **Nothing** — every other command. Regular commands (`version`,
`upgrade`, `server status`, `config list`, …) print their output
unbranded so the CLI stays quiet and scriptable. We deliberately do
*not* sprinkle a brandmark on individual commands.
A compact one-line brandmark helper (`ui.print_brandmark(subtitle=...)`,
`✦ omnigent`) exists for opt-in use, but is intentionally **not** wired
onto any command today — add it only if a specific surface clearly wants
light branding.
The bare `omnigent` invocation on a TTY launches the REPL (its own
branded header); it only falls back to `--help` when non-interactive, so
the landing banner naturally appears there.
## Gating & environment
| Condition | Effect |
| ---------------------------- | ------------------------------------------ |
| stdout/stderr not a TTY | No banner; rich emits no color (data clean)|
| `NO_COLOR` set | rich renders monochrome (art still shows) |
| `OMNIGENT_NO_BANNER` truthy | No banner/brandmark even on a TTY |
| `OMNIGENT_NO_SPINNER` truthy | No startup spinner (pre-existing) |
## Adding a new command — checklist
1. `from omnigent.inner import ui` (import lazily inside the command body
if the module is import-cost sensitive).
2. Print **data** to stdout via `ui.console.print` / `click.echo`.
3. Print **status** via `ui.step/success/info`; **problems** via
`ui.warn/error` (these go to stderr automatically).
4. Build tables/panels with `ui.table()` / `ui.panel()`.
5. Add a banner only if the command is a landing/first-run surface
(`print_landing`) or a read-only branded command (`print_brandmark`).
Leave it off scripted/data commands.
6. No raw ANSI, no `click.secho(fg=...)`, no decoration on stdout.
+3 -1
View File
@@ -177,6 +177,8 @@ def runner_startup_progress(
from rich.live import Live
from rich.spinner import Spinner
from omnigent.inner.mascots import MASCOT_ART_COLOR
# ``Console(stderr=True)`` keeps the spinner off stdout so piped
# one-shot output (``omnigent run … -p "…"``) stays clean.
# ``transient=True`` erases the spinner line on stop. We drive a
@@ -190,7 +192,7 @@ def runner_startup_progress(
# and a CPR handshake — isn't disturbed by a stream proxy being
# torn down a frame before the prompt's first paint.
console = Console(stderr=True)
spinner = Spinner("dots", text=initial_message)
spinner = Spinner("dots", text=initial_message, style=MASCOT_ART_COLOR)
live = Live(
spinner,
console=console,
+43 -13
View File
@@ -43,6 +43,7 @@ from omnigent.host.local_server import (
stop_local_omnigent_server,
stop_untracked_local_server,
)
from omnigent.inner import ui
from omnigent.onboarding.sandboxes import available_providers as _sandbox_providers
from omnigent.onboarding.ucode_setup import (
build_ucode_configure_command,
@@ -507,7 +508,7 @@ def _resolve_first_run_plan() -> _FirstRunPlan | None:
plan = _pick_first_run_harness()
if plan is None:
click.secho("Found no harnesses configured.", fg="yellow", err=True)
ui.warn("Found no harnesses configured.")
_run_configure_harnesses_interactive()
plan = _pick_first_run_harness()
return plan
@@ -1107,7 +1108,34 @@ def _print_version_callback(ctx: click.Context, _param: click.Parameter, value:
ctx.exit()
@click.group()
class _OmnigentCLI(click.Group):
"""Top-level group that prints the brand lockup above its help.
The Otto + wordmark lockup is drawn on stderr (decoration) and is
TTY-gated by :func:`omnigent.inner.ui.show_banner`, so ``omnigent
--help`` shows the banner interactively while piped/CI help stays
clean. Only the top-level group overrides help; subcommand help
(``omnigent run --help``) is untouched.
"""
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
from omnigent.inner import ui
if ui.show_banner():
import importlib.metadata
try:
version = importlib.metadata.version("omnigent")
except importlib.metadata.PackageNotFoundError: # pragma: no cover
version = ""
epilogue = [("Get started", "omnigent setup")]
if version:
epilogue.insert(0, ("Version", version))
ui.print_landing(tagline="all your agents, one cli", epilogue=epilogue)
super().format_help(ctx, formatter)
@click.group(cls=_OmnigentCLI)
@click.option(
"--version",
is_flag=True,
@@ -6801,7 +6829,8 @@ def _warn_missing_harness_dependencies() -> None:
``codex`` do need both, hence the prominent notice.
:returns: None. Side effect: writes a yellow warning block to stderr
via :func:`click.secho` when one or more dependencies are missing.
via :mod:`omnigent.inner.ui` when one or more dependencies are
missing.
"""
problems: list[str] = []
node_problem = _node_dependency_problem()
@@ -6815,20 +6844,16 @@ def _warn_missing_harness_dependencies() -> None:
)
if not problems:
return
click.secho(
"\nExternal tooling needed for some harnesses is missing or outdated:",
fg="yellow",
bold=True,
err=True,
)
ui.err_console.print()
ui.warn("External tooling needed for some harnesses is missing or outdated:")
for problem in problems:
click.secho(f"{problem}", fg="yellow", err=True)
click.secho(
ui.err_console.print(f"{problem}", style="omni.warning", markup=False)
ui.err_console.print(
"You can still configure credentials — the pure-Python openai-agents harness "
"runs without these — but install them before `omnigent claude` / "
"`omnigent codex` or the Pi harness.\n",
fg="yellow",
err=True,
style="omni.warning",
markup=False,
)
@@ -8652,6 +8677,11 @@ def setup(internal_beta: bool) -> None:
``omnigent config list``.) Pass ``--internal-beta`` to configure
Databricks internal-beta defaults and authentication instead.
"""
from omnigent.inner import ui
# Brand lockup at the top of the first-run experience (TTY-gated).
ui.print_landing(tagline="all your agents, one cli")
if internal_beta:
# The internal-beta workspace defaults are excluded from the public OSS
# build. Fail loud with a clear message instead of an ImportError deep
+10 -4
View File
@@ -17,6 +17,7 @@ from pathlib import Path
import click
from omnigent.inner import ui
from omnigent.onboarding.sandboxes import (
SandboxLauncher,
available_providers,
@@ -139,9 +140,12 @@ def _print_ready_banner(provider: str, sandbox_id: str, server_url: str) -> None
:param server_url: Server URL for the connect hint (``--server``
is required on create, so it is always known here).
"""
click.secho("\n✓ Sandbox ready.\n", fg="green", bold=True)
click.echo(f"Sandbox: {sandbox_id} (provider: {provider})")
click.echo(f"Server: {server_url}\n")
ui.console.print()
ui.success("Sandbox ready.")
ui.console.print()
ui.kv("Sandbox", f"{sandbox_id} (provider: {provider})")
ui.kv("Server", server_url)
ui.console.print()
click.echo("To register the sandbox as a host with your server:")
click.echo(
f" omnigent sandbox connect --provider {provider} --sandbox-id {sandbox_id} "
@@ -335,7 +339,9 @@ def sandbox_auth(
server_url=app_url,
workspace=workspace,
)
click.secho("\n✓ Sandbox logged in.\n", fg="green", bold=True)
ui.console.print()
ui.success("Sandbox logged in.")
ui.console.print()
@sandbox.command("connect")
+235
View File
@@ -0,0 +1,235 @@
"""Shared terminal-output styling for the Omnigent CLI.
This is the one place that owns the consoles, the brand palette, and the
status / structure helpers that every command should print through, so
that ``omnigent``'s output reads as one coherent product. See
``designs/CLI_CONTRACT.md`` for the full contract.
Core rule — **stdout carries data, stderr carries decoration**:
* Machine-readable output (IDs, paths, config dumps, the ``version``
string) goes to stdout via :data:`console`, so ``omnigent … | cat``
stays clean.
* Warnings, errors, and the brand banner go to stderr (via
:data:`err_console` / the banner helpers) and are TTY-gated, so they
never corrupt piped stdout.
Color is handled by rich: both consoles honor ``NO_COLOR`` and terminal
capability automatically, so callers never emit raw ANSI.
"""
from __future__ import annotations
import os
import sys
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from rich.theme import Theme
from . import wordmark
#: Brand accent — Otto's magenta-pink, shared with the mascot and banner.
ACCENT = wordmark.WORDMARK_COLOR
#: Env var that force-disables the brand banner even on a TTY. Mirrors the
#: ``OMNIGENT_NO_SPINNER`` convention in :mod:`omnigent._runner_startup`.
NO_BANNER_ENV_VAR = "OMNIGENT_NO_BANNER"
# Named styles, so call sites use semantic tokens ("omni.warning") rather
# than hard-coded colors. Semantic colors stay conventional; only the
# accent is brand-specific.
OMNIGENT_THEME = Theme(
{
"omni.accent": ACCENT,
"omni.success": "green",
"omni.warning": "yellow",
"omni.error": "bold red",
"omni.info": "cyan",
"omni.muted": "dim",
}
)
# ``highlight=False`` so rich never auto-recolors numbers / paths / URLs
# inside our messages — CLI output must be predictable. File is resolved
# lazily by rich, so these follow ``CliRunner`` / ``capsys`` stream swaps.
#: Console for stdout — data and primary output.
console = Console(theme=OMNIGENT_THEME, highlight=False)
#: Console for stderr — status, warnings, errors, and the brand banner.
err_console = Console(stderr=True, theme=OMNIGENT_THEME, highlight=False)
def show_banner(*, isatty: bool | None = None, env: dict[str, str] | None = None) -> bool:
"""
Decide whether the brand banner / brandmark should be drawn.
The banner is decoration, so it only shows on an interactive stderr
and can be force-disabled with ``OMNIGENT_NO_BANNER``. Color *within*
the banner is a separate concern handled by rich (``NO_COLOR`` simply
renders the art in monochrome).
:param isatty: Override for ``sys.stderr.isatty()`` (tests pass this
to exercise both branches without a real PTY).
:param env: Environment snapshot; defaults to ``os.environ``.
:returns: ``True`` when the banner should be drawn.
"""
if isatty is None:
isatty = sys.stderr.isatty()
if not isatty:
return False
env = os.environ if env is None else env
raw = str(env.get(NO_BANNER_ENV_VAR, "")).strip().lower()
return raw not in {"1", "true", "yes", "on"}
# ── Status helpers ────────────────────────────────────────────────────
# A consistent glyph + color per severity. ``step``/``success``/``info``
# are normal status on stdout; ``warn``/``error`` are diagnostics on
# stderr (always correct, never pollutes piped data). The message is
# appended as plain Text so it is never reinterpreted as rich markup.
def _emit(target: Console, glyph: str, style: str, message: str) -> None:
"""
Print ``<glyph> <message>`` with *glyph* styled, *message* plain.
:param target: Console to print to (stdout or stderr).
:param glyph: Leading status glyph, e.g. ``""``.
:param style: Style name for the glyph, e.g. ``"omni.success"``.
:param message: Plain message text (never parsed as markup).
"""
line = Text()
line.append(f"{glyph} ", style=style)
line.append(message)
target.print(line)
def step(message: str) -> None:
"""Print an ``==>`` progress step (accent) to stdout."""
_emit(console, "==>", "omni.accent", message)
def success(message: str) -> None:
"""Print a ``✓`` success line (green) to stdout."""
_emit(console, "", "omni.success", message)
def info(message: str) -> None:
"""Print a dim ``·`` informational line to stdout."""
_emit(console, "·", "omni.muted", message)
def warn(message: str) -> None:
"""Print a ``!`` warning (yellow) to stderr."""
_emit(err_console, "!", "omni.warning", message)
def error(message: str) -> None:
"""Print a ``✗`` error (red) to stderr."""
_emit(err_console, "", "omni.error", message)
# ── Structure helpers ─────────────────────────────────────────────────
def header(title: str) -> None:
"""Print a bold accent section header to stdout."""
console.print(Text(title, style="bold omni.accent"))
def kv(label: str, value: str, *, label_width: int = 10) -> None:
"""
Print one aligned ``label value`` row (dim label, bold value).
:param label: Left-hand label, e.g. ``"Session"``.
:param value: Right-hand value, e.g. ``"New session"``.
:param label_width: Column width the label is padded to.
"""
line = Text()
line.append(label.ljust(label_width), style="dim")
line.append(value, style="bold")
console.print(line)
def rule(title: str = "") -> None:
"""Print a horizontal accent rule (optionally titled) to stdout."""
console.rule(title, style="omni.accent")
def table(*, title: str | None = None, **kwargs: object) -> Table:
"""
Build a :class:`rich.table.Table` pre-styled with the brand palette.
Callers add columns/rows and then ``console.print(tbl)``. Centralizing
construction keeps every table's header/border consistent.
:param title: Optional table title.
:returns: A configured (empty) ``Table``.
"""
return Table(
title=title,
header_style="bold omni.accent",
border_style="omni.muted",
title_style="bold omni.accent",
**kwargs, # type: ignore[arg-type]
)
def panel(renderable: object, *, title: str | None = None, **kwargs: object) -> Panel:
"""
Wrap *renderable* in a :class:`rich.panel.Panel` with the brand border.
:param renderable: Any rich renderable or string to box.
:param title: Optional panel title.
:returns: A configured ``Panel``.
"""
return Panel(
renderable, # type: ignore[arg-type]
title=title,
border_style="omni.accent",
**kwargs, # type: ignore[arg-type]
)
# ── Brand banner ──────────────────────────────────────────────────────
def print_landing(
*,
epilogue: list[tuple[str, str]] | None = None,
gradient: bool = True,
tagline: str | None = None,
) -> None:
"""
Print the full Otto + wordmark lockup (the hero banner), TTY-gated.
Drawn on stderr so it never lands in piped stdout. No-op when the
banner is suppressed (non-TTY or ``OMNIGENT_NO_BANNER``).
:param epilogue: Optional aligned ``(label, value)`` rows beneath the
art (e.g. version / next-step).
:param gradient: Fade the wordmark magenta→pink (default on for the
hero moment); falls back to flat accent on low-color terminals.
:param tagline: Optional dim tagline under the art.
:returns: None.
"""
if not show_banner():
return
wordmark.render_lockup(err_console, gradient=gradient, tagline=tagline, epilogue=epilogue)
def print_brandmark(subtitle: str | None = None) -> None:
"""
Print the compact one-line brandmark (``✦ omnigent``), TTY-gated.
For non-interactive commands that want a branded header without the
full banner. Drawn on stderr; no-op when the banner is suppressed.
:param subtitle: Optional dim trailing text, e.g. a version string.
:returns: None.
"""
if not show_banner():
return
wordmark.render_compact(err_console, subtitle=subtitle)
+234
View File
@@ -0,0 +1,234 @@
"""The Omnigent brand wordmark and Otto lockup for CLI output.
A bold "ANSI-Shadow" block-letter ``omnigent`` wordmark — the canonical
figlet font with one duplicate body row dropped (5 rows), so every letter
stays legible and it sits exactly as tall as the Otto-the-starfish mascot
from :mod:`omnigent.inner.mascots`, which it pairs with 1:1.
This module owns the *art* and its rendering onto a caller-supplied
:class:`rich.console.Console`. The decision of *whether* to draw the
banner (TTY gating, ``OMNIGENT_NO_BANNER``) lives one layer up in
:mod:`omnigent.inner.ui`, which is the only module that should be imported
by command code. Keeping the gate out of here avoids a circular import
(``ui`` imports ``wordmark``) and keeps the art unit-testable in isolation.
The brand color is Otto's magenta-pink ``#F43BA6`` (see
:data:`omnigent.inner.mascots.MASCOT_ART_COLOR`); the optional gradient
fades it toward a lighter pink across the wordmark columns.
"""
from __future__ import annotations
from rich.cells import cell_len
from rich.console import Console
from rich.text import Text
from .mascots import MASCOT_ART_COL_WIDTH, MASCOT_ART_COLOR, MASCOT_ART_LINES
# Flat brand accent — kept in sync with the mascot/banner border so the
# wordmark, Otto, and the interactive REPL box all read as one color.
WORDMARK_COLOR = MASCOT_ART_COLOR
# Gradient endpoints (magenta → soft pink). Used only when a caller asks
# for ``gradient=True`` and the terminal supports enough colors; rich
# downgrades or drops the color automatically otherwise.
_GRADIENT_START = (0xF4, 0x3B, 0xA6) # #F43BA6
_GRADIENT_END = (0xFF, 0x9F, 0xD6) # #FF9FD6
# Two-space gutter between Otto and the wordmark in the lockup.
_GAP = " "
# Left indent applied to every printed row so the banner doesn't hug the
# terminal edge (matches the installer's two-space banner indent).
_INDENT = " "
# Per-letter "ANSI-Shadow" glyphs — the canonical figlet font (as used by
# NeonX and TAAG) with a single near-duplicate body row dropped, leaving 5
# rows. This keeps the full-height, fully-legible letterforms while sitting
# exactly as tall as Otto (5 rows), so the lockup pairs 1:1 with no unpaired
# rows. Stored as a glyph map rather than a frozen multi-line blob so the
# wordmark is regenerable and a missing letter fails loud at import. Each
# glyph's rows are equal display width so columns stay aligned when letters
# are concatenated.
_GLYPH_ROWS = 5
_GLYPHS: dict[str, tuple[str, ...]] = {
"o": (" ██████╗ ", "██╔═══██╗", "██║ ██║", "╚██████╔╝", " ╚═════╝ "),
"m": ("███╗ ███╗", "████╗ ████║", "██╔████╔██║", "██║ ╚═╝ ██║", "╚═╝ ╚═╝"),
"n": ("███╗ ██╗", "████╗ ██║", "██╔██╗ ██║", "██║ ╚████║", "╚═╝ ╚═══╝"),
"i": ("██╗", "██║", "██║", "██║", "╚═╝"),
"g": (" ██████╗ ", "██╔════╝ ", "██║ ███╗", "╚██████╔╝", " ╚═════╝ "),
"e": ("███████╗", "██╔════╝", "█████╗ ", "███████╗", "╚══════╝"),
"t": ("████████╗", "╚══██╔══╝", " ██║ ", " ██║ ", " ╚═╝ "),
}
_WORDMARK_TEXT = "omnigent"
def _build_wordmark(word: str) -> tuple[str, ...]:
"""
Concatenate per-letter glyphs into the wordmark rows.
:param word: The text to render; every character must have a glyph
in :data:`_GLYPHS`, e.g. ``"omnigent"``.
:returns: The wordmark rows (top cap · identity · bottom · shadow).
"""
rows = ["" for _ in range(_GLYPH_ROWS)]
for char in word:
glyph = _GLYPHS[char]
for i in range(_GLYPH_ROWS):
rows[i] += glyph[i]
return tuple(rows)
#: The rows of the ``omnigent`` wordmark, as plain (uncolored) text.
WORDMARK_LINES: tuple[str, ...] = _build_wordmark(_WORDMARK_TEXT)
# Which Otto row each wordmark row sits on. Otto and the wordmark are both
# five rows tall, so they pair 1:1 — no unpaired rows on either side.
_WORDMARK_ROW_FOR_OTTO_ROW = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4}
def wordmark_lines() -> list[str]:
"""
Return the wordmark on its own (no mascot), as plain text rows.
:returns: The wordmark rows, e.g. for embedding in a doc or a bash
banner.
"""
return list(WORDMARK_LINES)
def lockup_lines() -> list[str]:
"""
Return the Otto + wordmark lockup as plain text rows (no color).
Otto sits on the left (5 rows × :data:`MASCOT_ART_COL_WIDTH` cells)
with the 5-row wordmark aligned 1:1 beside it. Trailing whitespace is
stripped so the plain form is clean for snapshots and docs.
:returns: Five rows of the composed lockup.
"""
out: list[str] = []
for i, art in enumerate(MASCOT_ART_LINES):
pad = " " * (MASCOT_ART_COL_WIDTH - cell_len(art))
wm_index = _WORDMARK_ROW_FOR_OTTO_ROW.get(i)
wm = WORDMARK_LINES[wm_index] if wm_index is not None else ""
out.append(f"{_INDENT}{art}{pad}{_GAP}{wm}".rstrip())
return out
def _blend(start: tuple[int, int, int], end: tuple[int, int, int], t: float) -> str:
"""
Linearly interpolate two RGB triples into a ``#RRGGBB`` hex string.
:param start: RGB at ``t == 0``, e.g. ``(244, 59, 166)``.
:param end: RGB at ``t == 1``.
:param t: Position in ``[0, 1]``.
:returns: Hex color, e.g. ``"#f43ba6"``.
"""
r = round(start[0] + (end[0] - start[0]) * t)
g = round(start[1] + (end[1] - start[1]) * t)
b = round(start[2] + (end[2] - start[2]) * t)
return f"#{r:02x}{g:02x}{b:02x}"
def _wordmark_row_text(row: str, total_width: int, *, gradient: bool) -> Text:
"""
Render one wordmark row as a styled :class:`rich.text.Text`.
:param row: The plain wordmark row.
:param total_width: Full wordmark width, used as the gradient span so
every row shares the same per-column color ramp.
:param gradient: When ``True``, fade each column from magenta to pink;
otherwise paint the whole row the flat brand accent.
:returns: A styled ``Text`` for the row.
"""
if not gradient:
return Text(row, style=WORDMARK_COLOR)
text = Text()
span = max(1, total_width - 1)
for column, char in enumerate(row):
if char == " ":
text.append(" ")
continue
color = _blend(_GRADIENT_START, _GRADIENT_END, column / span)
text.append(char, style=color)
return text
def render_lockup(
console: Console,
*,
gradient: bool = False,
tagline: str | None = None,
epilogue: list[tuple[str, str]] | None = None,
) -> None:
"""
Print the Otto + wordmark lockup to *console*.
Color is applied via rich styles, so the console's own color settings
(NO_COLOR, terminal capability) decide whether color actually renders;
a no-color console prints the same art in monochrome.
:param console: Destination console (typically ``ui.err_console``).
:param gradient: Fade the wordmark magenta→pink instead of flat accent.
:param tagline: Optional dim line printed under the lockup, e.g.
``"all your agents, one cli"``.
:param epilogue: Optional aligned label/value rows printed beneath the
art, e.g. ``[("Version", "0.4.2"), ("Next", "omnigent setup")]``.
:returns: None.
"""
total_width = max(len(line) for line in WORDMARK_LINES)
console.print()
for i, art in enumerate(MASCOT_ART_LINES):
pad = " " * (MASCOT_ART_COL_WIDTH - cell_len(art))
line = Text(_INDENT)
line.append(f"{art}{pad}", style=WORDMARK_COLOR)
wm_index = _WORDMARK_ROW_FOR_OTTO_ROW.get(i)
if wm_index is not None:
line.append(_GAP)
line.append_text(
_wordmark_row_text(WORDMARK_LINES[wm_index], total_width, gradient=gradient)
)
console.print(line)
if tagline:
console.print(Text(f"{_INDENT}{tagline}", style="dim"))
if epilogue:
console.print()
_print_epilogue(console, epilogue)
console.print()
def _print_epilogue(console: Console, rows: list[tuple[str, str]]) -> None:
"""
Print aligned ``label value`` rows (dim label, bold value).
:param console: Destination console.
:param rows: ``(label, value)`` pairs; labels are left-padded to a
common width so the values line up.
:returns: None.
"""
label_width = max(cell_len(label) for label, _ in rows) + 3
for label, value in rows:
line = Text(_INDENT)
line.append(label.ljust(label_width), style="dim")
line.append(value, style="bold")
console.print(line)
def render_compact(console: Console, *, subtitle: str | None = None) -> None:
"""
Print the one-line brandmark: ``✦ omnigent <subtitle>``.
Used as a lightweight branded header on non-interactive commands that
don't warrant the full lockup (``version``, ``status``, ``upgrade``…).
:param console: Destination console (typically ``ui.err_console``).
:param subtitle: Optional dim trailing text, e.g. a version string.
:returns: None.
"""
line = Text(_INDENT)
line.append("", style=WORDMARK_COLOR)
line.append("omnigent", style=f"bold {WORDMARK_COLOR}")
if subtitle:
line.append(f" {subtitle}", style="dim")
console.print(line)
+4 -2
View File
@@ -46,6 +46,7 @@ from typing import TYPE_CHECKING, ClassVar
import click
from omnigent.inner import ui
from omnigent.onboarding.sandboxes.base import (
DEFAULT_HOST_IMAGE,
RemoteCommandResult,
@@ -407,11 +408,12 @@ class DaytonaSandboxLauncher(SandboxLauncher):
try:
handle.set_autostop_interval(_AUTO_STOP_DISABLED)
except daytona.DaytonaError as exc:
click.secho(
ui.console.print(
f" → warning: could not disable idle auto-stop on "
f"'{sandbox_id}' ({exc}); the sandbox may stop after "
"Daytona's idle timeout.",
fg="yellow",
style="omni.warning",
markup=False,
)
else:
click.echo(" → idle auto-stop disabled (sandbox lives until deleted)")
+7 -4
View File
@@ -49,6 +49,7 @@ from typing import TYPE_CHECKING, ClassVar
import click
from omnigent.inner import ui
from omnigent.onboarding.sandboxes.base import (
RemoteCommandResult,
RemoteProcess,
@@ -548,11 +549,12 @@ class E2BSandboxLauncher(SandboxLauncher):
# The requested lifetime exceeds this account's maximum (e.g. a
# Hobby account's 1 h cap vs the 24 h default) and E2B rejected it
# rather than clamping — retry once at the cap.
click.secho(
ui.console.print(
f" → requested {timeout // 3600}h lifetime exceeds this E2B account's "
f"maximum ({cap // 3600}h); retrying clamped to it (set "
f"{MAX_LIFETIME_ENV_VAR} to request a specific lifetime).",
fg="yellow",
style="omni.warning",
markup=False,
)
try:
return Sandbox.create(
@@ -598,10 +600,11 @@ class E2BSandboxLauncher(SandboxLauncher):
try:
handle.set_timeout(lifetime)
except SandboxException as exc:
click.secho(
ui.console.print(
f" → warning: could not extend the lifetime of '{sandbox_id}' "
f"({exc}); the sandbox will stop at its current timeout.",
fg="yellow",
style="omni.warning",
markup=False,
)
else:
# set_timeout accepts an over-cap request without raising (unlike
+4 -2
View File
@@ -33,7 +33,9 @@ console = Console()
_GREEN = "\033[32m"
_DIM = "\033[90m"
_BOLD = "\033[1m"
_CYAN = "\033[36m"
# Brand accent — Otto's magenta-pink (#F43BA6), matching omnigent.inner.ui so
# the setup picker's selection pointer reads as the same brand as the banner.
_ACCENT = "\033[38;2;244;59;166m"
_RESET = "\033[0m"
_CHECK = f"{_GREEN}\u2713{_RESET}"
_CROSS = f"{_DIM}\u2717{_RESET}"
@@ -123,7 +125,7 @@ def _arrow_menu(
sys.stdout.write(f"\033[{total_lines}A")
for i, label in enumerate(options):
pointer = f"{_CYAN}>{_RESET}" if i == cursor else " "
pointer = f"{_ACCENT}>{_RESET}" if i == cursor else " "
if multi:
check = f"{_GREEN}*{_RESET}" if i in selected else " "
prefix = f" {pointer} {check} "
+23 -5
View File
@@ -40,7 +40,7 @@ ESC=$(printf '\033')
RESET=
BOLD=
DIM=
CYAN=
MAGENTA=
GREEN=
YELLOW=
RED=
@@ -54,19 +54,36 @@ init_style() {
RESET="${ESC}[0m"
BOLD="${ESC}[1m"
DIM="${ESC}[2m"
CYAN="${ESC}[36m"
# Brand accent — Otto's magenta-pink (#F43BA6), matching the Python CLI
# palette in omnigent/inner/ui.py so the installer and the tool agree.
MAGENTA="${ESC}[38;2;244;59;166m"
GREEN="${ESC}[32m"
YELLOW="${ESC}[33m"
RED="${ESC}[31m"
fi
}
# The Otto + "omnigent" wordmark lockup, printed once at the top of an
# interactive install. Mirrors omnigent.inner.wordmark.lockup_lines(); the
# whole lockup is painted in the brand magenta (flat — no gradient in sh).
# Skipped off a TTY (use_terminal_ui) so piped/CI installs stay clean.
print_banner() {
use_terminal_ui || return 0
printf '\n'
printf '%s ⠀⠀⠀⢠⣿⡄⠀⠀⠀ ██████╗ ███╗ ███╗███╗ ██╗██╗ ██████╗ ███████╗███╗ ██╗████████╗%s\n' "$MAGENTA" "$RESET"
printf '%s ⢴⣶⣶⠉⣿⠉⣶⣶⡦ ██╔═══██╗████╗ ████║████╗ ██║██║██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝%s\n' "$MAGENTA" "$RESET"
printf '%s ⠀⠙⣿⣶⣿⣶⣿⠋⠀ ██║ ██║██╔████╔██║██╔██╗ ██║██║██║ ███╗█████╗ ██╔██╗ ██║ ██║%s\n' "$MAGENTA" "$RESET"
printf '%s ⠀⢠⣿⡿⠿⢿⣿⡄⠀ ╚██████╔╝██║ ╚═╝ ██║██║ ╚████║██║╚██████╔╝███████╗██║ ╚████║ ██║%s\n' "$MAGENTA" "$RESET"
printf '%s ⠀⠈⠁⠀⠀⠀⠈⠁⠀ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝%s\n' "$MAGENTA" "$RESET"
printf '%s all your agents, one cli%s\n\n' "$DIM" "$RESET"
}
usage() {
printf 'Usage: install_oss.sh [--non-interactive] [--verbose] [--version X] [--repo URL] [--extra NAME]\n'
}
step() {
printf '%s==>%s %s\n' "$CYAN" "$RESET" "$1"
printf '%s==>%s %s\n' "$MAGENTA" "$RESET" "$1"
}
verbose() {
@@ -124,7 +141,7 @@ run_with_spinner() {
frame=0
while [ ! -f "$status_file" ]; do
spinner="$(spinner_frame "$frame")"
printf '\r\033[K%s%s%s %s%s%s' "$CYAN" "$spinner" "$RESET" "$BOLD" "$label" "$RESET"
printf '\r\033[K%s%s%s %s%s%s' "$MAGENTA" "$spinner" "$RESET" "$BOLD" "$label" "$RESET"
frame=$((frame + 1))
sleep 0.1
done
@@ -593,7 +610,7 @@ print_next_steps() {
printf '\n%sOmnigent installed successfully.%s\n\n' "$BOLD" "$RESET"
printf 'Start chatting — first run sets up a model and a local web UI:\n'
printf ' %s%somnigent%s\n\n' "$command_prefix" "$CYAN" "$RESET"
printf ' %s%somnigent%s\n\n' "$command_prefix" "$MAGENTA" "$RESET"
printf 'Or launch a specific coding harness:\n'
printf ' %somnigent claude # Claude Code\n' "$command_prefix"
printf ' %somnigent codex # Codex\n\n' "$command_prefix"
@@ -607,6 +624,7 @@ print_next_steps() {
main() {
init_style
parse_args "$@"
print_banner
normalize_repo_url
check_platform
check_prerequisites
+70
View File
@@ -0,0 +1,70 @@
"""Tests for the shared CLI styling layer (``omnigent.inner.ui``)."""
from __future__ import annotations
import pytest
from omnigent.inner import ui
def test_accent_is_brand_magenta() -> None:
"""The shared accent is the Omnigent brand magenta."""
assert ui.ACCENT == "#F43BA6"
def test_show_banner_requires_a_tty() -> None:
"""The banner is decoration — never drawn off a TTY."""
assert ui.show_banner(isatty=False, env={}) is False
assert ui.show_banner(isatty=True, env={}) is True
@pytest.mark.parametrize("value", ["1", "true", "YES", "on"])
def test_show_banner_respects_no_banner_env(value: str) -> None:
"""``OMNIGENT_NO_BANNER`` force-disables the banner even on a TTY."""
assert ui.show_banner(isatty=True, env={ui.NO_BANNER_ENV_VAR: value}) is False
def test_warnings_and_errors_go_to_stderr(capsys: pytest.CaptureFixture[str]) -> None:
"""Diagnostics print to stderr so piped stdout stays clean."""
ui.warn("tmux not found")
ui.error("uv is required")
captured = capsys.readouterr()
assert captured.out == ""
assert "tmux not found" in captured.err
assert "uv is required" in captured.err
def test_status_lines_go_to_stdout(capsys: pytest.CaptureFixture[str]) -> None:
"""Normal status (step/success/info) prints to stdout."""
ui.step("Installing Omnigent")
ui.success("Verified omnigent")
ui.info("Using ~/.omnigent")
captured = capsys.readouterr()
assert "Installing Omnigent" in captured.out
assert "✓ Verified omnigent" in captured.out
assert captured.err == ""
def test_status_lines_are_plain_off_tty(capsys: pytest.CaptureFixture[str]) -> None:
"""Off a TTY (captured streams) output carries no ANSI escapes."""
ui.success("done")
ui.error("nope")
captured = capsys.readouterr()
assert "\x1b[" not in captured.out
assert "\x1b[" not in captured.err
def test_message_with_brackets_is_not_treated_as_markup(
capsys: pytest.CaptureFixture[str],
) -> None:
"""A message containing ``[...]`` is emitted verbatim, not as markup."""
ui.success("installed [databricks] extra")
captured = capsys.readouterr()
assert "[databricks]" in captured.out
+89
View File
@@ -0,0 +1,89 @@
"""Tests for the Omnigent brand wordmark and Otto lockup."""
from __future__ import annotations
from rich.cells import cell_len
from rich.console import Console
from omnigent.inner import wordmark
from omnigent.inner.mascots import MASCOT_ART_COLOR, MASCOT_ART_LINES
def test_wordmark_is_five_rows_of_equal_display_width() -> None:
"""The wordmark renders as five columns-aligned rows (Otto's height)."""
assert len(wordmark.WORDMARK_LINES) == 5
widths = {cell_len(line) for line in wordmark.WORDMARK_LINES}
assert len(widths) == 1, f"wordmark rows misaligned: {widths}"
def test_wordmark_uses_brand_color() -> None:
"""The wordmark accent stays in sync with the mascot brand color."""
assert wordmark.WORDMARK_COLOR == MASCOT_ART_COLOR == "#F43BA6"
def test_every_letter_in_omnigent_has_a_glyph() -> None:
"""The glyph map covers every letter rendered, and only symbols."""
for char in "omnigent":
assert char in wordmark._GLYPHS
# The art is symbol-only — no letters or digits leak into the rows.
assert all(not any(c.isalnum() for c in line) for line in wordmark.WORDMARK_LINES)
def test_lockup_lines_pair_otto_with_wordmark() -> None:
"""The lockup is Otto (5 rows) with the 5-row wordmark aligned 1:1."""
lines = wordmark.lockup_lines()
assert len(lines) == len(MASCOT_ART_LINES) == 5
# Every row pairs Otto with a wordmark row; the cap and body rows carry
# block glyphs (the final row is the all-line-art drop shadow).
assert "" in lines[0]
assert "" in lines[2]
# Plain text form carries no ANSI escapes.
assert all("\x1b[" not in line for line in lines)
def test_render_lockup_plain_console_has_no_ansi() -> None:
"""A no-color console renders the art in monochrome (no escapes)."""
console = Console(no_color=True, width=120, file=_StringFile())
wordmark.render_lockup(console)
assert "\x1b[" not in console.file.getvalue() # type: ignore[attr-defined]
def test_render_lockup_color_console_emits_ansi() -> None:
"""A color terminal renders the lockup with ANSI color codes."""
console = Console(force_terminal=True, width=120, file=_StringFile())
wordmark.render_lockup(console, gradient=True)
assert "\x1b[" in console.file.getvalue() # type: ignore[attr-defined]
def test_render_compact_includes_name() -> None:
"""The compact brandmark prints the product name and any subtitle."""
console = Console(no_color=True, width=120, file=_StringFile())
wordmark.render_compact(console, subtitle="0.4.2")
out = console.file.getvalue() # type: ignore[attr-defined]
assert "omnigent" in out
assert "0.4.2" in out
assert "" in out
class _StringFile:
"""Minimal in-memory text file for capturing rich Console output."""
def __init__(self) -> None:
self._buf: list[str] = []
def write(self, text: str) -> int:
self._buf.append(text)
return len(text)
def flush(self) -> None: # pragma: no cover - rich calls this
pass
def getvalue(self) -> str:
return "".join(self._buf)