type: user

role: user

content:
Combined spec + code-quality review of Task 12 (TUI Browser scaffold) in worktree `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`.

Diff: `git diff f0b337f..d3eec79 -- apps/birdseye/`

## Spec — What Was Requested

Three new files in `apps/birdseye/internal/tui/`:

1. `styles.go`: Theme struct with 11 lipgloss styles (FileTree, FileTreeItem, FileSelected, StatusAdd, StatusDel, StatusMod, Viewport, Footer, AddedLine, RemovedLine, HunkHeader). `DefaultTheme()` constructor.

2. `browser.go`: `Browser` struct with `result`, `theme`, `selected`, `width`, `height`, `viewport` fields. Functions: `NewBrowser`, `SetSize`, `SelectedIndex`, `SelectedFile`, `Move`, `View`. Helpers: `refreshViewport`, `colorizeUnified`, `renderTree`.

3. `browser_test.go`: `sampleResult()` returns 2-file diff.Result. `TestNewBrowserDefaults` and `TestBrowserViewContainsBothFiles` plus a `contains` helper.

NO Init/Update/RunBrowser yet — those land in Task 13.

Commit message exact: `feat(birdseye): tui browser scaffold with multi-pane layout`.

## Quality Focus

- Style: package doc accurate, files have one responsibility each?
- View: does `View()` produce a layout with header + body via JoinVertical, body via JoinHorizontal?
- `colorizeUnified`: correctly distinguishes `@@`, `+`, `-`, and file header lines (`+++`, `---`)?
- `renderTree`: status markers correct (`+`/`-`/`~`)?
- `SetSize` clamps size sensibly when window is tiny?
- `Move`: clamps to valid range?
- `bubbles/viewport` + `lipgloss` properly imported as direct deps in go.mod?
- Test independence?

## Report Format

**Spec:** ✅ / ❌
**Quality:** Strengths / Issues / Assessment

Be terse.

userType: external

entrypoint: cli

gitBranch: worktree-birdseye


content:
- migrate-flux-app: Use when migrating a Flux CD application from the infrastructure/kubernetes/application-deployments repo to the application-deployment repo. Covers copying app folders, updating kustomizations, committing, pushing, and creating merge requests.
- app-infra-generate: Generate an application-infrastructure Terraform module from a YAML spec produced by app-infra-spec. Reads pattern files and a versions manifest to produce main.tf, variables.tf, outputs.tf, overlay guidance, and a triage list.
- app-infra-spec: Structured questionnaire to gather requirements for an application-infrastructure Terraform module. Produces a YAML spec for the app-infra-generate skill.
- obsidian-load: Load context from Obsidian notes in the `Claude/` directory into the current conversation.
- obsidian-save: Save current conversation context to an Obsidian note under the `Claude/` directory.
- mind:search: Search memories for specific content or patterns
- mind:stats: Show memory statistics and storage information
- mind:recent: Show recent memories and activity timeline
- mind:ask: Ask questions about memories and get context-aware answers
- superpowers:using-git-worktrees: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
- superpowers:receiving-code-review: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
- superpowers:finishing-a-development-branch: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
- superpowers:dispatching-parallel-agents: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
- superpowers:writing-plans: Use when you have a spec or requirements for a multi-step task, before touching code
- superpowers:using-superpowers: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
- superpowers:writing-skills: Use when creating new skills, editing existing skills, or verifying skills work before deployment
- superpowers:requesting-code-review: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
- superpowers:executing-plans: Use when you have a written implementation plan to execute in a separate session with review checkpoints
- superpowers:verification-before-completion: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
- superpowers:brainstorming: You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
- superpowers:systematic-debugging: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
- superpowers:test-driven-development: Use when implementing any feature or bugfix, before writing implementation code
- superpowers:subagent-driven-development: Use when executing implementation plans with independent tasks in the current session
- mind:memory: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- mind:mind: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled remote agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).
TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.
SKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch

type: attachment


type: message

role: assistant

type: text

text: I'll review the Task 12 implementation by examining the diff and relevant files.

service_tier: standard

attributionAgent: general-purpose

attributionPlugin: superpowers

type: assistant


Bash

command: cd /Users/steve.briggs/projects/operations && git diff f0b337f..d3eec79 -- apps/birdseye/ 2>&1

description: Get the full diff for Task 12

type: direct

speed: standard


