diff --git a/AGENTS.md b/AGENTS.md
index b9a67ce17..ce630a0ea 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, new-dropout cues after a leftover return, simplification guidance, transposition or setup cues, part-overlap cues, visible confidence, and rehearsal priorities.
- 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..11fecc9a6 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 and tonight's first new dropout after a leftover return
- 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..255c87f74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Added
+- Name tonight's first new dropout on the ready rehearsal map and tell the leftover-return parts to count that part out, or the newly sitting-out part to stay out, after leftover parts return.
- 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 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.
diff --git a/CLAUDE.md b/CLAUDE.md
index b5a34c1fa..31c4342a1 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 and tonight's first new dropout after a leftover return. `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..b4381fe67 100644
--- a/apps/desktop/src/features/workspace/Workspace.test.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.test.tsx
@@ -196,6 +196,190 @@ describe("Workspace", () => {
);
});
+ it("names tonight's first new dropout after a leftover return", () => {
+ setNavigatorLanguage("en-US");
+ const song = createDemoRehearsalSong();
+ const verse = song.sections[0]!;
+ song.sections = [
+ {
+ ...verse,
+ partGraph: verse.partGraph.map((node) =>
+ node.role_id === "bass-guitar" || node.role_id === "keys-right"
+ ? { ...node, is_active: false }
+ : node
+ )
+ },
+ {
+ ...verse,
+ id: "chorus-1",
+ label: "chorus",
+ timeRange: { start: verse.timeRange.end, end: verse.timeRange.end + 20 }
+ },
+ {
+ ...verse,
+ id: "bridge-1",
+ label: "bridge",
+ timeRange: {
+ start: verse.timeRange.end + 20,
+ end: verse.timeRange.end + 40
+ },
+ partGraph: verse.partGraph.map((node) =>
+ node.role_id === "lead-vocal" ? { ...node, is_active: false } : node
+ )
+ }
+ ];
+
+ render(
{section.label} · {formatTimelineTime(section.timeRange.start)}–{formatTimelineTime(section.timeRange.end)}
@@ -163,6 +168,44 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp } ) : t("workspaceFirstRangeMissing"); + const namedNewDropout = useMemo(() => firstNewDropout(song, activeRole), [activeRole, song]); + const noNewDropoutNeeded = useMemo(() => hasTrustworthyAllActiveTimeline(song), [song]); + const firstNewDropoutCopy = namedNewDropout + ? fillRangeCopy( + t( + activeRole && activeRole === namedNewDropout.dropoutRoleId + ? "workspaceFirstNewDropoutStayOut" + : "workspaceFirstNewDropoutNamed" + ), + { + dropoutRoleName: namedNewDropout.dropoutRoleName, + sectionLabel: namedNewDropout.sectionLabel, + returnSectionLabel: namedNewDropout.returnSectionLabel, + fromSectionLabel: namedNewDropout.fromSectionLabel + } + ) + : t(noNewDropoutNeeded ? "workspaceFirstNewDropoutNone" : "workspaceFirstNewDropoutMissing"); + + /** Open the named new-dropout landing on the renderer-owned map section. */ + const openNamedNewDropout = () => { + if (!namedNewDropout) { + return; + } + const grid = document.querySelector("[data-testid='song-structure-grid']"); + if (!(grid instanceof HTMLElement)) { + return; + } + const landing = grid.querySelector(`[data-section-index="${namedNewDropout.sectionIndex}"]`); + if (!(landing instanceof HTMLElement)) { + return; + } + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + landing.scrollIntoView({ + behavior: reduceMotion ? "auto" : "smooth", + block: "nearest", + inline: "nearest" + }); + }; /** Handle the practice progress change internally by immutably updating the song state. */ const handlePracticeProgressChange = (newProgress: number) => { @@ -309,6 +352,25 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp{t("workspaceFirstRangeTitle")}
{firstRangeCopy}
+{t("workspaceFirstNewDropoutTitle")}
+{firstNewDropoutCopy}
+ {namedNewDropout ? ( + + ) : null} +