# DefenseClaw TUI

The DefenseClaw TUI is a Bubbletea (charmbracelet) dashboard built into the Go
gateway binary (`defenseclaw-gateway tui`, also reachable as `defenseclaw tui`
when the gateway is on `PATH`). It exposes every CLI capability through 12
keyboard-driven panels backed by the same Python CLI commands an operator
would run from a shell — the TUI never reimplements business logic, it shells
out to `defenseclaw` and observes the resulting audit events.

> **Tech stack:** Bubbletea + Lipgloss + Bubbles. Source lives in
> `internal/tui/` (one file per panel). The root model is
> `internal/tui/app.go::Model`.

---

## Launching

| Command | Notes |
|---------|-------|
| `defenseclaw` (no args, on a TTY) | Auto-handoff to `defenseclaw-gateway tui` if the gateway binary is on PATH (`cli/defenseclaw/main.py::_try_launch_tui`). |
| `defenseclaw tui` | Same handoff, explicit. |
| `defenseclaw-gateway tui` | Direct invocation. Useful when scripting. |

The TUI requires a real terminal. In CI / SSH-without-TTY contexts it
gracefully falls back to the Click CLI.

---

## Panels

Panels are addressable by number (1–9, 0 for Setup) and by `Tab` /
`Shift+Tab` cycling. The active panel is highlighted in the top bar.

| # | Panel | Source | What it does |
|---|-------|--------|--------------|
| 1 | Overview | `overview.go` | Dashboard home — gateway/guardrail status, recent activity, **cached `doctor` snapshot**, contextual notices. |
| 2 | Alerts | `alerts.go` | Live security alerts. Detail view shows humanized findings, scanner names, remediation, and `TraceID`/`RequestID` for correlation. |
| 3 | Skills | `skills.go` | Skill inventory with scan status. `Enter` opens the action menu (scan, block, allow, quarantine, restore, info). |
| 4 | MCPs | `mcps.go` | MCP servers. Same action menu as Skills, plus `set`/`unset` flows for OpenClaw integration. |
| 5 | Plugins | `plugins.go` | Installed plugins with status, install/remove/quarantine actions. |
| 6 | Inventory | `inventory.go` | AIBOM scan results. **Filter chips** (`f`, `o`) toggle category scope between full and "fast scan" presets. |
| 7 | Policy | `policy.go` | Policy CRUD. Sub-tabs for rule packs, edits, and `T` runs `policy test`. |
| 8 | Logs | `logs.go` | Live gateway + watchdog logs. `/` filters, `Space` pauses tail. |
| 9 | Audit | `audit.go` | Append-only audit store. Every block/allow/scan/config-change is here. |
| - | Activity | `activity.go` | Live tail of TUI-launched commands (output stream, exit codes). |
| - | Tools | `tools.go` | Tool block/allow/unblock surface. |
| - | Registries | `registries.go` | External skill / MCP catalog sources (corporate HTTPS YAML, smithery.ai, git, ClawHub, file). Sources / Entries / Approved sub-tabs (`1` / `2` / `3`). `s` syncs a single source, `S` syncs all enabled, `a` / `x` approve / reject. Reachable via `R`. See [`docs/REGISTRIES.md`](./REGISTRIES.md). |
| 0 | Setup | `setup.go` | All editable config — scanner endpoints, gateway watcher/watchdog, OTel signals, observability sinks, webhooks, guardrail tuning, OpenShell sandbox. The Registries wizard wraps `defenseclaw registry add --non-interactive` for first-run discovery. |

---

## Universal Keybindings

| Key | Action |
|-----|--------|
| `1`–`9`, `0` | Jump to panel by number |
| `T` | Jump to Tools panel |
| `R` | Jump to Registries panel (Skills / MCPs panels: jumps with cursor focused on the highlighted entry) |
| `Tab` / `Shift+Tab` | Cycle panels forward / back |
| `:` or `Ctrl+K` | Open command palette (any registry entry) |
| `/` | Filter the active list |
| `?` | Help overlay (full keybinding reference) |
| `Ctrl+C` / `q` | Quit |
| `r` | Manual refresh |

Per-panel hints live in the bottom status bar and are generated by
`internal/tui/hints.go::HintEngine`.

---

## Single Source of Truth: TUI → CLI Routing

Every TUI mutation routes through the Python CLI. There is **no separate
"TUI mode" code path in the Go gateway** for these actions:

```
[Skills panel] press 'b' on a row
       │
       ▼
internal/tui/app.go::executeActionMenuItem()
       │
       ▼  (CommandExecutor, streams output to Activity panel)
defenseclaw skill block <name> --non-interactive
       │
       ▼  (Click handler in cli/defenseclaw/commands/cmd_skill.py)
audit event "skill.blocked" → SQLite → propagates back to all panels
```

Why this matters:

- **One implementation per action.** Bug fixes in Python land in both the
  CLI and the TUI for free.
- **Audit parity.** Every TUI click produces the same audit event a CLI
  invocation would, with the same `request_id`/`trace_id`.
- **Reviewable history.** A user can run `defenseclaw audit` and see exactly
  what their TUI session did, in CLI-replayable form.
- **Easy automation.** Anything you can click, you can script.

