fix: keep presenter pairing visible in Q&A

This commit is contained in:
lda
2026-07-14 14:52:40 +07:00 Verified
parent be3e494488
commit 17da3e5d6c
4 changed files with 44 additions and 4 deletions
+23
View File
@@ -47,3 +47,26 @@ Passed with no whitespace errors.
## Self-Review
No critical or important findings. The change is limited to the presenter synchronization seam and tests. The untracked active LAN synchronization plan was not staged. The pre-existing intentional `presenter.css` mobile sticky-navigation edit was retained and verified as part of this task.
## Important Finding Fix: Pairing Persistence On Q&A
The original Task 7 render condition mounted `PresenterNavigationBar` only when `navigation.note` existed. Valid discussion hashes resolve with `note: null`, so Q&A content remained visible but the pairing controls, session-end action, and ended state disappeared.
The stable navigation surface now renders for either a valid presenter note or a resolved discussion branch. Discussion routes pass a nullable progress index and display `Q&A`; Previous and Next remain disabled because no note or destination is fabricated. The existing Q&A content, hash parsing, local note navigation, and presenter mobile CSS are unchanged.
TDD evidence:
```text
RED: pnpm --dir web --filter @lda/console test -- src/presentation/presenter/PresenterRoute.test.tsx
1 test file failed; 1 failed and 9 passed (10 total).
The regression test could not find the Pair presentation button on #discuss/where-is-ai-agent.
GREEN: pnpm --dir web --filter @lda/console test -- src/presentation/presenter/PresenterRoute.test.tsx
1 test file passed; 10 tests passed.
FINAL: pnpm --dir web --filter @lda/console test -- src/presentation/presenter/PresenterRoute.test.tsx src/presentation/presenter/PresenterShell.test.tsx
2 test files passed; 16 tests passed.
pnpm --dir web --filter @lda/console typecheck
@lda/presentation-sync build and console TypeScript project build passed.
```
@@ -4,7 +4,7 @@ import { PresentationPairingPanel } from "../sync/PresentationPairingPanel.js";
import type { PresentationSyncController } from "../sync/presentation-sync-state.js";
type PresenterNavigationBarProps = {
readonly currentIndex: number;
readonly currentIndex: number | null;
readonly total: number;
readonly previous: PresenterBeatNote | null;
readonly next: PresenterBeatNote | null;
@@ -19,7 +19,7 @@ const DirectionLink = ({ note, children }: { readonly note: PresenterBeatNote |
export const PresenterNavigationBar = ({ currentIndex, total, previous, next, syncController }: PresenterNavigationBarProps) => (
<nav className="presenter-navigation" aria-label="Presenter note navigation">
<DirectionLink note={previous}> Previous</DirectionLink>
<span>{currentIndex + 1} / {total}</span>
<span>{currentIndex === null ? "Q&A" : `${currentIndex + 1} / ${total}`}</span>
<DirectionLink note={next}>Next </DirectionLink>
<PresentationPairingPanel role="presenter" controller={syncController} />
</nav>
@@ -178,4 +178,21 @@ describe("PresenterRoute", () => {
expect(screen.getByText(/Defense Q&A/i).closest("details")).toHaveAttribute("open");
expect(screen.getByRole("link", { name: /Where is the AI agent in this thesis/i })).toHaveAttribute("aria-current", "page");
});
it("keeps pairing and ended-session state available on Q&A routes", () => {
mockedUsePresentationSync.mockReturnValue({
...idleController(),
state: { kind: "ended", reason: "presenter_ended" },
});
window.location.hash = "#discuss/where-is-ai-agent";
render(<PresenterRoute />);
expect(screen.getByRole("heading", { name: /Where is the AI agent/i })).toBeInTheDocument();
const navigation = screen.getByRole("navigation", { name: /presenter note navigation/i });
expect(navigation).toHaveTextContent("Q&A");
expect(navigation.querySelectorAll('[aria-disabled="true"]')).toHaveLength(2);
expect(screen.getByRole("button", { name: /pair presentation/i })).toBeInTheDocument();
expect(screen.getByText("The presenter ended this session.")).toBeInTheDocument();
});
});
@@ -65,9 +65,9 @@ export const PresenterRoute = () => {
? () => { moveFromCurrentHash("previous"); }
: undefined}
>
{navigation.note && (
{(navigation.note || discussion) && (
<PresenterNavigationBar
currentIndex={navigation.index}
currentIndex={navigation.note ? navigation.index : null}
total={presenterNotes.length}
previous={navigation.previous}
next={navigation.next}