<!-- STATUS: COMPLETE -->

# Phase 16 Summary: TUI — Layout, Widgets, and Command Routing

## At a Glance

**What this phase accomplishes:** Built the foundational Textual TUI application with persistent interactive session layout, custom widgets, command input/routing system, theme architecture, and Film Noir theme. The app now launches with a real layout, accepts commands, and renders content.

### Files Changed

| File | Description |
|------|-------------|
| `_planning/fixtures/seed_test_campaign.py` | Test fixture script seeding realistic campaign data for TUI development |
| `src/unfamiliar/tui/__init__.py` | Package exports for UnfamiliarApp and run_tui |
| `src/unfamiliar/tui/themes/__init__.py` | ThemeConfig dataclass, get_theme, register_theme, list_themes |
| `src/unfamiliar/tui/themes/film_noir/__init__.py` | Film Noir theme package marker |
| `src/unfamiliar/tui/themes/film_noir/theme.py` | FILM_NOIR_THEME constant with 10 command aliases and 17 vocabulary terms |
| `src/unfamiliar/tui/themes/film_noir/noir.tcss` | Film Noir TCSS stylesheet (325 lines) with dark background, amber accents |
| `src/unfamiliar/tui/views.py` | 10 view dataclasses (HeaderData, MissionSummary, StatusData, etc.) |
| `src/unfamiliar/tui/queries.py` | 7 query functions bridging DB to view data |
| `src/unfamiliar/tui/widgets/__init__.py` | Widget package exports |
| `src/unfamiliar/tui/widgets/header.py` | InvestigationHeader widget (3-line header with campaign info) |
| `src/unfamiliar/tui/widgets/command_ref.py` | CommandReference widget (right-side command panel) |
| `src/unfamiliar/tui/widgets/content.py` | ContentArea widget (main scrollable content region) |
| `src/unfamiliar/tui/widgets/command_input.py` | CommandInput widget + CommandSubmitted message |
| `src/unfamiliar/tui/commands.py` | CommandRouter, CommandDef, COMMANDS list (10 commands) |
| `src/unfamiliar/tui/app.py` | UnfamiliarApp with layout composition and 10 command handlers |
| `src/unfamiliar/cli.py` | Updated investigate/resume commands to launch TUI |

### Key Functions

| Function | Behavior |
|----------|----------|
| `get_theme(name)` | Load theme by name; raises ValueError if not found |
| `CommandRouter.parse(raw)` | Returns (canonical, args); handles aliases and multi-word commands |
| `load_header_data(db, campaign)` | Query DB and return HeaderData for header widget |
| `load_status_data(db, campaign)` | Query DB and return StatusData for case board view |
| `load_available_missions(db, campaign_id)` | Return list of MissionSummary for AVAILABLE missions |
| `load_mission_detail(db, mission_id)` | Return MissionDetail with file refs and prerequisites |
| `ContentArea.show_missions(list)` | Render mission list with Film Noir styling |
| `ContentArea.show_status(data)` | Render case board with progress bars and chains |
| `UnfamiliarApp.compose()` | Build 4-region layout: header, content+commands, input |
| `_launch_tui(campaign_dir, campaign_id)` | CLI helper to launch TUI after campaign creation/resume |

### Behavior Changes

- **CLI `investigate` command** now launches the TUI after campaign creation (was printing stats)
- **CLI `resume` command** now launches the TUI after finding campaign (was printing message)
- **10 canonical commands** available: next, complete, status, briefing, mission, notebook, unlock, help, quit, end campaign
- **Multi-word themed aliases** work correctly: "file report" → complete, "case board" → status, "break in 42" → unlock 42

---

## What Was Built

### Theme System (Tasks 1)

- `ThemeConfig` frozen dataclass with name, display_name, css_path, aliases, vocabulary
- Theme registry with `get_theme()`, `register_theme()`, `list_themes()`
- Film Noir theme with all 10 command aliases and 17 vocabulary terms
- 325-line TCSS stylesheet with CSS variables and widget-specific styles

### View Data Models (Task 2)

- 10 frozen dataclasses representing pure data for views
- No DB or Textual dependencies — pure data structures
- Includes helper `get_category_display_name()` for category name mapping

### Data Query Layer (Task 3)

- 7 query functions that transform DB models into view dataclasses
- Clean separation: widgets consume ViewData, never touch DB directly
- All functions handle edge cases (missing data, empty lists)

### Custom Widgets (Tasks 4-7)

- **InvestigationHeader**: 3-line docked header with campaign name, repo, progress stats
- **CommandReference**: Right-docked panel showing all 10 commands with aliases
- **ContentArea**: Main scrollable area with show_* methods for each content type
- **CommandInput**: Bottom-docked input with CommandSubmitted message on Enter

### Command Router (Task 8)

- 10 canonical commands defined with descriptions and accepts_args flags
- Alias resolution: themed → canonical (case-insensitive)
- Multi-word alias support (longest match first)
- Argument extraction for commands like "unlock 42"

### Main Application (Task 9)

- UnfamiliarApp with 4-region layout composition
- 10 command handlers (_handle_next, _handle_complete, etc.)
- Key bindings: q (quit), ? (help), escape (cancel)
- Dynamic CSS_PATH from theme

### CLI Integration (Task 10)

- `_launch_tui()` helper function
- investigate and resume commands now call TUI instead of printing

---

## Key Decisions

1. **Frozen dataclasses for ViewData** — Immutable data structures ensure widgets can't accidentally modify data. Enables future --plain mode without refactoring.

2. **Theme registry pattern** — `register_theme()` + `get_theme()` allows future themes to be added without modifying core code.

3. **Multi-word alias longest-match** — "break in 42" correctly parses to ("unlock", ["42"]) because we try longest alias match first.

4. **CSS variables in TCSS** — `$noir-accent`, `$noir-bg` etc. make the stylesheet maintainable and theme-aware.

5. **Handler naming convention** — `_handle_{canonical}` with space-to-underscore conversion enables simple dispatch via getattr.

---

## Major Logic Flows

### Command Dispatch Flow

```
User types "investigate" → CommandInput.on_input_submitted()
→ posts CommandSubmitted("investigate")
→ UnfamiliarApp.on_command_submitted()
→ router.parse("investigate") returns ("next", [])
→ getattr(self, "_handle_next") returns handler
→ _handle_next([]) calls load_available_missions()
→ ContentArea.show_missions(missions) renders list
```

### Multi-word Alias Resolution

```
User types "break in 42"
→ parse() normalizes to "break in 42"
→ Try 3-word alias: "break in 42" → not found
→ Try 2-word alias: "break in" → found! canonical = "unlock"
→ Remaining words = ["42"]
→ Return ("unlock", ["42"])
```

### Content Rendering

```
ContentArea.show_status(data)
→ Build Text with Rich styling
→ Section: Header with campaign name
→ Section: Overall progress (percent, counts)
→ Section: Lead Status breakdown (available/in_progress/locked/completed)
→ Section: By Precinct (categories with ASCII progress bars)
→ Section: Case Threads (chains with completion checkmarks)
→ _clear_and_set(text) updates the Static widget
```

---

## Connection to Previous Phases

- **Phase 13** — Campaign orchestration API used for complete_mission, unlock_mission, get_campaign_progress
- **Phase 15** — Pipeline produces missions that the TUI displays
- **DB layer** — CampaignDB, repositories provide all data access

---

## Test Coverage

- **903 tests passing** (no new tests added this phase)
- TUI code is primarily presentation layer — manual testing via fixture script
- Future phases may add Textual pilot tests for widget behavior

---

## Issues Discovered

None. Build completed cleanly with all 11 tasks verified.