content:
diff --git a/apps/birdseye/go.mod b/apps/birdseye/go.mod
index 93c54a8..8ebb445 100644
--- a/apps/birdseye/go.mod
+++ b/apps/birdseye/go.mod
@@ -10,8 +10,31 @@ require (
 )

 require (
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/charmbracelet/bubbles v1.0.0 // indirect
+	github.com/charmbracelet/bubbletea v1.3.10 // indirect
+	github.com/charmbracelet/colorprofile v0.4.1 // indirect
+	github.com/charmbracelet/lipgloss v1.1.0 // indirect
+	github.com/charmbracelet/x/ansi v0.11.6 // indirect
+	github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
+	github.com/charmbracelet/x/term v0.2.2 // indirect
+	github.com/clipperhouse/displaywidth v0.9.0 // indirect
+	github.com/clipperhouse/stringish v0.1.1 // indirect
+	github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
+	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-localereader v0.0.1 // indirect
+	github.com/mattn/go-runewidth v0.0.19 // indirect
+	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+	github.com/muesli/cancelreader v0.2.2 // indirect
+	github.com/muesli/termenv v0.16.0 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+	golang.org/x/sys v0.41.0 // indirect
+	golang.org/x/text v0.34.0 // indirect
 )

 replace github.com/immuta/operations/services/crow => ../../services/crow
diff --git a/apps/birdseye/go.sum b/apps/birdseye/go.sum
index f201662..6c03480 100644
--- a/apps/birdseye/go.sum
+++ b/apps/birdseye/go.sum
@@ -1,14 +1,62 @@
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
+github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
+github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
+github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
+github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
+github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
+github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
+github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
+github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
+github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
+github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
+github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
+github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
+github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
+github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
+github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
+github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
 golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/apps/birdseye/internal/tui/browser.go b/apps/birdseye/internal/tui/browser.go