The mapping table is `internal/tui/command.go::BuildRegistry()`. It returns
96 `CmdEntry` records (TUI label → CLI binary + args). To prevent drift,
the test `internal/tui/cli_parity_test.go::TestBuildRegistryParity`
introspects the live Click tree via `scripts/audit_parity.py` and asserts:

1. Every `defenseclaw` subcommand the TUI invokes actually exists.
2. Every flag the TUI passes is recognized by the target subcommand.

Gateway (`defenseclaw-gateway`) entries are validated by the
`test/e2e/gateway_command_help_test.sh` shell suite instead — they're
Cobra commands and fall outside the Click introspection.

---

## Cached `doctor` Status (Overview Panel)

`defenseclaw doctor` is network-intensive (it probes scanners, gateway,
sinks, OpenClaw connectivity). To keep the Overview panel fast, the CLI
**writes a cache file** every time it runs:

```
~/.defenseclaw/doctor_cache.json
```

Format (see `internal/tui/doctor_cache.go::DoctorCache`):

```json
{
  "captured_at": "2026-04-17T18:21:09Z",
  "passed": 12,
  "failed": 0,
  "warned": 1,
  "skipped": 2,
  "checks": [
    {"status": "warn", "label": "Splunk HEC", "detail": "queue depth 4200/5000"}
  ]
}
```

Behavior:

- The CLI writes the file **atomically** (`tempfile + os.replace`) at the end
  of every `doctor` run, including failed runs, so the snapshot always
  reflects the most recent reality.
- The TUI loads it on startup (`Model.Init`) and refreshes after any
  `defenseclaw doctor` invocation completes (`isDoctorCommand` matched in
  `CommandDoneMsg` handler).
- **Stale threshold**: 15 minutes. Older snapshots show a `(stale — [d] to
  rerun)` suffix and a top-of-screen notice. `[d]` triggers the same Click
  command, which writes a fresh cache.
- **Empty state**: first launch shows `not yet run — press [d] to probe`,
  no panic, no fallback to a network call.

---

## Observability Editor (Setup → Sinks / Webhooks)

The Setup panel includes two distinct mutation surfaces:

| Editor | Backing CLI | Purpose |
|--------|-------------|---------|
| Audit Sinks | `defenseclaw setup observability {add,remove,enable,disable,test}` | Forward every audit event to JSONL/HEC/OTLP/HTTP destinations. Bulk export channel. |
| Webhooks | `defenseclaw setup webhook {add,remove,enable,disable,test}` | Per-event chat/incident notifications (Slack/PagerDuty/Webex/HMAC). Cooldowns + per-channel filters. |

These are **not interchangeable**. See `CLAUDE.md` for the full
distinction. The TUI editors mirror the CLI wizards line-for-line.

---

## Inventory Filter Chips (Panel 6)

The Inventory panel calls `defenseclaw aibom scan --json --only=…` to feed
its rows. Two key bindings shape the result set:

| Key | Effect |
|-----|--------|
| `f` | Open the filter chip selector — toggle individual categories (`skills`, `mcps`, `agents`, `tools`, `models`, `memory`, `plugins`). |
| `o` | Toggle "fast scan" preset — narrows to `skills,mcps,plugins` for sub-second refreshes during triage. |

State is stored in `InventoryPanel.categoryScope` and survives panel
switches within the session.

---

## Files Created/Read by the TUI

| Path | Direction | Reason |
|------|-----------|--------|
| `~/.defenseclaw/doctor_cache.json` | **read** | Doctor snapshot for Overview. Written by `defenseclaw doctor`. |
| `~/.defenseclaw/config.yaml` | read+write | All Setup panel edits route through `defenseclaw setup …` which serializes via `cli/defenseclaw/config.py::cfg.save()`. |
| `~/.defenseclaw/.env` | read | Resolves API key env vars when displaying current values (never written by the TUI directly). |
| `~/.defenseclaw/audit.db` | **read** | SQLite backing for Alerts, Audit, Inventory panels. |
| `~/.defenseclaw/guardrail_runtime.json` | **read** | Hot-reload guardrail mode shown in status bar. |

The TUI **never opens config.yaml directly** — every persistent change
goes through the Python CLI for audit-event parity. See
[CONFIG_FILES.md](CONFIG_FILES.md) for the complete file map.

---

## Testing the TUI

The TUI has unit and renderer tests at `internal/tui/*_test.go`:

```bash
go test ./internal/tui/                # all panel + parity tests
go test ./internal/tui/ -run Parity    # CLI ↔ TUI mapping check
go test ./internal/tui/ -run Doctor    # doctor cache + Overview box
```

Renderer tests (e.g. `overview_doctor_test.go`, `alerts_enrichment_test.go`,
`inventory_scope_test.go`) call `panel.View()` directly and assert on the
ANSI-stripped output. They run headless and need no terminal.

---

## Extending the TUI

To add a new TUI action that calls a new CLI subcommand:

1. Add the Click subcommand to `cli/defenseclaw/commands/cmd_*.py`.
2. Add the matching `CmdEntry` to `internal/tui/command.go::BuildRegistry()`.
3. Wire the keystroke / menu item in the relevant panel file.
4. Run `go test ./internal/tui/ -run TestBuildRegistryParity` to confirm
   the registry entry is valid (the test will fail loudly if you typo a
   flag).

That's the whole loop — no Go-side reimplementation required.
