nvidia/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.
npx skills add https://github.com/NVIDIA/OpenShell --skill tui-development
Comprehensive reference for any agent working on the OpenShell TUI.
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.
openshell term or mise run termcrates/openshell-tui/ratatui (workspace version) — uses frame.size() (not frame.area())crossterm (workspace version) — terminal backend and event pollingtonic with TLS — gRPC client for the OpenShell gatewaytokio — async runtime for event loop, spawned tasks, and mpsc channelsopenshell-core — proto-generated types (OpenShellClient, request/response structs)openshell-bootstrap — gateway discovery (list_gateways())Theme struct — NVIDIA-branded green accents. Controlled by --theme flag, OPENSHELL_THEME env var, or auto-detection.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)
openshell_bootstrap::list_gateways(). Each gateway has a name, endpoint, local/remote flag, and source label.ListWorkspaces. The user cycles through workspaces with [w], or views all workspaces at once. The current workspace scopes provider and sandbox lists.ListProviderProfiles when providers_v2_enabled is true. 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.ListProviders scoped to the current workspace. Each ProviderListEntry pairs a provider with its optional cached profile. When providers_v2_enabled is true, CRUD operations are read-only in the TUI; when false, the TUI supports create/update/delete.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.ListSandboxes with a periodic tick refresh.GetSandboxConfig, each with a scope (sandbox, global, or unset). Globally-managed settings are blocked from sandbox-level edits.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 │ Current Gateway: <name> [source] (<status>) │ Workspace: <name|all> │ <screen/context>
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 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 |
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].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:
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, the top 20% renders sandbox metadata (sandbox_detail), and the bottom 80% dispatches based on focus and tab state:
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:
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),
}
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 │
└─────────────────────────────────────────────┘
>_ OpenShell ALPHA | Current Gateway: openshell [local] (Healthy) | Workspace: default | Dashboard >_ OpenShell ALPHA | Current Gateway: openshell [local] (Healthy) | Workspace: team-a | Sandbox: my-sandboxScreen in app.rs.src/ui/ with a pub fn draw(frame, app, area).ui/mod.rs.ui::draw() to dispatch to the new module.Focus variants if the screen has multiple panels.App for the new focus states.draw_nav_bar() for the new screen/focus combinations.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 on a 2-second tick, scoped to the current workspace (or all workspaces).
Providers: Fetched via ListProviders on each tick. When providers_v2_enabled is true, provider profiles are also fetched per-workspace via ListProviderProfiles and cached in a ProviderProfileCache keyed by (workspace, profile_id).
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 on each tick.
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:
// 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));
});
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).
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):
// In lib.rs
spawn_log_stream(&mut app, events.sender());
// In the spawned task
let _ = tx.send(Event::LogLines(lines));
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.
All gRPC calls use a 5-second timeout via tokio::time::timeout:
tokio::time::timeout(Duration::from_secs(5), client.health(req)).await
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 mode is controlled by three mechanisms (highest priority first):
--theme dark|light|auto CLI flag on openshell termOPENSHELL_THEME environment variableCOLORFGBG env var (falls back to dark)The ThemeMode enum (Auto, Dark, Light) is resolved at startup via theme::detect() before entering raw mode.
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 |
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 |
| 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 |
The Theme is stored on App and accessed via a local alias:
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:
fn draw_detail_popup(frame: &mut Frame<'_>, data: &MyData, area: Rect, theme: &Theme) {
let t = theme;
// ...
}
▌ left-border marker on the selected row. Active gateway also gets a green ● dot.border to border_focused style.│ characters between title bar segments and nav bar sections."sandbox" source renders in accent (green), "gateway" in muted.Always show a y/n confirm dialog before delete, stop, or other irreversible operations.
Delete sandbox 'my-sandbox'? [y] Confirm [Esc] Cancel
The confirm_delete flag in App gates destructive key handling — while true, only y, n, and Esc are processed.
TUI actions should parallel openshell CLI commands so users have familiar mental models:
| CLI Command | TUI Equivalent |
| --- | --- |
| openshell sandbox list | Sandbox table on Dashboard |
| openshell sandbox delete <name> | [d] on sandbox detail, then [y] to confirm |
| openshell sandbox create | [c] on sandbox panel to open create form |
| openshell sandbox connect | [s] on sandbox policy view to launch SSH shell |
| openshell logs <name> | [l] on sandbox detail to open log viewer |
| openshell provider list | Provider table on Dashboard (middle pane) |
| openshell provider create | [c] on provider panel (when not providers_v2) |
| openshell status | Status in title bar + gateway list |
When adding new TUI features, check what the CLI offers and maintain consistency.
Any scrollable content (logs, future long lists) should follow the k9s autoscroll pattern:
f or G re-enables — jump to bottom and resume following● FOLLOWING (green) or ○ PAUSED (yellow) in the panel footerScrollUp/ScrollDown events move by 3 lines and respect autoscroll state[current/total] in the panel footerState is tracked via log_autoscroll: bool on App. The scroll_logs(delta) method handles both keyboard and mouse input uniformly.
When content can exceed the viewport width (log lines, field lists, etc.):
…. This keeps density high and avoids wrapping that breaks the 1-line-per-entry model.Esc or Enter closes it. Track the open state via Option<usize> index.This pattern should be reused for any future view with potentially long entries.
| Key | Action |
| --- | --- |
| j / Down | Move selection down |
| k / Up | Move selection up |
| g | Jump to top (logs), disables autoscroll |
| G | Jump to bottom (logs), re-enables autoscroll |
| f | Follow / re-enable autoscroll (logs) |
| Tab / BackTab | Switch between panels on Dashboard |
| Enter | Select / drill into item; open detail popup in logs |
| Esc | Go back one level |
| q | Quit (from any screen) |
| Ctrl+C | Force quit |
All actions are accessible via keyboard shortcuts displayed in the nav bar. The nav bar is context-sensitive — it shows different hints depending on the current screen and focus state. Mouse scrolling is supported as a convenience but never required — every action must have a keyboard equivalent.
: enters command mode (like vim). The command bar renders at the bottom with a green : prompt and a block cursor. Currently supports:
:q / :quit — exit the applicationEsc returns to normal mode. Enter executes the command.
Dashboard (Gateways focus):
[Tab] Switch Panel [Enter] Select [j/k] Navigate │ [:] Command [q] Quit
Dashboard (Providers focus, providers_v2):
[Tab] Switch Panel [h/l] Switch Tab [j/k] Navigate [Enter] Detail read-only │ [:] Command [q] Quit
Dashboard (Providers focus, legacy):
[Tab] Switch Panel [h/l] Switch Tab [j/k] Navigate [Enter] Detail [c] Create [u] Update [d] Delete │ [:] Command [q] Quit
Dashboard (Global Settings focus):
[Tab] Switch Panel [h/l] Switch Tab [j/k] Navigate [Enter] Edit [d] Delete │ [:] Command [q] Quit
Dashboard (Sandboxes focus):
[Tab] Switch Panel [j/k] Navigate [Enter] Select [c] Create Sandbox [w] Workspace │ [:] Command [q] Quit
Sandbox (Policy focus):
[h] Switch Tab [j/k] Scroll [g/G] Top/Bottom [s] Shell [l] Logs [r] Rules [d] Delete │ [Esc] Back [q] Quit
Sandbox (Settings focus):
[h/l] Switch Tab [j/k] Navigate [Enter] Edit [d] Delete │ [Esc] Back [q] Quit
Sandbox (Logs focus):
[j/k] Navigate [Enter] Detail [g/G] Top/Bottom [f] Follow [s] Source: <filter> [y] Copy [Y] Copy All [v] Select [r] Rules │ [Esc] Policy [q] Quit
Sandbox (Draft focus):
[j/k] Navigate [Enter] Detail [a] Approve [x] Reject [A] Approve All [p] Policy [l] Logs │ [Esc] Back [q] Quit
| File | Purpose |
| --- | --- |
| crates/openshell-tui/Cargo.toml | Crate manifest — dependencies on openshell-core, openshell-bootstrap, ratatui, crossterm, tonic, tokio |
| crates/openshell-tui/src/lib.rs | Entry point. Event loop, gRPC calls (refresh_data, refresh_providers, refresh_global_settings, refresh_workspaces, refresh_sandboxes, spawn_log_stream, handle_sandbox_delete, fetch_providers_v2_setting), gateway switching, mTLS channel building, provider CRUD spawners, settings CRUD spawners, draft approval spawners |
| crates/openshell-tui/src/app.rs | App state struct, Screen/Focus/InputMode/LogSourceFilter/MiddlePaneTab/SandboxPolicyTab enums, LogLine/GatewayEntry/GlobalSettingEntry/SandboxSettingEntry/ProviderListEntry/ProviderDetailView structs, create sandbox/provider form state, all key handling logic |
| crates/openshell-tui/src/event.rs | Event enum (Key, Mouse, Tick, Redraw, Resize, LogLines, CreateResult, ProviderCreateResult, ProviderDetailFetched, ProviderUpdateResult, ProviderDeleteResult, DraftActionResult, GlobalSettingsFetched, GlobalSettingSetResult, GlobalSettingDeleteResult, SandboxSettingSetResult, SandboxSettingDeleteResult, ForwardWarnings), EventHandler with mpsc channels and crossterm polling |
| crates/openshell-tui/src/theme.rs | colors module (NVIDIA_GREEN, EVERGLADE, BG, FG) and styles module (all Style constants) |
| crates/openshell-tui/src/clipboard.rs | Clipboard copy support for log lines |
| crates/openshell-tui/src/ui/mod.rs | Top-level draw() dispatcher, draw_title_bar (with workspace display), draw_nav_bar, draw_command_bar, screen routing, shared setting-edit overlay, modal helpers |
| crates/openshell-tui/src/ui/dashboard.rs | Dashboard screen — 3-pane vertical layout: gateway list (25%) + provider/settings middle pane (25%) + sandbox table (50%) |
| crates/openshell-tui/src/ui/providers.rs | Provider list table with profile-aware columns: Name, Category, Type, Credentials, Workspace |
| crates/openshell-tui/src/ui/global_settings.rs | Global settings table: Key, Type, Value. Includes edit overlay, confirm-set, and confirm-delete popups |
| crates/openshell-tui/src/ui/sandboxes.rs | Reusable sandbox table widget with columns: Name, Status, Created, Age, Image, Workspace, Notes |
| crates/openshell-tui/src/ui/sandbox_detail.rs | Sandbox metadata view — name, status, image, created, age, providers, policy version |
| crates/openshell-tui/src/ui/sandbox_policy.rs | Policy viewer — rendered policy lines with scroll support, tab title |
| crates/openshell-tui/src/ui/sandbox_settings.rs | Sandbox settings table: Key, Type, Value, Scope. Includes edit overlay and confirm popups |
| crates/openshell-tui/src/ui/sandbox_logs.rs | Structured log viewer — timestamp, source, level, target, message, key=value fields, scroll position, source filter, visual selection mode, clipboard copy |
| crates/openshell-tui/src/ui/sandbox_draft.rs | Draft policy recommendations — chunk list, detail popup, approve/reject/approve-all flows |
| crates/openshell-tui/src/ui/create_sandbox.rs | Create sandbox modal form with name, image, command, providers, ports |
| crates/openshell-tui/src/ui/create_provider.rs | Create provider modal, provider detail popup, update provider form |
| crates/openshell-tui/src/ui/splash.rs | Splash/boot screen |
lib.rs (event loop, gRPC, async tasks, capability fetch)
├── app.rs (state + key handling + tab/workspace logic)
├── event.rs (Event enum + EventHandler)
├── clipboard.rs (copy support)
├── theme.rs (colors + styles)
└── ui/
├── mod.rs (draw dispatcher, chrome, shared overlays)
├── splash.rs (boot screen)
├── dashboard.rs (3-pane layout: gateways + middle + sandboxes)
├── providers.rs (provider list with profile awareness)
├── global_settings.rs (settings table + edit/confirm overlays)
├── sandboxes.rs (sandbox table widget)
├── sandbox_detail.rs (metadata view)
├── sandbox_policy.rs (policy viewer)
├── sandbox_settings.rs (sandbox settings table + overlays)
├── sandbox_logs.rs (log viewer + visual selection)
├── sandbox_draft.rs (draft recommendations)
├── create_sandbox.rs (create sandbox modal)
└── create_provider.rs (create/detail/update provider modals)
openshell-tui cannot depend on openshell-cli — this would create a circular dependency. TLS channel building for gateway switching is done directly in lib.rs using tonic::transport primitives (Certificate, Identity, ClientTlsConfig, Endpoint).connect_to_gateway() reads gateway metadata to determine the auth mode, then builds an EdgeAuthInterceptor (bearer token for OIDC, noop for mTLS).~/.config/openshell/gateways/<name>/mtls/ (ca.crt, tls.crt, tls.key).openshell_bootstrap::oidc_token::load_oidc_token() and checked for expiry.Proto types come from openshell-core which generates them from OUT_DIR via include!. They are not checked into the repo. Import paths look like:
use openshell_core::proto::openshell_client::OpenShellClient;
use openshell_core::proto::{ListSandboxesRequest, GetSandboxLogsRequest, ...};
DeleteSandboxRequest uses the name field (not id): let req = openshell_core::proto::DeleteSandboxRequest { name: sandbox_name };
WatchSandboxRequest has extra fields beyond what you might need — always use ..Default::default(): let req = openshell_core::proto::WatchSandboxRequest {
id: sandbox_id,
follow_status: false,
follow_logs: true,
follow_events: false,
log_tail_lines: 0,
..Default::default()
};
SandboxLogLine proto fields: sandbox_id, timestamp_ms, level, target, message, source, fields (HashMap<String, String>).GetSandboxLogsRequest fields: sandbox_id, lines (u32), since_ms (i64), sources (Vec<String>), min_level (String), workspace (String).ListSandboxesRequest fields: limit (i64), offset (i64), label_selector (String), workspace (String), all_workspaces (bool).ListProvidersRequest fields: limit (i64), offset (i64), workspace (String), all_workspaces (bool).ListWorkspacesRequest fields: limit (i64), offset (i64), label_selector (String).UpdateConfigRequest fields: name (String, sandbox name or empty for global), setting_key, setting_value, delete_setting (bool), global (bool), workspace.workspace field that scopes the operation to the current workspace.All gRPC calls use a 5-second timeout:
tokio::time::timeout(Duration::from_secs(5), client.health(req)).await
The connect timeout for gateway switching is 10 seconds with HTTP/2 keepalive at 10-second intervals.
[l] on sandbox detail → pending_log_fetch = truespawn_log_stream()cancel_log_stream()tokio::spawn task: fetches initial 500 lines, then streams via WatchSandboxEvent::LogLines and are appended to app.sandbox_log_linesEsc or navigates away (handle is .abort()ed)Enter → pending_gateway_switch = Some(name)handle_gateway_switch()connect_to_gateway() (mTLS or OIDC depending on gateway metadata)app.client is replaced with a new intercepted clientreset_sandbox_state() clears all sandbox/log/draft/policy datafetch_providers_v2_setting() probes the new gateway's GetGatewayConfig to determine whether providers_v2 mode is enabled, so provider CRUD controls render correctlyrefresh_data() runs the full capability refresh sequence: refresh_health → refresh_global_settings → refresh_workspaces → refresh_providers → refresh_sandboxesstatus_text shows the errorOn launch, before the event loop starts:
fetch_providers_v2_setting() — probe gateway capabilityrefresh_gateway_list() — discover gateways from diskrefresh_data() — full refresh (health, global settings, workspaces, providers, sandboxes)[w] on the sandboxes panel → cycle_workspace() advances through discovered workspace names, then "all"pending_workspace_refresh = true is set, cursor indices are resetrefresh_providers() and refresh_sandboxes() with the new workspace scope[Enter] on a setting → edit overlay opens (bool types toggle inline and jump to confirmation)[Enter] opens a confirmation popup → [y] fires the pending flagspawn_set_global_setting() or spawn_set_sandbox_setting() → UpdateConfig RPC[d] on a setting with a value → confirmation popup → spawn_delete_*_setting() → UpdateConfig with delete_setting: trueFor sandbox settings, globally-managed entries (scope = global) are blocked from editing or deletion at the sandbox level.
# Build the crate
cargo build -p openshell-tui
# Run the TUI against the active gateway
mise run term
# Run with cargo-watch for hot-reload during development
mise run term:dev
# Format
cargo fmt -p openshell-tui
# Lint
cargo clippy -p openshell-tui
Always run before committing:
mise run pre-commit
If you change sandbox or server code that affects the backend, restart or redeploy the gateway for the compute platform you are using.
For Docker-backed local development:
mise run gateway:docker
For Kubernetes Helm deployments:
helm upgrade --install openshell deploy/helm/openshell --namespace openshell
For Kubernetes, pick up new sandbox images after changing sandbox code by deleting the pod manually so it gets recreated:
kubectl delete pod <pod-name> -n <namespace>
openshell-core for available RPCs and message types.lib.rs following the existing pattern (timeout wrapper, error handling, state update).pending_* flag to App and handle it in the event loop.Event variants.Event in event.rs.match events.next().await block in lib.rs.App state as needed from the event data.Take nvidia/tui-development from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.