Skip to content

Add human-readable output format#37

Merged
n8agrin merged 9 commits into
mainfrom
nate/tui
Apr 21, 2026
Merged

Add human-readable output format#37
n8agrin merged 9 commits into
mainfrom
nate/tui

Conversation

@n8agrin
Copy link
Copy Markdown
Contributor

@n8agrin n8agrin commented Apr 18, 2026

Summary

Adds a human-readable output format alongside JSON, modeled after gh CLI. Lists become stylized lipgloss tables; single resources become key: value blocks; mixed objects (e.g. AI search → answer + sources) render each section under its own heading. A spinner runs on stderr for long requests. JSON mode stays the default when piped so omni … | jq … pipelines keep working.

Highlights

  • Three-level precedence for format selection: -o/--format flag > OMNI_OUTPUT_FORMAT env > defaultOutputFormat in config.json > auto (human on TTY, JSON when piped).
  • Lipgloss-rendered tables with box-drawing borders, bold headers, dimmed UUID columns, gray timestamps. Color auto-disables when piped.
  • Humanized labels: modelKindModel Kind, MODEL_KINDModel Kind, created_atCreated At. No more SHOUTING headers.
  • Shape heuristics for the non-uniform Omni response shapes: {records, pageInfo} lists, resource-named arrays ({connections: […]}), success envelopes ({success, message, <resource>}), bare arrays, single resources, and mixed scalar+array.
  • Null scalars are hidden in human mode so "error": null doesn't render as a scary Error: - line.
  • Rotating spinner on stderr while the HTTP request is in flight — cycles through 15 playful BI-flavored phrases every 3 seconds ("consulting the warehouse…", "aligning facts and dimensions…", "squinting at the query plan…", …). Only shown when stderr is a TTY and format is human; piped/scripted invocations stay quiet.
  • omni config set-format <json|human|auto> subcommand to persist a preference.

Example output

./bin/omni models list --modelkind SHARED:

┌──────────────────────────────────────┬────────────────────────────┬────────────┬────────────┐
│ Id                                   │ Name                       │ Model Kind │ Created At │
├──────────────────────────────────────┼────────────────────────────┼────────────┼────────────┤
│ ef0ba8e6-7bbc-4f3f-9097-7d10a764c60f │ GSBQ                       │ SHARED     │ 3h ago     │
│ 7ea1fa6d-932f-4d03-aeac-9084a516aad4 │ eCommerce New              │ SHARED     │ 3y ago     │
│ 0c056618-c536-438d-ba04-821cf5f37ad6 │ BigQuery Demo              │ SHARED     │ 3y ago     │
└──────────────────────────────────────┴────────────────────────────┴────────────┴────────────┘

./bin/omni ai search-omni-docs "how to add a format to field?" — mixed scalar+array shape renders the answer body (including nested markdown code fences) and the sources table side by side. The outer fence below is four backticks so the inner yaml block stays intact:

Answer
  To add a format to a field in Omni, use the `format` parameter on a dimension or measure.

  ## Basic syntax:

  ```yaml
  field_name:
    format: <format_string>
  ```

  See [Formatting values](/modeling/models/format-values) for the complete reference.

Sources
┌─────────────────────────────────────────────┬────────────────────────────────────────────────────────────┐
│ Title                                       │ Url                                                        │
├─────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ Modeling - Dimensions - Parameters - Format │ https://docs.omni.co/modeling/dimensions/parameters/format │
│ Modeling - Models - Format Values           │ https://docs.omni.co/modeling/models/format-values         │
└─────────────────────────────────────────────┴────────────────────────────────────────────────────────────┘

Commits

  1. Add output format resolver with flag/env/config precedence
  2. Add human-readable output formatter
  3. Wire --format flag and omni config set-format subcommand
  4. Humanize table headers and key labels
  5. Render tables with lipgloss
  6. Hide null scalar fields in human mode
  7. Show a spinner while the API request is in flight
  8. Rotate spinner phrases while waiting on the API
  9. Swap spinner phrases that hinted at mutation

Test plan

  • make test — resolver precedence, humanizeKey, shape heuristics (records list, resource-named list, bare array, single resource, success envelope, empty body, non-JSON passthrough, mixed scalar+array, null-skip regression)
  • make build
  • ./bin/omni models list --format human renders a bordered table; | cat drops ANSI (termenv auto-downgrade)
  • ./bin/omni ai search-omni-docs "…" renders Answer block + Sources table
  • ./bin/omni ai generate-query <id> "…" hides null Error, shows the Result table
  • OMNI_OUTPUT_FORMAT=human ./bin/omni models list | cat forces human
  • ./bin/omni config set-format human persists the preference to config.json
  • Spinner only appears when stderr is a TTY and format is human (verified via 2>/dev/null and --format json)

