---
phase: 02-subtitle-overlay
plan: 02
type: execute
wave: 2
depends_on: ["02-01"]
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
autonomous: false
requirements: [SUB-03, SUB-04, SUB-05]

must_haves:
  truths:
    - "Current word highlights in gold bold as words advance"
    - "Past words dim to silver-grey, future words remain white"
    - "NSAttributedString updates complete in under 1ms per word"
    - "Demo mode cycles through sample text with 200ms per word timing"
    - "Panel clears 2 seconds after the final word"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift"
      provides: "Karaoke highlighting engine and demo mode"
      contains: "func demo()"
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift"
      provides: "Panel creation and demo activation"
      contains: "SubtitlePanel"
  key_links:
    - from: "SubtitlePanel.swift"
      to: "SubtitleStyle.swift"
      via: "uses currentWordColor, pastWordColor, futureWordColor for attributed string styling"
      pattern: "SubtitleStyle\\.currentWordColor"
    - from: "main.swift"
      to: "SubtitlePanel.swift"
      via: "creates panel instance and calls demo()"
      pattern: "SubtitlePanel\\("
---

<objective>
Add word-level karaoke highlighting to the SubtitlePanel and wire it into main.swift with a demo mode.

Purpose: Deliver the core karaoke experience — gold highlighting advances word-by-word while past words dim to silver-grey. The demo mode proves the subtitle system works end-to-end without needing TTS (Phase 3). This is the user-visible payoff of Phase 2.

Output: Karaoke engine in SubtitlePanel.swift + demo() method + main.swift wiring + visual checkpoint
</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
@.planning/phases/02-subtitle-overlay/02-CONTEXT.md
@.planning/phases/02-subtitle-overlay/02-01-SUMMARY.md

<interfaces>
<!-- From SubtitleStyle.swift (created in Plan 01): -->
```swift
enum SubtitleStyle {
    static let currentWordColor: NSColor   // #FFD700 gold
    static let pastWordColor: NSColor      // #A0A0A0 silver-grey
    static let futureWordColor: NSColor    // #FFFFFF white
    static let backgroundColor: NSColor    // #000000 at 0.3 alpha
    static let cornerRadius: CGFloat       // 10
    static let horizontalPadding: CGFloat  // 16
    static let verticalPadding: CGFloat    // 12
    static let bottomOffset: CGFloat       // 80
    static let widthRatio: CGFloat         // 0.7
    static let maxLines: Int               // 2
    static let lingerDuration: TimeInterval // 2.0
    static func font(size: CGFloat, weight: NSFont.Weight) -> NSFont
    enum FontSize: CGFloat { case small = 22, medium = 28, large = 36 }
}
```

<!-- From SubtitlePanel.swift (created in Plan 01): -->

