# Build Spec: Shot-Grouped Tree for Workspace

## Objective
Restructure the workspace file tree so that shots appear as single nodes (not one node per take). Takes are shown in the inspector when a shot is clicked. The tree becomes: Episode → Shot → (click to see takes in inspector).

## Context
- Workspace server: `recoil/workspace/server.py`
- Frontend: `recoil/workspace/static/workspace.js`, `workspace.css`, `index.html`
- The tree is built by `_scan_output_dir()` in server.py which does a raw filesystem scan
- Shot/take data lives in ExecutionStore (shots.db) — but may be empty for some projects
- Files follow naming patterns like `ep_001_shot_001_take1.mp4`, `shot_001_take7.mp4`
- The refs tree (characters, locations, props) should NOT be grouped — only frames/video output

## Phase 1: Server-side shot grouping

**File:** `recoil/workspace/server.py`

In `_scan_output_dir()`, after scanning the filesystem, post-process the tree to group files by shot ID:

1. For directories under `output/frames/` and `output/video/`, detect files that belong to the same shot using filename pattern matching:
   - Pattern: `{prefix}_shot_{NNN}_take{N}.{ext}` or `shot_{NNN}_take{N}.{ext}`
   - Also handle: `{PROJ}EP{NNN}_SH{NN}_take{N}.{ext}` and similar variations
   - Extract `shot_id` and `take_number` from the filename

2. Group files with the same shot_id into a single node:
   ```json
   {
     "name": "SH01",
     "type": "shot",
     "shot_id": "EP001_SH01",
     "path": null,
     "takes": [
       {"name": "take1.mp4", "path": "output/video/ep_001/shot_001_take1.mp4", "media_url": "..."},
       {"name": "take2.mp4", "path": "output/video/ep_001/shot_001_take2.mp4", "media_url": "..."},
       {"name": "keyframe.png", "path": "output/frames/ep_001/shot_001.png", "media_url": "..."}
     ],
     "take_count": 3,
     "status": "video_complete",
     "status_color": "green",
     "model": "kling-v2"
   }
   ```

3. Files that DON'T match a shot pattern stay as regular file nodes (no grouping).

4. The `refs/` subtree is NOT grouped — it stays as-is (individual files).

**Validation gate:** The tree JSON returned by `/api/tree/{project}` should have `"type": "shot"` nodes in frames/video directories. Load Tartarus (which has 72 videos) and verify the tree is shorter.

## Phase 2: Frontend shot node rendering

**File:** `recoil/workspace/static/workspace.js`

1. In `renderTreeNode()`, handle `node.type === 'shot'`:
   - Render as a single clickable item (like a file node) showing the shot name + take count badge
   - Example: `SH01 (3 takes)` or `SH01 ×3`
   - Use the shot's status_color for the status dot
   - On click: set `WS.selection = [node.takes[0].path]`, display first take in viewer, load shot detail in inspector

2. In `showContextMenu()`, handle shot nodes:
   - Show: Pin, Archive All Takes, Copy Path (of first take)
   - Don't show: Promote to Canonical (doesn't make sense for shots)

3. Arrow navigation within a shot: when a shot node is selected and arrows are pressed, cycle through that shot's takes in the viewer.

**Validation gate:** Load Tartarus workspace, verify tree shows collapsed shots, clicking a shot shows takes in inspector, arrows cycle takes.

## Phase 3: Take count badge CSS

**File:** `recoil/workspace/static/workspace.css`

Add styling for the take count badge:
```css
.take-count {
  font-size: 9px;
  color: var(--text-dim);
  margin-left: 4px;
}
```

**Validation gate:** Badge is visible, doesn't overflow the narrow panel.

## Phase 4: Filter compatibility

**File:** `recoil/workspace/static/workspace.js`

Update `applyTreeFilter()` to handle shot nodes:
- Media type filter: a shot matches if ANY of its takes match the type
- Model filter: match on the shot's model field
- Text filter: match on shot_id or any take filename
- Shot nodes need `data-media-type`, `data-model` attributes (use the primary take's type, or "mixed" if both stills and video)

**Validation gate:** Filter by video-only, verify shot nodes with only keyframe takes are hidden. Filter by model, verify correct filtering.

## Test Plan
1. Load Tartarus (has 72 videos across multiple shots) — tree should be dramatically shorter
2. Load afterimage-anime (has keyframes but empty execution store) — frames should group, refs should not
3. Click a shot → inspector shows takes, arrows navigate
4. Filter by video → shots with only stills hidden
5. Multi-select shots → bulk actions work
6. Pin a shot → pin icon appears

## Anti-patterns to avoid
- Do NOT modify the refs tree — only frames/video output
- Do NOT require ExecutionStore data — grouping must work from filenames alone (execution store may be empty)
- Do NOT break the existing sidecar/inspector flow for individual files in refs
- Do NOT use complex regex that breaks on unexpected filenames — be lenient, fall back to ungrouped
