---
phase: 23-caption-history-panel
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistoryPanel.swift
  - plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistory.swift
  - plugins/claude-tts-companion/Sources/CompanionCore/HTTPControlServer.swift
  - plugins/claude-tts-companion/Sources/CompanionCore/CompanionApp.swift
autonomous: true
requirements: [CAPT-01, CAPT-02, CAPT-03, CAPT-04]

must_haves:
  truths:
    - "A scrollable NSPanel shows past captions with HH:MM timestamps"
    - "Panel auto-scrolls to latest entry when new captions arrive"
    - "Manual scroll up pauses auto-scroll; returning to bottom resumes it"
    - "Clicking a caption copies its text to the macOS clipboard"
    - "POST /captions/panel/show opens the panel; POST /captions/panel/hide closes it"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistoryPanel.swift"
      provides: "Scrollable caption history NSPanel with NSTableView"
      min_lines: 80
    - path: "plugins/claude-tts-companion/Sources/CompanionCore/HTTPControlServer.swift"
      provides: "Panel show/hide HTTP endpoints"
      contains: "/captions/panel"
  key_links:
    - from: "CaptionHistoryPanel"
      to: "CaptionHistory"
      via: "reads entries via getAll()"
      pattern: "captionHistory\\.getAll"
    - from: "HTTPControlServer"
      to: "CaptionHistoryPanel"
      via: "MainActor.run show/hide calls"
      pattern: "captionHistoryPanel\\."
    - from: "CompanionApp"
      to: "CaptionHistoryPanel"
      via: "creates and passes to HTTPControlServer"
      pattern: "CaptionHistoryPanel"
---

<objective>
Build the caption history panel UI and HTTP API endpoints so users can view, scroll, and copy past subtitle captions.

Purpose: Users need a way to review what Claude said in past TTS sessions -- especially when they missed audio or want to reference a specific phrase.
Output: CaptionHistoryPanel.swift (new), updated HTTPControlServer with panel toggle endpoints, wired into CompanionApp.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistory.swift
@plugins/claude-tts-companion/Sources/CompanionCore/SubtitlePanel.swift
@plugins/claude-tts-companion/Sources/CompanionCore/HTTPControlServer.swift
@plugins/claude-tts-companion/Sources/CompanionCore/CompanionApp.swift

<interfaces>
<!-- Key types and contracts the executor needs -->

From CaptionHistory.swift:

```swift
public struct CaptionEntry: Codable, Sendable {
    let text: String
    let timestamp: String  // ISO 8601
    let index: Int
}

public final class CaptionHistory: @unchecked Sendable {
    func record(_ text: String)
    func getAll(limit: Int? = nil) -> [CaptionEntry]
    var count: Int
    @MainActor func copyToClipboard(limit: Int? = nil) -> Int
    func clear()
}
```

From HTTPControlServer.swift:

```swift
public final class HTTPControlServer: @unchecked Sendable {
    init(settingsStore:, subtitlePanel:, playbackManager:, ttsEngine:, captionHistory:, pipelineCoordinator:)
    func start() async throws
    // Uses: await server.appendRoute("METHOD /path") { request in ... }
    // Helper: jsonResponse<T: Encodable>(_ value: T) -> HTTPResponse
}
```

From CompanionApp.swift:

```swift
public final class CompanionApp: @unchecked Sendable {
    @MainActor public init()  // Creates all subsystems
    @MainActor public func start()
    @MainActor public func shutdown()
}
```

SubtitlePanel pattern (for NSPanel setup):

