diff --git a/AGENTS.md b/AGENTS.md
index b9a67ce17..76872c82a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,7 +1,7 @@
# AGENTS.md
## Project overview
-- BandScope is a local-first desktop app for rehearsal prep: a practical song view with likely harmony by section and by instrument or vocal role, form and groove cues, stems, playable ranges, simplification guidance, transposition or setup cues, part-overlap cues, visible confidence, and rehearsal priorities.
+- BandScope is a local-first desktop app for rehearsal prep: a practical song view with likely harmony by section and by instrument or vocal role, form and groove cues, stems, playable ranges, simplification guidance, transposition or setup cues, part-overlap cues, visible confidence, and rehearsal priorities. The ready workspace names tonight's first playable range and offers a next action that opens that part on the map.
- Authoritative delivery rules live in `ARCHITECTURE.md`, `docs/plans/`, and the root verification scripts.
- Brand, tone, UX copy, and prioritization rules live in `docs/brand-story.md` and must be applied to PRDs, TRDs, UI copy, onboarding, empty states, and error messages.
- App security rules live in `docs/security/app-security.md` and must be applied to file handling, URL intake, subprocesses, IPC, WebView usage, model loading, updates, logging, cache handling, and export behavior.
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index ca0df5ac4..da96a1407 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -82,7 +82,7 @@ Last updated: 2026-03-11
- likely harmony by section and by role
- section roadmap with entries, dropouts, pickups, stops, tags, and handoffs
- groove and timing cues relevant to locking the band together
- - playable ranges and density or overlap warnings, with the ready workspace naming tonight's first span and the next instrument check
+ - playable ranges and density or overlap warnings, with the ready workspace naming tonight's first span, offering a control that opens that part on the map, and naming the next instrument check
- simplification, transposition, capo, tuning, or setup cues where applicable
- role-specific rehearsal priorities and confidence flags
- cue-sheet or chart-style exports that summarize the analysis in rehearsal-friendly form
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b6f7e784..262c446a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
### Added
-- 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.
+- Name tonight's first playable range on the ready rehearsal map, offer Open {role} on the map when the full-band view is showing that part, 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 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.
diff --git a/CLAUDE.md b/CLAUDE.md
index b5a34c1fa..12d91ee9c 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). The ready workspace names tonight's first playable range, offers a control that opens that part on the map, and tells the player to check that span on their instrument. `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.
diff --git a/apps/desktop/src/features/workspace/Workspace.test.tsx b/apps/desktop/src/features/workspace/Workspace.test.tsx
index 7837bf80e..7c609d9bc 100644
--- a/apps/desktop/src/features/workspace/Workspace.test.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.test.tsx
@@ -151,6 +151,7 @@ describe("Workspace", () => {
expect(callout).toHaveTextContent(
"Bass Guitar sits C#2–E3 in verse. Hear that clash on your instrument before the verse."
);
+ expect(screen.getByRole("button", { name: "Open Bass Guitar on the map" })).toBeTruthy();
});
it("asks for an ear check when the selected part has no named span", () => {
@@ -167,6 +168,7 @@ describe("Workspace", () => {
expect(screen.getByTestId("first-range-squeeze")).toHaveTextContent(
"Tonight's first range still needs an ear check. Confirm the high and low notes on the selected part before the first section."
);
+ expect(screen.queryByRole("button", { name: /Open .+ on the map/ })).toBeNull();
});
it("limits the range callout to the selected role", () => {
@@ -179,6 +181,7 @@ describe("Workspace", () => {
expect(screen.getByTestId("first-range-squeeze")).toHaveTextContent(
"Lead Vocal sits G#3–C#5 in verse. Hear that clash on your instrument before the verse."
);
+ expect(screen.queryByRole("button", { name: "Open Lead Vocal on the map" })).toBeNull();
});
it("asks the player to check a named span when no clash is present", () => {
@@ -196,6 +199,28 @@ describe("Workspace", () => {
);
});
+ it("opens the named first-range part from the range card", () => {
+ setNavigatorLanguage("en-US");
+ const song = createDemoRehearsalSong();
+
+ render();
+
+ fireEvent.click(screen.getByRole("button", { name: "Open Bass Guitar on the map" }));
+
+ expect(screen.getByText("Stem Player")).toBeTruthy();
+ expect(screen.getByText(/The bass holds the vi center/i)).toBeTruthy();
+ expect(screen.queryByRole("button", { name: "Open Bass Guitar on the map" })).toBeNull();
+ });
+
+ it("localizes the first-range open-part action", () => {
+ setNavigatorLanguage("ko-KR");
+ const song = createDemoRehearsalSong();
+
+ render();
+
+ expect(screen.getByRole("button", { name: "지도에서 Bass Guitar 열기" })).toBeTruthy();
+ });
+
it("falls back from blank planning copy and tolerates partial collaboration payloads", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx
index d44e20777..6c29d0082 100644
--- a/apps/desktop/src/features/workspace/Workspace.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.tsx
@@ -4,7 +4,7 @@ import { RoleSwitcher } from "./RoleSwitcher";
import { SectionRoadmap } from "./SectionRoadmap";
import { GrooveMap } from "./GrooveMap";
import { PracticeProgress } from "./PracticeProgress";
-import { fillRangeCopy, firstRangeSqueeze } from "./firstRangeSqueeze";
+import { fillRangeCopy, firstRangeOpenPart, firstRangeSqueeze } from "./firstRangeSqueeze";
import { createTranslator, detectPreferredLocale } from "../../i18n";
import { generateCueSheetCsv, generateChartSummaryJson, generateMetadataHandoffJson, sanitizeFilename } from "../../lib/export";
import { Button } from "@/components/ui/button";
@@ -163,6 +163,10 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
}
)
: t("workspaceFirstRangeMissing");
+ const firstRangePart = firstRangeOpenPart(firstRange, activeRole, allRoles);
+ const firstRangeOpenCopy = firstRangePart
+ ? fillRangeCopy(t("workspaceFirstRangeOpenPart"), { roleName: firstRangePart.roleName })
+ : null;
/** Handle the practice progress change internally by immutably updating the song state. */
const handlePracticeProgressChange = (newProgress: number) => {
@@ -308,6 +312,17 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
>