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.

来源信息

仓库
NVIDIA/OpenShell
最近来源活动
2026年10月3日 20:32
检测到的 SKILL.md 语言
英语
星标
14,949
分支
1,703

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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.
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看