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 ## 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. 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"; import type { PresentationSyncController } from "../sync/presentation-sync-state.js";
type PresenterNavigationBarProps = { type PresenterNavigationBarProps = {
readonly currentIndex: number; readonly currentIndex: number | null;
readonly total: number; readonly total: number;
readonly previous: PresenterBeatNote | null; readonly previous: PresenterBeatNote | null;
readonly next: 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) => ( export const PresenterNavigationBar = ({ currentIndex, total, previous, next, syncController }: PresenterNavigationBarProps) => (
<nav className="presenter-navigation" aria-label="Presenter note navigation"> <nav className="presenter-navigation" aria-label="Presenter note navigation">
<DirectionLink note={previous}> Previous</DirectionLink> <DirectionLink note={previous}> Previous</DirectionLink>
<span>{currentIndex + 1} / {total}</span> <span>{currentIndex === null ? "Q&A" : `${currentIndex + 1} / ${total}`}</span>
<DirectionLink note={next}>Next </DirectionLink> <DirectionLink note={next}>Next </DirectionLink>
<PresentationPairingPanel role="presenter" controller={syncController} /> <PresentationPairingPanel role="presenter" controller={syncController} />
</nav> </nav>
@@ -178,4 +178,21 @@ describe("PresenterRoute", () => {
expect(screen.getByText(/Defense Q&A/i).closest("details")).toHaveAttribute("open"); 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"); 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"); } ? () => { moveFromCurrentHash("previous"); }
: undefined} : undefined}
> >
{navigation.note && ( {(navigation.note || discussion) && (
<PresenterNavigationBar <PresenterNavigationBar
currentIndex={navigation.index} currentIndex={navigation.note ? navigation.index : null}
total={presenterNotes.length} total={presenterNotes.length}
previous={navigation.previous} previous={navigation.previous}
next={navigation.next} next={navigation.next}