diff --git a/AGENTS.md b/AGENTS.md
index b9a67ce17..dc31d9f09 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.
+- After analysis, the ready workspace must name the first part to start. Do not leave Roles & Harmony or an empty collaboration card as a description that never opens tonight's chords, range, or simplification.
- 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..20b21e10d 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -68,6 +68,7 @@ Last updated: 2026-03-11
- BandScope is not only a shell around chord labels, stems, and ranges.
- The technical scope includes rehearsal-facing outputs for harmony, section roadmap, groove cues, role entry and dropout cues, simplification guidance, transposition or setup guidance, confidence flags, and rehearsal priority.
- These outputs must stay aligned with `docs/brand-story.md` rather than drifting back to a song-summary-only analyzer.
+- After analysis, the ready workspace must start the first extracted part so tonight's chords, range, and simplification are one action away. Empty collaboration copy must start that same part instead of waiting for assignments.
## Analysis target model
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b6f7e784..1d764cdc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Added
+- Ready workspace now names the first extracted part and starts that role board so tonight's chords, range, and simplification are one action away.
- 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..8f94de504 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
`AGENTS.md` is the canonical agent operating guide — read and follow it before making changes. It defines the security workflow (`Security Notes`), supply-chain workflow, cross-platform build rules, GitHub bootstrap rules, code style, and safety guardrails. This file complements it with commands and architecture; when in doubt, `AGENTS.md` and the docs it references win.
+After analysis, Roles & Harmony and empty collaboration cards must name the first part and start that role board. Do not leave those ready-state surfaces as dead-end descriptions.
+
Agent execution and delegation rules live in `docs/agents/README.md`. PR canonicalization rules live in `docs/workflow/pr-continuity.md`.
## Common commands
diff --git a/apps/desktop/src/features/workspace/Workspace.pick-part-reduced-motion.test.tsx b/apps/desktop/src/features/workspace/Workspace.pick-part-reduced-motion.test.tsx
new file mode 100644
index 000000000..30586d591
--- /dev/null
+++ b/apps/desktop/src/features/workspace/Workspace.pick-part-reduced-motion.test.tsx
@@ -0,0 +1,60 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { createDemoRehearsalSong } from "@bandscope/shared-types";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { Workspace } from "./Workspace";
+
+const originalLanguage = navigator.language;
+const originalMatchMedia = window.matchMedia;
+const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
+
+function setNavigatorLanguage(language: string): void {
+ Object.defineProperty(navigator, "language", {
+ configurable: true,
+ value: language
+ });
+}
+
+describe("Workspace pick-part reduced-motion navigation", () => {
+ afterEach(() => {
+ setNavigatorLanguage(originalLanguage);
+ Object.defineProperty(window, "matchMedia", {
+ configurable: true,
+ value: originalMatchMedia
+ });
+ Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
+ configurable: true,
+ value: originalScrollIntoView
+ });
+ vi.restoreAllMocks();
+ });
+
+ it("uses non-animated scrolling when reduced motion is requested", () => {
+ setNavigatorLanguage("en-US");
+ const scrollIntoView = vi.fn();
+ Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
+ configurable: true,
+ value: scrollIntoView
+ });
+ Object.defineProperty(window, "matchMedia", {
+ configurable: true,
+ value: vi.fn((query: string) => ({
+ matches: query === "(prefers-reduced-motion: reduce)",
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn()
+ }))
+ });
+
+ render();
+ fireEvent.click(screen.getByRole("button", { name: "Start as Bass Guitar" }));
+
+ expect(scrollIntoView).toHaveBeenCalledWith({
+ behavior: "auto",
+ block: "nearest"
+ });
+ });
+});
diff --git a/apps/desktop/src/features/workspace/Workspace.test.tsx b/apps/desktop/src/features/workspace/Workspace.test.tsx
index 7837bf80e..045181e9c 100644
--- a/apps/desktop/src/features/workspace/Workspace.test.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.test.tsx
@@ -325,5 +325,49 @@ describe("Workspace", () => {
expect(screen.getByText("스템")).toBeTruthy();
expect(screen.getByText("합주 우선순위")).toBeTruthy();
expect(screen.getByText("역할과 화성")).toBeTruthy();
+ expect(screen.getByRole("button", { name: "Bass Guitar로 시작" })).toBeTruthy();
+ });
+
+ it("names the first part and opens that role board from the ready workspace", () => {
+ setNavigatorLanguage("en-US");
+ const song = createDemoRehearsalSong();
+
+ render();
+
+ expect(screen.getByTestId("workspace-pick-part-callout")).toBeTruthy();
+ expect(screen.getByText(/Start as Bass Guitar to see tonight's chords/i)).toBeTruthy();
+ expect(screen.queryByText(/The bass holds the vi center/i)).toBeNull();
+
+ fireEvent.click(screen.getByRole("button", { name: "Start as Bass Guitar" }));
+
+ expect(screen.queryByTestId("workspace-pick-part-callout")).toBeNull();
+ expect(screen.getByText(/The bass holds the vi center/i)).toBeTruthy();
+ expect(screen.getByRole("tab", { name: "Bass Guitar", selected: true })).toBeTruthy();
+ });
+
+ it("starts the first part from an empty collaboration card", () => {
+ setNavigatorLanguage("en-US");
+ const song = createDemoRehearsalSong();
+ song.collaboration = undefined;
+
+ render();
+
+ expect(screen.getByText(/No assignments yet. Start as Bass Guitar/i)).toBeTruthy();
+ fireEvent.click(screen.getByRole("button", { name: "Start the room as Bass Guitar" }));
+
+ expect(screen.getByText(/The bass holds the vi center/i)).toBeTruthy();
+ });
+
+ it("hides the pick-part next action when the song has no roles", () => {
+ setNavigatorLanguage("en-US");
+ const song = createDemoRehearsalSong();
+ song.sections = [];
+ song.collaboration = undefined;
+
+ render();
+
+ expect(screen.queryByTestId("workspace-pick-part-callout")).toBeNull();
+ expect(screen.queryByRole("button", { name: /Start as/i })).toBeNull();
+ expect(screen.getByText(/No assignments yet. Start as your part/i)).toBeTruthy();
});
});
diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx
index d44e20777..99811e53f 100644
--- a/apps/desktop/src/features/workspace/Workspace.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.tsx
@@ -9,7 +9,7 @@ import { createTranslator, detectPreferredLocale } from "../../i18n";
import { generateCueSheetCsv, generateChartSummaryJson, generateMetadataHandoffJson, sanitizeFilename } from "../../lib/export";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardDescription } from "@/components/ui/card";
-import { Download, CheckCheck, ClipboardList, MessageSquareMore, CloudOff, Music4 } from "lucide-react";
+import { Download, CheckCheck, ClipboardList, MessageSquareMore, CloudOff, Music4, Users } from "lucide-react";
interface WorkspaceProps {
song: RehearsalSong;
@@ -71,6 +71,28 @@ function safeProjectBootstrapSummary(value: ProjectBootstrapSummary | null): Pro
}
}
+/** Substitute the player-facing role name into a copy template. */
+function fillRoleCopy(template: string, roleName: string): string {
+ return template.replaceAll("{role}", roleName);
+}
+
+/** Scroll the roles board into view after the player starts a part. */
+function focusWorkspaceRolesCard(): void {
+ const node = document.getElementById("workspace-roles-card");
+ if (!(node instanceof HTMLElement)) {
+ return;
+ }
+ const reduceMotion =
+ typeof window.matchMedia === "function" &&
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ if (typeof node.scrollIntoView === "function") {
+ node.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "nearest" });
+ }
+ if (typeof node.focus === "function") {
+ node.focus();
+ }
+}
+
/** Documented. */
const SongStructure = memo(function SongStructure({ sections, t }: { sections: RehearsalSong["sections"]; t: Translator }) {
return (
@@ -151,6 +173,18 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
return roleMap.get(activeRole);
}, [activeRole, roleMap]);
const canTranscribeBass = activeRoleDetails?.name.toLowerCase().includes("bass") ?? false;
+ const firstRole = allRoles[0];
+ const firstRoleName = firstRole?.name.trim() || t("workspacePickPartFallback");
+
+ /** Start tonight's part view on the first extracted role. */
+ const handlePickFirstPart = () => {
+ if (!firstRole) {
+ return;
+ }
+ setActiveRole(firstRole.id);
+ focusWorkspaceRolesCard();
+ };
+
const firstRange = useMemo(() => firstRangeSqueeze(song, activeRole), [activeRole, song]);
const firstRangeCopy = firstRange
? fillRangeCopy(
@@ -336,7 +370,24 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
) : (
-