---
phase: 10-deployment-extras
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/CaptionHistory.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/ThinkingWatcher.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/HTTPControlServer.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
autonomous: true
requirements: [EXT-01, EXT-02, EXT-04]

must_haves:
  truths:
    - "User can retrieve scrollable caption history via HTTP endpoint"
    - "User can copy any subtitle entry to clipboard via HTTP endpoint"
    - "Thinking watcher detects extended thinking blocks and summarizes them via MiniMax"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/CaptionHistory.swift"
      provides: "Thread-safe ring buffer of recent subtitle entries with clipboard copy"
      exports: ["CaptionHistory", "CaptionEntry"]
      min_lines: 40
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/ThinkingWatcher.swift"
      provides: "Monitors transcript JSONL for thinking blocks, summarizes via MiniMax"
      exports: ["ThinkingWatcher"]
      min_lines: 50
  key_links:
    - from: "SubtitlePanel.show(text:)"
      to: "CaptionHistory.append"
      via: "Panel records every shown subtitle into history"
      pattern: "captionHistory.*append"
    - from: "HTTPControlServer"
      to: "CaptionHistory"
      via: "GET /captions and POST /captions/copy endpoints"
      pattern: "/captions"
    - from: "ThinkingWatcher"
      to: "SummaryEngine or MiniMaxClient"
      via: "Summarizes thinking text via MiniMax API"
      pattern: "client.query"
---

<objective>
Add caption history tracking, clipboard copy support, and a thinking watcher that summarizes Claude's extended thinking blocks.

Purpose: Polish features that make the companion more useful -- users can review what was said and get summaries of Claude's thinking process.
Output: CaptionHistory.swift, ThinkingWatcher.swift, updated HTTPControlServer + SubtitlePanel + main.swift.
</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/claude-tts-companion/SubtitlePanel.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/HTTPControlServer.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/FileWatcher.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift

<interfaces>
From SubtitlePanel.swift:
```swift
@MainActor
final class SubtitlePanel: NSPanel {
    func show(text: String)
    func hide()
    func updateAttributedText(_ text: NSAttributedString)
    func highlightWord(at index: Int, in words: [String])
    func showUtterance(_ text: String, wordTimings: [TimeInterval])
}
```

From HTTPControlServer.swift:

```swift
final class HTTPControlServer: @unchecked Sendable {
    init(settingsStore: SettingsStore, subtitlePanel: SubtitlePanel, ttsEngine: TTSEngine)
    func start() async throws
    // Uses FlyingFox HTTPServer, appendRoute pattern
    // jsonResponse<T: Encodable>(_ value: T, status:) -> HTTPResponse
    // errorResponse(_ message: String, status:) -> HTTPResponse
}
```

From MiniMaxClient.swift (shared circuit breaker):

```swift
final class MiniMaxClient: @unchecked Sendable {
    func query(prompt: String, systemPrompt: String, maxTokens: Int) async throws -> MiniMaxResult
}
struct MiniMaxResult { let text: String; let durationMs: Int }
```

From FileWatcher.swift:

```swift
final class JSONLTailer {
    init(filePath: String, onLine: @escaping (String) -> Void)
    func start()
    func stop()
}
```

From Config.swift:

```swift
static let transcriptBaseDir: String  // ~/.claude/projects
static let notificationDir: String    // ~/.claude/notifications
```

Thread-safety pattern used throughout codebase: NSLock + @unchecked Sendable
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: CaptionHistory + ThinkingWatcher + SubtitlePanel integration</name>
  <files>
    plugins/claude-tts-companion/Sources/claude-tts-companion/CaptionHistory.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/ThinkingWatcher.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift
  </files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/FileWatcher.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/TranscriptParser.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
  </read_first>
  <action>
    1. Create CaptionHistory.swift:
       - `struct CaptionEntry: Codable, Sendable` with fields: `id` (Int, auto-increment), `text` (String), `timestamp` (Date as ISO 8601 string)
       - `final class CaptionHistory: @unchecked Sendable` with NSLock for thread safety
       - Private `entries: [CaptionEntry]` array, `maxEntries: Int` (default 100), `nextId: Int`
       - `func append(_ text: String)` — adds entry with current timestamp, trims oldest if over maxEntries
       - `func getAll() -> [CaptionEntry]` — returns copy of all entries (newest last)
       - `func getRecent(_ count: Int) -> [CaptionEntry]` — returns last N entries
       - `func copyToClipboard(id: Int) -> Bool` — finds entry by id, copies text to `NSPasteboard.general`, returns success
       - `func copyToClipboard(text: String)` — direct copy to pasteboard (for latest subtitle)
       - Import AppKit for NSPasteboard

    2. Create ThinkingWatcher.swift:
       - `final class ThinkingWatcher: @unchecked Sendable` with NSLock
       - Init takes `client: MiniMaxClient`, `subtitlePanel: SubtitlePanel` (for displaying thinking summaries)
       - Private state: `watchedFiles: [String: JSONLTailer]`, `thinkingBuffer: String`, `isInThinkingBlock: Bool`
       - `func processTranscriptLine(_ line: String)` — parses JSON line, detects thinking blocks:
         - Claude Code JSONL uses `type: "assistant"` entries with `message.content` array
         - Thinking blocks appear as content items with `type: "thinking"` and `thinking` text field
         - When a thinking block is detected and text is > 200 chars, summarize it
       - `func summarizeThinking(_ thinkingText: String) async` — calls MiniMaxClient with a prompt like:
         "Summarize this internal reasoning in 1-2 sentences for spoken narration. Focus on the key insight or decision made. No code, no file paths, natural language only."
         System prompt: "You condense technical reasoning into brief spoken summaries."
         Max tokens: 256
       - On successful summary, dispatch to main thread: `subtitlePanel.show(text: summary)` with auto-hide after 8 seconds
       - Log thinking summary via swift-log
       - Use Logging import

    3. Update SubtitlePanel.swift:
       - Add an optional `captionHistory: CaptionHistory?` property (set via a public setter method `func setCaptionHistory(_ history: CaptionHistory)`)
       - In the `show(text:)` method, after setting textField.stringValue, call `captionHistory?.append(text)` to record every displayed subtitle
       - Do NOT record in `updateAttributedText` or `highlightWord` — those are karaoke sub-updates of the same utterance. Only record in `show(text:)` and at the START of `showUtterance` (the full sentence, not each word highlight).

  </action>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - CaptionHistory stores entries with auto-increment IDs and timestamps
    - CaptionHistory.copyToClipboard uses NSPasteboard.general
    - ThinkingWatcher detects thinking blocks in JSONL transcript lines
    - ThinkingWatcher summarizes thinking text > 200 chars via MiniMaxClient
    - SubtitlePanel.show(text:) appends to CaptionHistory
    - Project compiles with zero errors
  </acceptance_criteria>
  <done>CaptionHistory and ThinkingWatcher classes exist, SubtitlePanel records captions, project compiles</done>
