# Review Tab (v2) ### Route: `/initiatives/$id` (Review tab) ### Source: `packages/web/src/components/review/ReviewTab.tsx` --- ## v1 -> v2 Changes | Aspect | v1 | v2 | |--------|----|----| | Loading state | Plain "Loading diff..." text | Centered `[spinner]` + "Loading diff..." text | | Error state | None | `[AlertCircle]` + error message + `[Retry]` button | | Unresolved threads | Count in diff header only | Count in header + `[ChevronUp]`/`[ChevronDown]` prev/next-unresolved buttons in sidebar | | Empty state | "No changes in this phase" | `[check-circle]` icon + "No changes to review" + description | --- ## Default State (with diff and threads) ``` +=============================================================================+ | Files (3) | Diff: src/auth/oauth.ts [Unified|Split] 3 unresolved |=| Threads [^] [v] | | ---------------+-------------------------------------------------------------+-+ | | * oauth.ts | @@ -12,6 +12,14 @@ |‖| Thread #1 | | pkce.ts | import { generatePKCE } from './pkce'; |‖| Line 14 | | index.ts | +export async function authorize( [+ comment] |‖| "Should we | | | + client: OAuthClient, |‖| validate | | | + scopes: string[] |‖| scopes?" | | | +) { |‖| [Reply___________] | | | + const { verifier, challenge } = generatePKCE(); |‖| [Resolve] | | | + // ... |‖| | | | +} |‖| Thread #2 | | | |‖| Line 28 | | | export function validateToken( |‖| "Add expiry | | | - token: string |‖| check here" | | | + token: string, |‖| [RESOLVED] | | | + options?: ValidateOptions |‖| | | | |‖| | +=============================================================================+ ^ resize handle (drag) ``` ### Layout: three-pane | Pane | Width | Notes | |------|-------|-------| | File tree | `w-48` (192px), collapsible | Lists all files in the diff. `*` marks files with unresolved threads. Click to navigate. | | Diff viewer | flex-1 (fills remaining) | Unified or split view. Scrolls independently. | | Threads sidebar | min `240px`, default `320px`, max `480px`, **resizable** via drag handle | `‖` is a 4px drag handle (`cursor-col-resize`). User can drag to resize. Stores width in `localStorage` key `cw-review-sidebar-width`. | ### Navigation buttons `[^]` (ChevronUp) = "Previous unresolved". `[v]` (ChevronDown) = "Next unresolved". Both scroll the diff viewer to the corresponding unresolved thread and highlight it. Disabled when all threads are resolved. Wraps around (last -> first, first -> last). Both have visible text labels on wider viewports: `[^ Prev] [Next v]`. On narrow viewports (< 1280px), icon-only with tooltips: "Previous unresolved (Shift+N)" / "Next unresolved (N)". ### Diff header anatomy ``` +------------------------------------------------------------------------+ | Diff: [Unified | Split] N unresolved | +------------------------------------------------------------------------+ ``` - File path is the currently-focused file (bold `font-mono text-sm font-medium`) - `[Unified | Split]` — segmented toggle, same style as ``. Default: Unified. Persists in `localStorage` key `cw-review-view-mode`. - **Unified**: standard diff with `+`/`-` lines interleaved (default) - **Split**: side-by-side two-column view — old on left, new on right, synced scroll - "N unresolved" uses `text-status-warning-fg` when N > 0, `text-muted-foreground` when 0 ### Sidebar header anatomy ``` +----------------------------------+ | Threads (3) [^] [v] | +----------------------------------+ ``` - `(3)` = total thread count for current file, `text-muted-foreground` - `[^]` (ChevronUp icon) scrolls to previous unresolved thread. Tooltip: "Previous unresolved (Shift+N)" - `[v]` (ChevronDown icon) scrolls to next unresolved thread. Tooltip: "Next unresolved (N)" - Both buttons: `variant="ghost" size="icon-sm"`, disabled state when 0 unresolved --- ## Loading State ``` +=============================================================================+ | | | | | [spinner] | | Loading diff... | | | | | +=============================================================================+ ``` Centered vertically and horizontally within the tab content area. Spinner uses `animate-spin` on a `Loader2` Lucide icon. Text is `text-sm text-muted-foreground`. --- ## Error State ``` +=============================================================================+ | | | | | [AlertCircle] | | Failed to load diff data | | | | [ Retry ] | | | +=============================================================================+ ``` - `[AlertCircle]` icon: `h-8 w-8 text-destructive` - Error message: `text-sm text-destructive` - `[ Retry ]` button: `variant="outline" size="sm"`, calls `diffQuery.refetch()` --- ## Empty State (no proposals/diffs) ``` +=============================================================================+ | | | [check-circle] | | No changes to review | | All proposals have been reviewed. | | | +=============================================================================+ ``` - `[check-circle]` icon: `h-8 w-8 text-status-success-fg` - Title: `text-sm font-medium` - Description: `text-sm text-muted-foreground` This replaces the v1 "No phases pending review" plain text. --- ## Full Page Context (within initiative detail) ``` +=============================================================================+ | [CW] [*Initiatives*] Agents *3 Inbox (2) Settings [cmd-k] [sun] * | +=============================================================================+ | < Back | | Auth System Overhaul [ACTIVE] [REVIEW] [git] cw/auth [P] backend | | [ Content ] [ Plan ] [ Execution ] [*Review*] | +-----------------------------------------------------------------------------| | | | Phase: [*Phase 2: OAuth*] [Phase 3: UI] | | | | +-----------------------------------------------------------+ | | | [play] Start Preview | | | +-----------------------------------------------------------+ | | | | Files (3) | Diff: src/auth/oauth.ts [Uni|Spl] 3 unresolved | Threads [^][v] | | ----------+---------------------------------------------------+-+ | | * oauth | @@ -12,6 +12,14 @@ |‖| Thread #1 | | pkce | import { generatePKCE } from './pkce'; |‖| Line 14 | | index | +export async function authorize( |‖| "Should we | | | + client: OAuthClient, |‖| validate | | | + scopes: string[] |‖| scopes?" | | | +) { |‖| [Reply___] | | | + const { verifier, challenge } = ... |‖| [Resolve] | | | + // ... |‖| | | | +} |‖| Thread #2 | | | |‖| Line 28 | | | export function validateToken( |‖| "Add expiry | | | - token: string |‖| check here" | | | + token: string, |‖| [RESOLVED] | | | + options?: ValidateOptions |‖| | | | |‖| | +=============================================================================+ ``` --- ## Thread States ### Unresolved thread (in sidebar) ``` +----------------------------------+ | Thread #1 [x] | | Line 14 · agent: blue-fox-7 | | "Should we validate | | scopes before passing | | to the provider?" | | | | [Reply_____________________] | | [Resolve] | +----------------------------------+ ``` - `[x]` = delete thread (destructive, Shift+click to skip confirmation per UI Patterns) - Line number is a clickable link — scrolls diff viewer to that line and highlights it - Agent attribution shows who created the thread (`text-xs text-muted-foreground font-mono`) - Reply input: `