```swift
@MainActor
public final class SubtitlePanel: NSPanel {
    init(settingsStore: SettingsStore? = nil) {
        super.init(contentRect: NSRect(...), styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
        // configureWindowBehavior(), configureContentView(), positionOnScreen()
    }
    // level = .floating, collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
    // sharingType = .readOnly, ignoresMouseEvents = true (BUT history panel needs clicks)
}
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: CaptionHistoryPanel NSPanel with scrollable table, timestamps, and click-to-copy</name>
  <files>
    plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistoryPanel.swift
    plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistory.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/CompanionCore/SubtitlePanel.swift
    plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistory.swift
  </read_first>
  <action>
Create `CaptionHistoryPanel.swift` as a `@MainActor public final class CaptionHistoryPanel: NSPanel`.

**Window setup** (similar to SubtitlePanel but interactive):

- `contentRect`: 400x500 centered on main screen
- `styleMask`: `[.titled, .closable, .resizable, .nonactivatingPanel]` -- user needs to interact (NOT borderless like SubtitlePanel)
- `level`: `.floating` (stays on top like SubtitlePanel)
- `collectionBehavior`: `[.canJoinAllSpaces, .fullScreenAuxiliary]`
- `sharingType`: `.readOnly`
- `title`: "Caption History"
- `ignoresMouseEvents = false` -- user must click to copy (DIFFERS from SubtitlePanel)
- `isMovableByWindowBackground = true`
- `minSize = NSSize(width: 300, height: 200)`

**Content view hierarchy:**

- NSScrollView filling the panel content area via Auto Layout
- NSTableView with 2 columns: "Time" (60pt fixed width, HH:MM format) and "Caption" (flexible width)
- NSTableView uses `NSTableViewDiffableDataSource` or manual `NSTableViewDataSource`/`NSTableViewDelegate` pattern
- Table rows show: timestamp formatted as HH:MM (parse ISO 8601 from CaptionEntry.timestamp), caption text (word-wrapped)
- Dark appearance: `appearance = NSAppearance(named: .darkAqua)`
- Background color: dark semi-transparent (similar to subtitle panel aesthetic)

**Properties:**

- `private let captionHistory: CaptionHistory` -- reference to shared ring buffer
- `private let tableView: NSTableView`
- `private let scrollView: NSScrollView`
- `private var entries: [CaptionEntry] = []` -- snapshot for table display
- `private var isUserScrolling: Bool = false` -- tracks manual scroll override (CAPT-02)
- `private let logger = Logger(label: "caption-history-panel")`

**Init:**

- `init(captionHistory: CaptionHistory)` -- store reference, call super.init, configure window, build content view

**Public API:**

- `func show()` -- refresh entries from `captionHistory.getAll()`, reload table, auto-scroll to bottom, `orderFrontRegardless()`
- `func hide()` -- `orderOut(nil)`
- `func refresh()` -- reload entries from captionHistory, reload table, auto-scroll if `!isUserScrolling`

**Auto-scroll (CAPT-02):**

- After reloading table data, if `!isUserScrolling`, scroll to last row via `tableView.scrollRowToVisible(entries.count - 1)`
- Detect manual scroll: implement `NSScrollView` notification observer for `NSScrollView.didLiveScrollNotification` -- set `isUserScrolling = true` when user scrolls up (content not at bottom)
- Reset `isUserScrolling = false` when user scrolls back to bottom (check if `clipView.bounds.origin.y >= documentView.frame.height - clipView.bounds.height - 20` with a 20pt tolerance)

**Click-to-copy (CAPT-03):**

- Implement `NSTableViewDelegate` method -- on row click (override `tableView(_:shouldSelectRow:)` or use target-action on table), copy that entry's `.text` to `NSPasteboard.general`
- Show brief visual feedback: flash the row background or log to logger

**Timestamp formatting:**

- Parse `CaptionEntry.timestamp` (ISO 8601 string) to Date using `ISO8601DateFormatter`
- Format to "HH:MM" using `DateFormatter` with `dateFormat = "HH:mm"`

**Also update CaptionHistory.swift:**

- Add a `public var onChange: (() -> Void)?` callback property (called after each `record()`) so CaptionHistoryPanel can auto-refresh when new captions arrive
- In `record()`, after `lock.unlock()`, call `onChange?()` on the main thread via `DispatchQueue.main.async`
  </action>
  <verify>
  <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
  - grep -q "class CaptionHistoryPanel" plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistoryPanel.swift
  - grep -q "NSPanel" plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistoryPanel.swift
  - grep -q "HH:mm" plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistoryPanel.swift
  - grep -q "isUserScrolling" plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistoryPanel.swift
  - grep -q "NSPasteboard" plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistoryPanel.swift
  - grep -q "onChange" plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistory.swift
  - swift build succeeds with zero errors
    </acceptance_criteria>
    <done>CaptionHistoryPanel.swift exists with scrollable NSTableView showing HH:MM timestamps and caption text. Click-to-copy implemented. Auto-scroll with manual override. CaptionHistory has onChange callback. swift build passes.</done>
    </task>

<task type="auto">
  <name>Task 2: HTTP panel endpoints + CompanionApp wiring</name>
  <files>
    plugins/claude-tts-companion/Sources/CompanionCore/HTTPControlServer.swift
    plugins/claude-tts-companion/Sources/CompanionCore/CompanionApp.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/CompanionCore/HTTPControlServer.swift
    plugins/claude-tts-companion/Sources/CompanionCore/CompanionApp.swift
    plugins/claude-tts-companion/Sources/CompanionCore/CaptionHistoryPanel.swift
  </read_first>
  <action>
**HTTPControlServer updates:**

Add a `private let captionHistoryPanel: CaptionHistoryPanel` property.

Update `init()` to accept `captionHistoryPanel: CaptionHistoryPanel` parameter (add after existing `captionHistory` param).

Add two new endpoints in `start()`:

```swift
// CAPT-04: Show caption history panel
await server.appendRoute("POST /captions/panel/show") { [self] _ in
    await MainActor.run { captionHistoryPanel.show() }
    return jsonResponse(OkResponse(ok: true))
}