new file mode 100644
index 0000000..bfc0c4a
--- /dev/null
+++ b/apps/birdseye/internal/tui/browser.go
@@ -0,0 +1,143 @@
+package tui
+
+import (
+	"strings"
+
+	"github.com/charmbracelet/bubbles/viewport"
+	"github.com/charmbracelet/lipgloss"
+
+	"github.com/immuta/operations/apps/birdseye/internal/diff"
+)
+
+// Browser is the multi-pane diff viewer: file tree on the left,
+// rendered unified diff on the right, footer at the bottom.
+type Browser struct {
+	result   diff.Result
+	theme    Theme
+	selected int
+	width    int
+	height   int
+	viewport viewport.Model
+}
+
+// NewBrowser builds a Browser from a pre-computed Result. SetSize must
+// be called once before the first View() so the viewport sizes itself.
+func NewBrowser(r diff.Result) *Browser {
+	vp := viewport.New(0, 0)
+	return &Browser{
+		result:   r,
+		theme:    DefaultTheme(),
+		viewport: vp,
+	}
+}
+
+// SetSize informs the browser of the terminal dimensions.
+func (b *Browser) SetSize(w, h int) {
+	b.width, b.height = w, h
+	fileTreeW := 32
+	if w < 80 {
+		fileTreeW = 24
+	}
+	contentW := w - fileTreeW - 4
+	contentH := h - 2
+	if contentW < 10 {
+		contentW = 10
+	}
+	if contentH < 5 {
+		contentH = 5
+	}
+	b.viewport.Width = contentW
+	b.viewport.Height = contentH
+	b.refreshViewport()
+}
+
+// SelectedIndex returns the currently focused file diff index.
+func (b *Browser) SelectedIndex() int { return b.selected }
+
+// SelectedFile returns the FileDiff at the current selection. Falls
+// back to a zero value when the result has no files.
+func (b *Browser) SelectedFile() diff.FileDiff {
+	if len(b.result.Files) == 0 {
+		return diff.FileDiff{}
+	}
+	return b.result.Files[b.selected]
+}
+
+// Move adjusts the selection by delta, clamping to the valid range, and
+// refreshes the viewport content.
+func (b *Browser) Move(delta int) {
+	if len(b.result.Files) == 0 {
+		return
+	}
+	b.selected += delta
+	if b.selected < 0 {
+		b.selected = 0
+	}
+	if b.selected >= len(b.result.Files) {
+		b.selected = len(b.result.Files) - 1
+	}
+	b.refreshViewport()
+}
+
+func (b *Browser) refreshViewport() {
+	if len(b.result.Files) == 0 {
+		b.viewport.SetContent("no differences")
+		return
+	}
+	b.viewport.SetContent(b.colorizeUnified(b.SelectedFile().Unified))
+	b.viewport.GotoTop()
+}
+
+func (b *Browser) colorizeUnified(unified string) string {
+	var sb strings.Builder
+	for _, line := range strings.Split(unified, "\n") {
+		switch {
+		case strings.HasPrefix(line, "@@"):
+			sb.WriteString(b.theme.HunkHeader.Render(line))
+		case strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++"):
+			sb.WriteString(b.theme.AddedLine.Render(line))
+		case strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---"):
+			sb.WriteString(b.theme.RemovedLine.Render(line))
+		default:
+			sb.WriteString(line)
+		}
+		sb.WriteByte('\n')
+	}
+	return sb.String()
+}
+
+// View renders the full multi-pane layout.
+func (b *Browser) View() string {
+	tree := b.renderTree()
+	right := b.theme.Viewport.Render(b.viewport.View())
+	body := lipgloss.JoinHorizontal(lipgloss.Top, tree, right)
+	footer := b.theme.Footer.Render("j/k or ↑/↓ move · q quit · esc back")
+	return lipgloss.JoinVertical(lipgloss.Left, body, footer)
+}
+
+func (b *Browser) renderTree() string {
+	var sb strings.Builder
+	for i, fd := range b.result.Files {
+		marker := " "
+		switch fd.Status {
+		case diff.StatusAdded:
+			marker = b.theme.StatusAdd.Render("+")
+		case diff.StatusRemoved:
+			marker = b.theme.StatusDel.Render("-")
+		case diff.StatusModified:
+			marker = b.theme.StatusMod.Render("~")
+		}
+		line := marker + " " + fd.Path
+		if i == b.selected {
+			line = b.theme.FileSelected.Render(line)
+		} else {
+			line = b.theme.FileTreeItem.Render(line)
+		}
+		sb.WriteString(line)
+		sb.WriteByte('\n')
+	}
+	if len(b.result.Files) == 0 {
+		sb.WriteString("(no files)")
+	}
+	return b.theme.FileTree.Render(sb.String())
+}
diff --git a/apps/birdseye/internal/tui/browser_test.go b/apps/birdseye/internal/tui/browser_test.go
new file mode 100644
index 0000000..563300c
--- /dev/null
+++ b/apps/birdseye/internal/tui/browser_test.go
@@ -0,0 +1,44 @@
+package tui
+
+import (
+	"testing"
+
+	"github.com/immuta/operations/apps/birdseye/internal/diff"
+)
+
+func sampleResult() diff.Result {
+	return diff.Result{Files: []diff.FileDiff{
+		{Path: "a.tf", Status: diff.StatusModified, Unified: "@@\n-x\n+y\n", Adds: 1, Dels: 1},
+		{Path: "b.tf", Status: diff.StatusAdded, Unified: "+++ b.tf\n+new\n", Adds: 1},
+	}}
+}
+
+func TestNewBrowserDefaults(t *testing.T) {
+	b := NewBrowser(sampleResult())
+	if b.SelectedIndex() != 0 {
+		t.Errorf("default selected index = %d, want 0", b.SelectedIndex())
+	}
+	if b.SelectedFile().Path != "a.tf" {
+		t.Errorf("first selected = %s", b.SelectedFile().Path)
+	}
+}
+
+func TestBrowserViewContainsBothFiles(t *testing.T) {
+	b := NewBrowser(sampleResult())
+	b.SetSize(120, 30)
+	view := b.View()
+	for _, want := range []string{"a.tf", "b.tf"} {
+		if !contains(view, want) {
+			t.Errorf("view missing %q\n%s", want, view)
+		}
+	}
+}
+
+func contains(s, sub string) bool {
+	for i := 0; i+len(sub) <= len(s); i++ {
+		if s[i:i+len(sub)] == sub {
+			return true
+		}
+	}
+	return false
+}
diff --git a/apps/birdseye/internal/tui/styles.go b/apps/birdseye/internal/tui/styles.go
new file mode 100644
index 0000000..8a0496e
--- /dev/null
+++ b/apps/birdseye/internal/tui/styles.go
@@ -0,0 +1,40 @@
+// Package tui hosts the bubbletea presentation for birdseye. Models are
+// pure — they never make network calls or read the filesystem. The
+// browser displays a pre-computed diff.Result; the picker (Task 14)
+// dispatches network work as tea.Cmds.
+package tui
+
+import "github.com/charmbracelet/lipgloss"
+
+// Theme keeps every lipgloss style in one place for easy tweaking.
+type Theme struct {
+	FileTree     lipgloss.Style
+	FileTreeItem lipgloss.Style
+	FileSelected lipgloss.Style
+	StatusAdd    lipgloss.Style
+	StatusDel    lipgloss.Style
+	StatusMod    lipgloss.Style
+	Viewport     lipgloss.Style
+	Footer       lipgloss.Style
+	AddedLine    lipgloss.Style
+	RemovedLine  lipgloss.Style
+	HunkHeader   lipgloss.Style
+}
+
+// DefaultTheme returns a high-contrast theme that should look reasonable
+// in both light and dark terminals.
+func DefaultTheme() Theme {
+	return Theme{
+		FileTree:     lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).Padding(0, 1),
+		FileTreeItem: lipgloss.NewStyle(),
+		FileSelected: lipgloss.NewStyle().Bold(true).Reverse(true),
+		StatusAdd:    lipgloss.NewStyle().Foreground(lipgloss.Color("10")),
+		StatusDel:    lipgloss.NewStyle().Foreground(lipgloss.Color("9")),
+		StatusMod:    lipgloss.NewStyle().Foreground(lipgloss.Color("11")),
+		Viewport:     lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).Padding(0, 1),
+		Footer:       lipgloss.NewStyle().Faint(true),
+		AddedLine:    lipgloss.NewStyle().Foreground(lipgloss.Color("10")),
+		RemovedLine:  lipgloss.NewStyle().Foreground(lipgloss.Color("9")),
+		HunkHeader:   lipgloss.NewStyle().Foreground(lipgloss.Color("14")),
+	}
+}