</task>

<task type="auto">
  <name>Task 2: HTTP endpoints + main.swift wiring</name>
  <files>
    plugins/claude-tts-companion/Sources/claude-tts-companion/HTTPControlServer.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
  </files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/HTTPControlServer.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/CaptionHistory.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/ThinkingWatcher.swift
  </read_first>
  <action>
    1. Update HTTPControlServer.swift:
       - Add `captionHistory: CaptionHistory` parameter to init (store as property)
       - Add `GET /captions` route (EXT-01):
         - Optional query param `?count=N` (default 50, max 100)
         - Returns JSON array of CaptionEntry objects `{ entries: [...], total: N }`
         - Parse count from request URL query string
       - Add `POST /captions/copy` route (EXT-02):
         - Request body: `{ "id": 42 }` or `{ "text": "direct text" }`
         - If `id` provided: calls `captionHistory.copyToClipboard(id:)`, returns `{ ok: true }` or `{ error: "Not found" }`
         - If `text` provided: calls `captionHistory.copyToClipboard(text:)`, returns `{ ok: true }`
       - Add private request/response structs:
         - `CaptionsResponse: Codable` with `entries: [CaptionEntry]` and `total: Int`
         - `CopyRequest: Codable` with `id: Int?` and `text: String?`

    2. Update main.swift:
       - Create `CaptionHistory()` instance after SettingsStore creation
       - Call `subtitlePanel.setCaptionHistory(captionHistory)` to wire history tracking
       - Pass `captionHistory` to HTTPControlServer init
       - Create `ThinkingWatcher(client: miniMaxClient, subtitlePanel: subtitlePanel)` after autoContinue creation
       - In the NotificationWatcher callback, after evaluating auto-continue, if transcriptPath exists:
         Process the last few lines of the transcript through `thinkingWatcher.processTranscriptLine()` to detect thinking blocks
         (Read last 50 lines of the transcript file and feed each through the watcher)
       - Add `nonisolated(unsafe) var keepCaptionHistory: CaptionHistory? = captionHistory` to prevent ARC deallocation
       - Add `nonisolated(unsafe) var keepThinkingWatcher: ThinkingWatcher? = thinkingWatcher` to prevent ARC deallocation

  </action>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - GET /captions returns JSON with entries array and total count
    - POST /captions/copy accepts id or text, copies to clipboard
    - ThinkingWatcher is created and wired in main.swift
    - CaptionHistory is passed to both SubtitlePanel and HTTPControlServer
    - All ARC-preventing global references are in place
    - Project compiles with zero errors
  </acceptance_criteria>
  <done>HTTP endpoints for caption history and clipboard work; ThinkingWatcher wired into notification flow; full build succeeds</done>
</task>

</tasks>

<verification>
1. `cd plugins/claude-tts-companion && swift build 2>&1 | tail -5` — compiles with zero errors
2. Grep for all EXT requirements:
   - `grep -q "GET /captions" plugins/claude-tts-companion/Sources/claude-tts-companion/HTTPControlServer.swift` (EXT-01)
   - `grep -q "NSPasteboard" plugins/claude-tts-companion/Sources/claude-tts-companion/CaptionHistory.swift` (EXT-02)
   - `grep -q "ThinkingWatcher" plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift` (EXT-04)
</verification>

<success_criteria>

- EXT-01: GET /captions returns scrollable caption history (last N entries with count param)
- EXT-02: POST /captions/copy copies subtitle text to macOS clipboard via NSPasteboard
- EXT-04: ThinkingWatcher detects thinking blocks in JSONL and summarizes via MiniMax
  </success_criteria>

<output>
After completion, create `.planning/phases/10-deployment-extras/10-02-SUMMARY.md`
</output>