// CAPT-04: Hide caption history panel
await server.appendRoute("POST /captions/panel/hide") { [self] _ in
    await MainActor.run { captionHistoryPanel.hide() }
    return jsonResponse(OkResponse(ok: true))
}
```

**CompanionApp updates:**

Add `private let captionHistoryPanel: CaptionHistoryPanel` property.

In `@MainActor init()`:

- Create `captionHistoryPanel = CaptionHistoryPanel(captionHistory: captionHistory)` (AFTER captionHistory creation, BEFORE httpServer creation)
- Pass `captionHistoryPanel` to `HTTPControlServer` init

Wire onChange callback in `start()`:

- Set `captionHistory.onChange = { [weak self] in self?.captionHistoryPanel.refresh() }` so the panel auto-refreshes when new captions are recorded

In `shutdown()`:

- Add `captionHistoryPanel.hide()` alongside `subtitlePanel.hide()`
  </action>
  <verify>
  <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
  - grep -q "captionHistoryPanel" plugins/claude-tts-companion/Sources/CompanionCore/HTTPControlServer.swift
  - grep -q "/captions/panel/show" plugins/claude-tts-companion/Sources/CompanionCore/HTTPControlServer.swift
  - grep -q "/captions/panel/hide" plugins/claude-tts-companion/Sources/CompanionCore/HTTPControlServer.swift
  - grep -q "captionHistoryPanel" plugins/claude-tts-companion/Sources/CompanionCore/CompanionApp.swift
  - grep -q "CaptionHistoryPanel" plugins/claude-tts-companion/Sources/CompanionCore/CompanionApp.swift
  - grep -q "onChange" plugins/claude-tts-companion/Sources/CompanionCore/CompanionApp.swift
  - swift build succeeds with zero errors
    </acceptance_criteria>
    <done>HTTP endpoints POST /captions/panel/show and POST /captions/panel/hide work. CompanionApp creates CaptionHistoryPanel and wires onChange for auto-refresh. swift build passes.</done>
    </task>

</tasks>

<verification>
1. `cd plugins/claude-tts-companion && swift build` -- compiles with zero errors
2. `grep -r "CaptionHistoryPanel" plugins/claude-tts-companion/Sources/` -- exists in 3 files (own file, HTTPControlServer, CompanionApp)
3. `grep -c "/captions/panel" plugins/claude-tts-companion/Sources/CompanionCore/HTTPControlServer.swift` -- returns 2 (show + hide)
</verification>

<success_criteria>

- CaptionHistoryPanel.swift exists as @MainActor NSPanel subclass with NSTableView
- Panel shows captions with HH:MM timestamps, supports click-to-copy
- Auto-scroll with manual override implemented
- HTTP API has /captions/panel/show and /captions/panel/hide endpoints
- CompanionApp creates panel and wires onChange for live refresh
- swift build passes with zero errors
  </success_criteria>

<output>
After completion, create `.planning/phases/23-caption-history-panel/23-01-SUMMARY.md`
</output>