command: cd /Users/steve.briggs/projects/operations && git show d3eec79 --stat 2>&1 | head -20

description: Check commit message and stats


command: cd /Users/steve.briggs/projects/operations && cat apps/birdseye/go.mod 2>&1

description: Check go.mod for direct vs indirect dep markers


content:
commit d3eec79a8f8af05d8df99a15af0040241ba27076
Author: mr0xb <47467008+mr0xb@users.noreply.github.com>
Date:   Tue May 12 23:56:56 2026 -0400

    feat(birdseye): tui browser scaffold with multi-pane layout

 apps/birdseye/go.mod                       |  23 +++++
 apps/birdseye/go.sum                       |  48 ++++++++++
 apps/birdseye/internal/tui/browser.go      | 143 +++++++++++++++++++++++++++++
 apps/birdseye/internal/tui/browser_test.go |  44 +++++++++
 apps/birdseye/internal/tui/styles.go       |  40 ++++++++
 5 files changed, 298 insertions(+)


toolUseResult:
Error: Exit code 1
cat: apps/birdseye/go.mod: No such file or directory


description: Check worktree structure


content:
drwxr-xr-x steve.briggs staff 256 B  Tue May 12 18:19:50 2026 ..
.rw-r--r-- steve.briggs staff 226 B  Tue May 12 18:19:59 2026 Makefile
.rw-r--r-- steve.briggs staff   6 B  Tue May 12 18:20:01 2026 .gitignore
.rw-r--r-- steve.briggs staff 602 B  Tue May 12 18:20:06 2026 main.go
drwxr-xr-x steve.briggs staff 288 B  Tue May 12 23:12:50 2026 .
drwxr-xr-x steve.briggs staff 320 B  Tue May 12 23:53:15 2026 cmd
drwxr-xr-x steve.briggs staff 192 B  Tue May 12 23:55:20 2026 internal
.rw-r--r-- steve.briggs staff 1.7 KB Tue May 12 23:55:54 2026 go.mod
.rw-r--r-- steve.briggs staff 5.5 KB Tue May 12 23:55:54 2026 go.sum


command: cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./internal/tui/... -v 2>&1

description: Run the tui tests


