Skip to main content

tui-development

Guide for developing the OpenShell TUI — a ratatui-based terminal UI for the OpenShell platform. Covers architecture, navigation, data fetching, theming, UX conventions, and development workflow. Trigger keywords - term, TUI, terminal UI, ratatui, openshell-tui, tui development, tui feature, tui bug.

Quellinformationen

Repository
NVIDIA/OpenShell
Letzte Quellaktivität
3. Oktober 2026 um 20:32
Erkannte Sprache von SKILL.md
Englisch
Sterne
14.949
Forks
1.703

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
tui-development
description
Guide for developing the OpenShell TUI — a ratatui-based terminal UI for the OpenShell platform. Covers architecture, navigation, data fetching, theming, UX conventions, and development workflow. Trigger keywords - term, TUI, terminal UI, ratatui, openshell-tui, tui development, tui feature, tui bug.
metadata
{"internal":true}
# OpenShell TUI Development Guide Comprehensive reference for any agent working on the OpenShell TUI. ## 1. Overview The OpenShell TUI is a ratatui-based terminal UI for the OpenShell platform. It provides a keyboard-driven interface for managing gateways, sandboxes, and logs — the same operations available via the `openshell` CLI, but with a live, interactive dashboard. - **Launched via:** `openshell term` or `mise run term` - **Crate:** `crates/openshell-tui/` - **Key dependencies:** - `ratatui` (workspace version) — uses `frame.area()` for the drawable terminal area - `crossterm` (workspace version) — terminal backend and event polling - `tonic` with TLS — gRPC client for the OpenShell gateway - `tokio` — async runtime for event loop, spawned tasks, and mpsc channels - `openshell-core` — proto-generated types (`OpenShellClient`, request/response structs) - `openshell-bootstrap` — gateway discovery (`list_gateways()`) - **Theme:** Adaptive dark/light via `Theme` struct — NVIDIA-branded green accents. Controlled by `--theme` flag, `OPENSHELL_THEME` env var, or auto-detection. ## 2. Domain Object Hierarchy The data model follows a strict hierarchy: **Gateway > Workspace > Sandboxes/Providers/Settings > Logs**. ``` Gateway (discovered via openshell_bootstrap::list_gateways()) ├── Global Settings (fetched via GetGatewayConfig) ├── Global Policy indicator (fetched via ListSandboxPolicies global=true) ├── Workspaces (fetched via ListWorkspaces) ├── Provider Profiles (fetched via ListProviderProfiles, workspace-scoped) ├── Providers (fetched via ListProviders, workspace-scoped) │ └── cached ProviderProfile (matched by type + workspace) └── Sandboxes (fetched via ListSandboxes, workspace-scoped) ├── Policy (fetched via GetSandboxConfig) ├── Settings (effective settings with scope, from GetSandboxConfig) ├── Draft recommendations (fetched via GetDraftPolicy) └── Logs (fetched via GetSandboxLogs + streamed via WatchSandbox) ``` - **Gateways** are discovered from on-disk config via `openshell_bootstrap::list_gateways()`. Each gateway has a name, endpoint, local/remote flag, and source label. - **Workspaces** are fetched via `ListWorkspaces`. The user cycles through workspaces with `[w]`, or views all workspaces at once. The current workspace scopes provider and sandbox lists. - **Provider Profiles** are fetched per-workspace via `ListProviderProfiles`. Profiles are cached in a `ProviderProfileCache` keyed by `(workspace, profile_id)` and matched to providers by type. They provide category, credential metadata, endpoint/binary counts, and inference capability. - **Providers** are fetched via `ListProviders` scoped to the current workspace. Each `ProviderListEntry` pairs a provider with its optional cached profile. The TUI supports profile-backed create, update, and delete operations. - **Global Settings** are fetched via `GetGatewayConfig` and displayed in a tabbed pane alongside providers on the dashboard. Each setting is a registered key with a typed value (bool/int/string). Platform-admin access is required; `PermissionDenied` disables the pane. - **Sandboxes** belong to the active gateway and workspace. Fetched via `ListSandboxes` with a periodic tick refresh. - **Sandbox Settings** are effective settings returned by `GetSandboxConfig`, each with a scope (sandbox, global, or unset). Globally-managed settings are blocked from sandbox-level edits. - **Logs** belong to a single sandbox. Initial batch fetched via `GetSandboxLogs` (500 lines), then live-tailed via `WatchSandbox` with `follow_logs: true`. The **title bar** always reflects this hierarchy, reading left-to-right from general to specific: ``` OpenShell v<version> │ Current Gateway: <name> [source] (<status>) │ Workspace: <name|all> │ <screen/context> ``` ## 3. Navigation & Screen Architecture ### Screens (`Screen` enum) Top-level layouts that own the full content area. Each has its own nav bar hints. | Screen | Description | Module | | --- | --- | --- | | `Splash` | Boot screen shown on startup, auto-dismissed after 3 seconds | `ui/splash.rs` | | `Dashboard` | Gateway list (top) + providers/settings (middle) + sandbox table (bottom) | `ui/dashboard.rs` | | `Sandbox` | Single-sandbox view — metadata (top) + policy/settings/logs/drafts (bottom) | `ui/sandbox_detail.rs`, `ui/sandbox_policy.rs`, `ui/sandbox_settings.rs`, `ui/sandbox_logs.rs`, `ui/sandbox_draft.rs` | ### Focus (`Focus` enum) Tracks which panel currently receives keyboard input. | Focus | Screen | Description | | --- | --- | --- | | `Gateways` | Dashboard | Gateway list panel has input focus | | `Providers` | Dashboard | Provider list or global settings pane (depends on `MiddlePaneTab`) | | `Sandboxes` | Dashboard | Sandbox table panel has input focus | | `SandboxPolicy` | Sandbox | Policy viewer or settings table (depends on `SandboxPolicyTab`) | | `SandboxLogs` | Sandbox | Log viewer with structured rendering | | `SandboxDraft` | Sandbox | Draft policy recommendations list | ### Tab enums Two tab enums control which sub-view renders within a focus area: - **`MiddlePaneTab`** (`Providers` | `GlobalSettings`): toggles the middle dashboard pane between the provider list and the global settings table. Switched with `[h/l]`. - **`SandboxPolicyTab`** (`Policy` | `Settings`): toggles the sandbox bottom pane between the policy viewer and the sandbox settings table. Switched with `[h]`. ### Screen dispatch The top-level `ui::draw()` function (`ui/mod.rs`) handles the chrome (title bar, nav bar, command bar) and dispatches to the correct screen module: ```rust match app.screen { Screen::Splash => unreachable!(), Screen::Dashboard => dashboard::draw(frame, app, chunks[1]), Screen::Sandbox => draw_sandbox_screen(frame, app, chunks[1]), } ``` Within the `Sandbox` screen, `sandbox_detail::required_height` sizes the metadata and restart status pane to its contents. The remaining area dispatches based on focus and tab state: ```rust match app.focus { Focus::SandboxLogs => sandbox_logs::draw(frame, app, chunks[1]), Focus::SandboxDraft => sandbox_draft::draw(frame, app, chunks[1]), _ => match app.sandbox_policy_tab { SandboxPolicyTab::Settings => sandbox_settings::draw(frame, app, chunks[1]), SandboxPolicyTab::Policy => sandbox_policy::draw(frame, app, chunks[1]), }, } ``` On the dashboard, the middle pane dispatches by `MiddlePaneTab`: ```rust match app.middle_pane_tab { MiddlePaneTab::Providers => providers::draw(frame, app, chunks[1], mid_focused), MiddlePaneTab::GlobalSettings => global_settings::draw(frame, app, chunks[1], mid_focused), } ``` ### Layout structure Every frame renders four vertical regions: ``` ┌─────────────────────────────────────────────┐ │ Title bar (1 row) — brand + gateway + context│ ├─────────────────────────────────────────────┤ │ │ │ Main content (flexible) │ │ │ ├─────────────────────────────────────────────┤ │ Nav bar (1 row) — context-sensitive key hints│ ├─────────────────────────────────────────────┤ │ Command bar (1 row) — `:` command input │ └─────────────────────────────────────────────┘ ``` ### Title bar examples - Dashboard: ` >_ OpenShell v<version> | Current Gateway: openshell [local] (Healthy) | Workspace: default | Dashboard` - Sandbox detail: ` >_ OpenShell v<version> | Current Gateway: openshell [local] (Healthy) | Workspace: team-a | Sandbox: my-sandbox` ### Adding a new screen 1. Add a variant to `Screen` in `app.rs`. 2. Create a new module under `src/ui/` with a `pub fn draw(frame, app, area)`. 3. Add the module declaration in `ui/mod.rs`. 4. Add a match arm in `ui::draw()` to dispatch to the new module. 5. Add relevant `Focus` variants if the screen has multiple panels. 6. Add key handling methods in `App` for the new focus states. 7. Add nav bar hints in `draw_nav_bar()` for the new screen/focus combinations. ## 4. Data Fetching Pattern ### Initial fetch first, then stream Always grab a batch of initial data so the UI has content immediately, then attach streaming for live updates. **Logs example** (`spawn_log_stream` in `lib.rs`): ``` Phase 1: GetSandboxLogs → 500 initial lines → send via Event::LogLines Phase 2: WatchSandbox(follow_logs: true) → live tail → send via Event::LogLines ``` **Sandboxes**: Fetched via `ListSandboxes` in a background collection-refresh task scheduled from the 2-second tick, scoped to the current workspace (or all workspaces). Follow `next_page_token` until empty so the dashboard reflects the complete collection. The NOTES column summarizes active `ConfigurationInvalid` readiness conditions as `Invalid config` before port forwards and clears the note on refresh after repair. Full diagnostics remain available through `openshell sandbox get <name> -o json`. Timed-out provisioning attempts show `Provisioning timed out` with cleanup pending or compute reclaimed, preserving port forwards. The sandbox detail pane wraps the full configuration error in its Notes field. **Providers**: Fetched via `ListProviders` in the background collection-refresh task. Provider profiles are fetched per-workspace via `ListProviderProfiles` and cached in a `ProviderProfileCache` keyed by `(workspace, profile_id)`. Follow each list RPC's `next_page_token` until empty. **Settings**: Global settings are fetched via `GetGatewayConfig` on each tick. Sandbox settings are fetched alongside the sandbox policy via `GetSandboxConfig` and refreshed on each tick when viewing a sandbox. **Workspaces**: The workspace list is fetched via `ListWorkspaces` in the background collection-refresh task, following `next_page_token` until empty. Only one collection-refresh task may run at a time. Workspace and gateway changes abort the active task, and refresh results carry their gateway/workspace context so stale results are discarded. ### Never block the event loop All network calls must be spawned as async tasks via `tokio::spawn`. The event loop in `lib.rs` must remain responsive to keyboard input and rendering at all times. **Pattern:** ```rust // Background task sends data back via mpsc channel let handle = tokio::spawn(async move { let result = client.some_rpc(request).await; let _ = tx.send(Event::SomeData(result)); }); ``` ### Loading states Show `"Loading..."` while async data is in flight (see `sandbox_logs.rs` — renders a loading message when `filtered` is empty and `sandbox_log_lines` is also empty). ### Event channel Background tasks communicate with the event loop via `mpsc::UnboundedSender<Event>`. The `EventHandler` provides a `sender()` method to clone the transmit handle. There are many `Event` variants for different async results (log lines, create results, provider CRUD results, setting CRUD results, draft action results, forward warnings): ```rust // In lib.rs spawn_log_stream(&mut app, events.sender()); // In the spawned task let _ = tx.send(Event::LogLines(lines)); ``` ### Access denial handling Global settings and global policy queries may return `PermissionDenied` when the user lacks platform-admin access. The TUI sets `global_settings_access_denied` / `global_policy_access_denied` flags to stop retrying these calls on subsequent ticks, and clears the corresponding UI state. ### gRPC timeouts All gRPC calls use a 5-second timeout via `tokio::time::timeout`: ```rust tokio::time::timeout(Duration::from_secs(5), client.health(req)).await ``` ## 5. Style Guide & Colors ### Theme System (`theme.rs`) Colors and styles are defined in `crates/openshell-tui/src/theme.rs` via the `Theme` struct. The TUI supports dark and light terminal backgrounds. #### Theme selection Theme mode is controlled by three mechanisms (highest priority first): 1. `--theme dark|light|auto` CLI flag on `openshell term` 2. `OPENSHELL_THEME` environment variable 3. Auto-detection via `COLORFGBG` env var (falls back to dark) The `ThemeMode` enum (`Auto`, `Dark`, `Light`) is resolved at startup via `theme::detect()` before entering raw mode. #### Brand colors (`theme::brand`) | Constant | Value | Usage | | --- | --- | --- | | `NVIDIA_GREEN` | `Color::Rgb(118, 185, 0)` | Primary accent (dark theme) | | `NVIDIA_GREEN_DARK` | `Color::Rgb(80, 140, 0)` | Primary accent (light theme — darker for contrast) | | `EVERGLADE` | `Color::Rgb(18, 49, 35)` | Dark green — borders, title bar bg (dark theme) | | `MAROON` | `Color::Rgb(128, 0, 0)` | Pacman chase animation | #### Theme struct fields The `Theme` struct has 16 `Style` fields, accessed at runtime via `app.theme`: | Field | Dark value | Light value | Usage | | --- | --- | --- | --- | | `text` | White fg | Near-black fg | Default body text | | `muted` | White + DIM | Gray fg | Secondary info, separators | | `heading` | White + BOLD | Near-black + BOLD | Panel titles, names | | `accent` | NVIDIA_GREEN fg | NVIDIA_GREEN_DARK fg | Selected row marker, source labels | | `accent_bold` | NVIDIA_GREEN + BOLD | NVIDIA_GREEN_DARK + BOLD | Brand text, command prompt | | `selected` | BOLD only | BOLD only | Selected row emphasis | | `border` | EVERGLADE fg | Light sage fg | Unfocused panel borders | | `border_focused` | NVIDIA_GREEN fg | NVIDIA_GREEN_DARK fg | Focused panel borders | | `status_ok` | NVIDIA_GREEN fg | NVIDIA_GREEN_DARK fg | Healthy, INFO, Ready | | `status_warn` | Yellow fg | Dark yellow fg | Degraded, WARN, Provisioning, Starting | | `status_err` | Red fg | Dark red fg | Unhealthy, ERROR | | `key_hint` | NVIDIA_GREEN fg | NVIDIA_GREEN_DARK fg | Keyboard shortcut labels | | `log_cursor` | EVERGLADE bg | Light green bg | Selected log line highlight | | `claw` | MAROON + BOLD | MAROON + BOLD | Pacman animation | | `title_bar` | White on EVERGLADE + BOLD | Near-black on light green + BOLD | Title bar strip | | `badge` | Black on NVIDIA_GREEN + BOLD | White on NVIDIA_GREEN_DARK + BOLD | Notification badges | #### Accessing the theme in draw functions The `Theme` is stored on `App` and accessed via a local alias: ```rust fn draw_my_widget(frame: &mut Frame<'_>, app: &App, area: Rect) { let t = &app.theme; frame.render_widget( Paragraph::new(Span::styled("Hello", t.text)), area, ); } ``` For functions that don't take `&App` (e.g., detail popups, helpers), pass `&Theme` as a parameter: ```rust fn draw_detail_popup(frame: &mut Frame<'_>, data: &MyData, area: Rect, theme: &Theme) { let t = theme; // ... } ``` #### Visual conventions - **Selected row**: Green `▌` left-border marker on the selected row. Active gateway also gets a green `●` dot. - **Focused panel**: Border changes from `border` to `border_focused` style. - **Status indicators**: Green for healthy/ready/info, yellow for degraded/provisioning/starting/warn, red for unhealthy/error. - **Separators**: Muted `│` characters between title bar segments and nav bar sections.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen