diff --git a/AGENTS.md b/AGENTS.md
index b9a67ce17..af9168b79 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -83,6 +83,7 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working
- Keep UI and analysis engine decoupled through shared contracts.
- Prefer minimal, test-first changes for production code.
- Prefer practical, friendly, rehearsal-first wording over academic or authority-heavy language.
+- Name tonight's first lyric cue with the part, words, section, and start time so the singer's next action is obvious.
- Do not reduce the product to a chord analyzer when form, timing, player coordination, playable ranges, simplification, and setup cues are the real rehearsal blockers.
- Do not frame usability as a reason to accept weak analysis quality; BandScope should aim for both easy use and high accuracy.
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index ca0df5ac4..3aeb7c772 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -6,6 +6,7 @@ Last updated: 2026-03-11
- Product identity, UX tone, copy rules, and prioritization tie-breakers live in `docs/brand-story.md`.
- Future PRDs, TRDs, onboarding copy, empty states, error messages, and marketing copy should use that document as the single brand source of truth.
+- Workspace and player copy for tonight's first lyric cue must name the part, words, section, and start time so the singer's next action is obvious.
## Security source
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b6f7e784..0e57e319f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Added
+- Name tonight's first lyric cue on the workspace and player with part, lyric, section, and start time; the workspace action arms the cue, opens the matching map section, and honors the user's reduced-motion preference for that navigation, while the player exposes a Hear action only when its owning playback surface supplies a seek callback.
- Name tonight's first playable range on the ready rehearsal map and tell the player to check that span on their instrument before the section.
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.
@@ -74,4 +75,5 @@
- `ChordsFeature` (코드 분석) 화면에서 각 파트(Role)의 `transpositionPlan`(이조/조옮김 계획)을 표시하는 기능을 추가했습니다.
- `RangesFeature` (음역대 분석) 화면에서 겹침 경고(Overlap warning) 외에 해당 파트의 채보(Transcription) 가능 노드 수를 요약하여 보여주는 기능을 추가했습니다.
-- 신규 UI 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
\ No newline at end of file
+- 신규 UI 요소에 대한 100% 테스트 커버리지를 보장하는 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
+- 신규 UI 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
diff --git a/CLAUDE.md b/CLAUDE.md
index b5a34c1fa..60e676a65 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -51,7 +51,7 @@ BandScope is a local-first desktop app for rehearsal prep: it turns a song into
Three layers, decoupled through shared contracts:
-- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). The ready workspace names tonight's first playable range and the next instrument check. `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri.
+- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). Workspace and player name tonight's first lyric cue with its part, lyric, section, and start time; the workspace action arms the cue and opens the mapped section, while the player renders the Hear action only when its owning playback surface supplies a seek callback. The ready workspace also names tonight's first playable range and the next instrument check. `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri.
- `apps/desktop/src-tauri/src/main.rs` — the Rust orchestration boundary. Tauri commands (`start_analysis_job`, `get_analysis_job_status`, `select_local_audio_source`, `import_youtube_url`) validate untrusted input (project IDs, file paths, URLs) and spawn the Python engine as a subprocess. There is no loopback HTTP listener and no network path for local analysis.
- `services/analysis-engine` — Python package `bandscope_analysis` (librosa/numpy). Entry point `cli.py` reads a JSON job request on stdin and prints a structured job-status JSON envelope on stdout (`--progress-jsonl` streams progress lines). `api.py` orchestrates the pipeline across the `separation`, `sections`, `roles`, `chords`, `ranges`, `temporal`, `transcription`, and `youtube` modules.
@@ -72,4 +72,4 @@ Supporting packages:
- Documents under `docs/plans/` must include `Security Notes`; `scripts/checks/verify_security_notes.py` enforces this mechanically.
- Lockfiles (`package-lock.json`, `uv.lock`, `Cargo.lock`) are committed and must stay in sync; GitHub Actions are SHA-pinned. Adding a direct dependency requires the admission rationale defined in `AGENTS.md` and `docs/security/dependency-policy.md`.
- CI beyond quickcheck: `gate / ci / rust-check` (Tauri cargo check on macOS) and `build-baseline` Windows/macOS amd64+arm64 native builds are merge gates, alongside CodeQL, dependency-review, sbom, bandit, trivy, secret-scan, and security-audit workflows. Do not weaken or skip them.
-- Version metadata lives in `VERSION`, the root `package.json`, and `CHANGELOG.md`; release flow is tag-driven (see `docs/operations/deploy-runbook.md`).
+- Version metadata lives in `VERSION`, the root `package.json`, and `CHANGELOG.md`; release flow is tag-driven (see `docs/operations/deploy-runbook.md`).
\ No newline at end of file
diff --git a/apps/desktop/src/features/player/index.test.tsx b/apps/desktop/src/features/player/index.test.tsx
new file mode 100644
index 000000000..1410062d6
--- /dev/null
+++ b/apps/desktop/src/features/player/index.test.tsx
@@ -0,0 +1,44 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { createDemoRehearsalSong } from "@bandscope/shared-types";
+import { describe, expect, it, vi } from "vitest";
+import { PlayerFeature } from "./index";
+
+describe("PlayerFeature", () => {
+ it("asks the room to analyze first when no song is loaded", () => {
+ render();
+ expect(
+ screen.getByText("Analyze tonight's song first, then hear the first lyric cue from this player.")
+ ).toBeTruthy();
+ });
+
+ it("keeps the lyric hear action unavailable without a player playback callback", () => {
+ render();
+
+ expect(
+ screen.queryByRole("button", {
+ name: "Hear Lead Vocal enter on “city lights” in the verse at 0:10"
+ })
+ ).toBeNull();
+ expect(screen.getByText("Lead Vocal enters the verse on “city lights” at 0:10.")).toBeTruthy();
+ });
+
+ it("delegates the lyric hear action to the owning player callback", () => {
+ const onPlayFromSeconds = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "Hear Lead Vocal enter on “city lights” in the verse at 0:10"
+ })
+ );
+
+ expect(onPlayFromSeconds).toHaveBeenCalledTimes(1);
+ expect(onPlayFromSeconds).toHaveBeenCalledWith(10);
+ });
+});
\ No newline at end of file
diff --git a/apps/desktop/src/features/player/index.tsx b/apps/desktop/src/features/player/index.tsx
index 37bc12f71..1f2e541f2 100644
--- a/apps/desktop/src/features/player/index.tsx
+++ b/apps/desktop/src/features/player/index.tsx
@@ -1,14 +1,22 @@
import type { RehearsalSong } from "@bandscope/shared-types";
+import { FirstLyricCueCallout } from "../workspace/FirstLyricCueCallout";
+import { createTranslator, detectPreferredLocale } from "../../i18n";
-/** Documented. */
-export function PlayerFeature(props: { title: string; song?: RehearsalSong | null }) {
- const { title, song } = props;
+type PlayerFeatureProps = {
+ title: string;
+ song?: RehearsalSong | null;
+ onPlayFromSeconds?: (startSeconds: number) => void;
+};
+
+/** Player surface that names tonight's first lyric cue and delegates playback to the owning player. */
+export function PlayerFeature({ title, song, onPlayFromSeconds }: PlayerFeatureProps) {
+ const t = createTranslator(detectPreferredLocale());
if (!song) {
return (
{title}
-
No song loaded. Start an analysis to use the player.