content:
1	module github.com/immuta/operations/apps/birdseye
2
3	go 1.25.5
4
5	require (
6		github.com/hexops/gotextdiff v1.0.3
7		github.com/immuta/operations/services/crow v0.0.0-20260512210412-d8eb33139c1a
8		github.com/spf13/cobra v1.9.1
9		golang.org/x/sync v0.20.0
10	)
11
12	require (
13		github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
14		github.com/charmbracelet/bubbles v1.0.0 // indirect
15		github.com/charmbracelet/bubbletea v1.3.10 // indirect
16		github.com/charmbracelet/colorprofile v0.4.1 // indirect
17		github.com/charmbracelet/lipgloss v1.1.0 // indirect
18		github.com/charmbracelet/x/ansi v0.11.6 // indirect
19		github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
20		github.com/charmbracelet/x/term v0.2.2 // indirect
21		github.com/clipperhouse/displaywidth v0.9.0 // indirect
22		github.com/clipperhouse/stringish v0.1.1 // indirect
23		github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
24		github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
25		github.com/inconshreveable/mousetrap v1.1.0 // indirect
26		github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
27		github.com/mattn/go-isatty v0.0.20 // indirect
28		github.com/mattn/go-localereader v0.0.1 // indirect
29		github.com/mattn/go-runewidth v0.0.19 // indirect
30		github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
31		github.com/muesli/cancelreader v0.2.2 // indirect
32		github.com/muesli/termenv v0.16.0 // indirect
33		github.com/rivo/uniseg v0.4.7 // indirect
34		github.com/spf13/pflag v1.0.6 // indirect
35		github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
36		golang.org/x/sys v0.41.0 // indirect
37		golang.org/x/text v0.34.0 // indirect
38	)
39
40	replace github.com/immuta/operations/services/crow => ../../services/crow
41
42	replace github.com/immuta/operations/libs/clog => ../../libs/clog
43


content:
=== RUN   TestNewBrowserDefaults
--- PASS: TestNewBrowserDefaults (0.00s)
=== RUN   TestBrowserViewContainsBothFiles
--- PASS: TestBrowserViewContainsBothFiles (0.00s)
PASS
ok  	github.com/immuta/operations/apps/birdseye/internal/tui	0.572s


text:
All tests pass. Here is the review.

---

**Spec: PASS**

All three files are present with the correct package, correct struct fields, all 11 styles, and all required functions. No Init/Update/RunBrowser introduced. Commit message matches exactly.

---

**Quality**

**Strengths**

- `styles.go` has one responsibility (theme) with a clean package-doc comment that accurately describes the whole `tui` package and previews the Task 14 picker — good forward context without over-engineering.
- `colorizeUnified` correctly handles the ordering problem: `---`/`+++` file headers match `default` because the `+`/`-` cases guard with `!HasPrefix(line, "+++")` / `!HasPrefix(line, "---")`. The `@@` hunk header is checked first, which is correct since `@@` lines never start with `+` or `-`.
- `renderTree` status markers are `+`/`-`/`~` as specified, each rendered through the appropriate themed style before being used as a plain prefix character — no raw ANSI leakage.
- `Move` clamps correctly in both directions and calls `refreshViewport` after every mutation.
- `SetSize` has a two-tier clamp: `fileTreeW` shrinks from 32 to 24 below 80 columns, and `contentW`/`contentH` floor at 10/5 respectively — sensible for tiny windows.
- `View` produces `JoinVertical(Left, body, footer)` where `body = JoinHorizontal(Top, tree, right)` — matches the spec's required layout structure.
- Tests are fully independent (no shared state, no `TestMain`), and `sampleResult` gives a two-file result covering both `StatusModified` and `StatusAdded`.

**Issues**

1. **`bubbles` and `lipgloss` are `// indirect` in go.mod.** Both are directly imported in `browser.go` and `styles.go`, so they must appear in the top-level `require` block without the `// indirect` marker. `go mod tidy` would move them to direct. This is a correctness issue in the module graph, not just cosmetic — it signals the module graph is stale and will produce a `go mod tidy` diff in CI.

2. **`contains` helper re-implements `strings.Contains`.** The spec calls for a `contains` helper, and the implementation is functionally correct, but it's a hand-rolled O(n·m) substring search when `strings.Contains` does the same thing. Not a bug, but pointless complexity in test code.

3. **`refreshViewport` resets to `GotoTop()` on every selection change.** A user who has scrolled a long diff mid-way and nudges the selection by 1 will lose their scroll position. For a scaffold this is acceptable, but it should be noted as a known limitation before Task 13 wires up keyboard input.

4. **No `---`/`+++` header styling.** The spec lists `FileTree`, `FileTreeItem`, `FileSelected`, `StatusAdd`, `StatusDel`, `StatusMod`, `Viewport`, `Footer`, `AddedLine`, `RemovedLine`, `HunkHeader` — 11 styles, all present. File-header lines (`---`/`+++`) intentionally fall through to `default` (uncolored). That is consistent with typical diff renderers and not a spec violation, but worth a brief inline comment since the guard conditions (`!HasPrefix(line, "+++")`) look like bugs until you understand the intent.

**Assessment:** Solid scaffold. One real issue to fix before merging: `bubbles` and `lipgloss` must be promoted to direct dependencies in go.mod (`go mod tidy` in `apps/birdseye/`). The other findings are minor.