```swift
@MainActor class SubtitlePanel: NSPanel {
    func show(text: String)
    func hide()
    func updateAttributedText(_ text: NSAttributedString)
    func positionOnScreen()
}
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement karaoke highlighting engine and demo mode in SubtitlePanel</name>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift</files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift (current state from Plan 01)
    - plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitleStyle.swift (color/font constants)
    - .planning/phases/02-subtitle-overlay/02-CONTEXT.md (karaoke behavior spec)
  </read_first>
  <action>
Add karaoke highlighting logic to the existing SubtitlePanel class.

**Word-level highlighting method:**
Add `func highlightWord(at index: Int, in words: [String])` that builds an NSAttributedString:

- Words before `index`: styled with `SubtitleStyle.pastWordColor` + regular weight font via `SubtitleStyle.font(size: 28, weight: .regular)`
- Word at `index`: styled with `SubtitleStyle.currentWordColor` + bold weight font via `SubtitleStyle.font(size: 28, weight: .bold)`
- Words after `index`: styled with `SubtitleStyle.futureWordColor` + regular weight font via `SubtitleStyle.font(size: 28, weight: .regular)`
- Join words with space using NSMutableAttributedString, appending each word with its attributes
- Set paragraph style: `.alignment = .center`, `.lineBreakMode = .byWordWrapping`
- Call `updateAttributedText(result)` at the end

**Utterance display method:**
Add `func showUtterance(_ text: String, wordTimings: [TimeInterval])` that:

- Splits `text` into words via `text.split(separator: " ").map(String.init)`
- Stores `words` and `wordTimings` as instance properties
- Shows the panel via `show(text: text)`
- Starts a `DispatchQueue.main.asyncAfter` chain that calls `highlightWord(at: i, in: words)` for each word at cumulative timing offsets
- After the last word, schedules `hide()` after `SubtitleStyle.lingerDuration` (2.0 seconds)
- Store a cancellation token (a `DispatchWorkItem?` property named `lingerWorkItem`) so that a new utterance cancels any pending linger/hide

**Demo method:**
Add `func demo()` that calls `showUtterance` with sample sentences and hardcoded 200ms per word timing:

```swift
func demo() {
    let sentences = [
        "Welcome to claude TTS companion, your real-time subtitle overlay",
        "This is a demo of the karaoke highlighting system",
        "Words light up in gold as they are spoken aloud"
    ]
    var delay: TimeInterval = 0.5  // initial delay
    for sentence in sentences {
        let words = sentence.split(separator: " ").map(String.init)
        let timings = Array(repeating: 0.2, count: words.count)  // 200ms per word
        let sentenceDelay = delay
        DispatchQueue.main.asyncAfter(deadline: .now() + sentenceDelay) { [weak self] in
            self?.showUtterance(sentence, wordTimings: timings)
        }
        // Next sentence starts after this one finishes + linger
        delay += Double(words.count) * 0.2 + SubtitleStyle.lingerDuration + 0.5
    }
}
```

**Performance note for SUB-05:** The NSAttributedString construction in `highlightWord` must be trivial — no layout passes, no font loading in the hot path. Font references should be cached as static lets in SubtitleStyle (already done). The method does: create NSMutableAttributedString, append N word+space pairs, set on textField. This is well under 1ms for typical sentence lengths.
</action>
<verify>
<automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
</verify>
<acceptance*criteria> - SubtitlePanel.swift contains `func highlightWord(at index: Int, in words: [String])` - SubtitlePanel.swift contains `func showUtterance(* text: String, wordTimings: [TimeInterval])`    - SubtitlePanel.swift contains`func demo()`    - Contains`SubtitleStyle.currentWordColor`usage for the active word
    - Contains`SubtitleStyle.pastWordColor`usage for past words
    - Contains`SubtitleStyle.futureWordColor`usage for future words
    - Contains`lingerWorkItem`property for cancellation
    - Contains`0.2`(200ms) timing in demo method
    - Contains`SubtitleStyle.lingerDuration`reference for post-utterance clear
    -`swift build` succeeds
</acceptance_criteria>
<done>Karaoke highlighting engine builds NSAttributedString with gold/silver-grey/white word coloring, demo() cycles through 3 sample sentences at 200ms/word, panel clears after 2s linger</done>
</task>

<task type="auto">
  <name>Task 2: Wire SubtitlePanel into main.swift and launch demo on startup</name>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift</files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift (current entry point)
    - plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift (karaoke panel from Task 1)
  </read_first>
  <action>
Modify main.swift to create the SubtitlePanel and start demo mode on launch.

**Add after `app.setActivationPolicy(.accessory)` and before the SIGTERM handler:**

```swift
// Create subtitle overlay panel
let subtitlePanel = SubtitlePanel()
subtitlePanel.positionOnScreen()
```

**Add after `logger.info("Starting \(Config.appName)")` and before `app.run()`:**

```swift
// Launch demo mode (temporary — replaced by TTS-driven highlighting in Phase 3)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    subtitlePanel.demo()
}
```

**Add subtitlePanel to the SIGTERM cleanup:**
In the SIGTERM handler, before `app.stop(nil)`, add:

```swift
subtitlePanel.hide()
```

Keep all existing code (sherpa-onnx version check, logging, SIGTERM, keepAlive). The panel creation slots in naturally between app setup and run loop entry.

Do NOT add `subtitlePanel` to the `keepAlive` pattern — it is retained by the run loop as a visible window. But if the panel is not yet shown, ensure it is retained. Safest approach: make it `nonisolated(unsafe)` like keepAlive, or store in a `let` at module scope.

**Swift 6 concurrency:** Since main.swift runs on the main actor implicitly (it is the main file of an executable target), and SubtitlePanel is `@MainActor`, this should compile without sendability issues. If Swift 6 strict concurrency flags it, wrap the panel creation in `MainActor.assumeIsolated { }` — but try without first.
</action>
<verify>
<automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria> - main.swift contains `SubtitlePanel()` constructor call - main.swift contains `positionOnScreen()` call - main.swift contains `.demo()` call - main.swift contains `subtitlePanel.hide()` in SIGTERM handler - main.swift still contains `SherpaOnnxGetVersionStr` (existing C interop check preserved) - main.swift still contains `app.run()` as last executable statement before exit log - `swift build` succeeds
</acceptance_criteria>
<done>main.swift creates SubtitlePanel, positions it, launches demo mode on startup, and hides panel on SIGTERM. Build succeeds.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 3: Visual verification of karaoke subtitle overlay</name>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift</files>
  <action>
Human visually verifies the karaoke subtitle overlay by running the binary and observing the demo mode. No code changes in this task.
  </action>
  <what-built>
Floating subtitle panel with word-level karaoke highlighting. Three demo sentences cycle with gold highlighting advancing at 200ms/word, past words dimming to silver-grey, future words in white. Panel positioned bottom-center of screen, 80px from bottom, 70% width, dark 30% opacity background with rounded corners.
  </what-built>
  <how-to-verify>
1. Run the binary:
   ```bash
   cd plugins/claude-tts-companion && swift run claude-tts-companion
   ```
2. Within 0.5 seconds, a dark semi-transparent panel should appear at bottom center of your screen
3. Verify karaoke behavior:
   - Words highlight one at a time in GOLD (#FFD700) with bold weight
   - Previously highlighted words dim to SILVER-GREY (#A0A0A0)
   - Upcoming words are WHITE
   - Each word highlights for ~200ms
4. Verify panel properties:
   - Panel is at bottom center, ~80px from bottom edge
   - Panel width is approximately 70% of screen width
   - Background is dark with ~30% opacity (you can see through it)
   - Corners are rounded (~10px radius)
   - Click through the panel — mouse events should pass to windows behind it
   - The panel should NOT appear in the dock or app switcher
5. Verify word-wrap: The first sentence "Welcome to claude TTS companion, your real-time subtitle overlay" should wrap to 2 lines
6. After 3 sentences complete, the panel should clear (hide) after a 2-second linger
7. Press Ctrl+C or send SIGTERM to stop: `kill $(pgrep claude-tts-companion)`
8. If you can share your screen (Zoom, FaceTime, etc.), verify the panel does NOT appear in the shared view
  </how-to-verify>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <done>Human confirms: karaoke highlighting looks correct, panel positioning is right, click-through works, no dock icon appears</done>
  <resume-signal>Type "approved" if the overlay looks correct, or describe any visual issues</resume-signal>
</task>

</tasks>

<verification>
1. `cd plugins/claude-tts-companion && swift build` compiles with zero errors
2. Running the binary shows a floating panel with karaoke demo
3. Gold/silver-grey/white word coloring transitions smoothly
4. Panel is click-through, non-focus-stealing, visible on all Spaces
5. Panel is invisible to screen sharing (if tested)
6. Panel clears after final word + 2 second linger
</verification>

<success_criteria>

- Karaoke highlighting advances word-by-word with correct gold/silver-grey/white coloring
- Demo mode runs 3 sentences at 200ms/word
- Panel lingers 2 seconds after final word, then clears
- Human confirms visual appearance matches expectations
- NSAttributedString updates are perceptually instant (no visible lag between words)
  </success_criteria>

<output>
After completion, create `.planning/phases/02-subtitle-overlay/02-02-SUMMARY.md`
</output>