🤖 Generated with Claude Code

n8agrin and others added 9 commits April 18, 2026 10:00
Introduces FormatAuto/FormatJSON/FormatHuman constants and a
ResolveOutputFormat helper that picks an effective format from
--format flag, OMNI_OUTPUT_FORMAT env, the config file's
defaultOutputFormat field, or auto(TTY). This is the foundation for
a human-readable default output mode — the renderer and CLI wiring
land in follow-up commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders JSON responses in a terminal-friendly way, similar to the
gh CLI. List-wrapper objects (records/connections/bare arrays)
become aligned tables with capitalized headers. Single resources
become key: value blocks. Mixed objects — e.g. AI endpoints that
return both an `answer` string and a `sources` array — render each
top-level field under its own uppercase heading so nothing gets
silently dropped. Timestamps are formatted as relative ages
("2h ago"), and pageInfo becomes a cursor footer.

No new dependencies: columns are hand-aligned by rune width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds -o/--format as a persistent flag on the root command and
dispatches API responses to the human renderer when chosen. Default
is auto: human when stdout is a TTY, JSON when piped, so existing
pipelines like `omni models list | jq ...` keep working unchanged.

Users who prefer human output everywhere can set it once with
`omni config set-format human`, which persists to config.json and
is surfaced by `omni config show`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert API field names like `modelKind` or `MODEL_KIND` into
`Model Kind`, and add a blank line between the header row and
data rows so tables are easier to scan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the hand-rolled space-aligned table renderer for
lipgloss/table: normal box-drawing borders, bold headers,
dim identifier columns, grey timestamp columns. Color is
auto-disabled when stdout isn't a TTY via termenv.

Picked lipgloss over go-pretty so this investment carries
into any future bubbletea work on the nate/tui branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
An API response with `"error": null` (e.g. the ai/generate-query
shape) was rendering as `Error: -`, which looks like a failure
even though the request succeeded. Null scalars carry no
information to a human reader, so skip them in both the
key-value and mixed scalar/array renderers. JSON mode is
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Long-running endpoints (ai/generate-query, model compilation,
etc.) left the terminal silent for seconds. Add a small
briandowns/spinner on stderr while the HTTP request is
pending. Only runs when stderr is a TTY and the output
format is human — piped/scripted invocations stay quiet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The static "waiting for Omni…" suffix was fine for fast calls
but got boring on slow endpoints. Rotate through a short list
of playful BI-flavored phrases ("consulting the warehouse…",
"aligning facts and dimensions…", etc.) every 3 seconds so the
wait feels less monolithic.

Spinner logic moves to cmd/omni/spinner.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"warming up the cache…" and "polishing the dashboard…" could
read as "the CLI is touching your data or clearing caches,"
which it is not. Replace with two phrases that leave that
implication off the table: "squinting at the query plan…"
and "summoning rows…".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@n8agrin n8agrin requested review from cmruderman and elico5 April 21, 2026 20:41
@n8agrin
Copy link
Copy Markdown
Contributor Author

n8agrin commented Apr 21, 2026

@claude review this


func configSetFormatCmd() *cobra.Command {
return &cobra.Command{
Use: "set-format <json|human|auto>",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sets the format default

Comment thread internal/output/human.go
// pickColumns selects up to 6 scalar columns across the given records.
// Priority: id, name, then keys observed in insertion-ish order (alphabetical
// since we walked JSON), skipping complex types.
func pickColumns(records []map[string]any) []string {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be brittle. Something to watch out for.

@n8agrin
Copy link
Copy Markdown
Contributor Author

n8agrin commented Apr 21, 2026

@cmruderman this one is interesting:

Shape heuristics for the non-uniform Omni response shapes: {records, pageInfo} lists, resource-named arrays ({connections: […]}), success envelopes ({success, message, }), bare arrays, single resources, and mixed scalar+array.

I've gotten a request to generally abstract all the api responses behind a uniform data structure / format. We can talk about it in the next couple of weeks. Not sure it needs to be at the API layer, but I was surprised that OpenAPI didn't require one.

@n8agrin n8agrin merged commit eb460d1 into main Apr 21, 2026
2 checks passed
@n8agrin n8agrin deleted the nate/tui branch April 21, 2026 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants