diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt
index 7c10833d2..3fb4228d0 100644
--- a/docs_build/dev/reports/codex_changed_files.txt
+++ b/docs_build/dev/reports/codex_changed_files.txt
@@ -1,34 +1,54 @@
-# git status --short
-M docs_build/dev/reports/codex_changed_files.txt
- M docs_build/dev/reports/codex_review.diff
- M docs_build/dev/reports/coverage_changed_js_guardrail.txt
- M docs_build/dev/reports/playwright_v8_coverage_report.txt
- M tests/playwright/tools/TextToSpeechFunctional.spec.mjs
- M tests/tools/Text2SpeechShell.test.mjs
- M toolbox/text-to-speech/index.html
- M toolbox/text-to-speech/text2speech.js
-?? docs_build/dev/reports/PR_26171_067-instruction-compliance-checklist.md
-?? docs_build/dev/reports/PR_26171_067-manual-validation-notes.md
-?? docs_build/dev/reports/PR_26171_067-message-tts-contract-checklist.md
-?? docs_build/dev/reports/PR_26171_067-parent-child-table-checklist.md
-?? docs_build/dev/reports/PR_26171_067-tts-profile-emotion-table-foundation.md
-?? docs_build/dev/reports/PR_26171_067-validation.md
+PR_26171_044-idea-board-game-hub-project-flow
-# git ls-files --others --exclude-standard
-docs_build/dev/reports/PR_26171_067-instruction-compliance-checklist.md
-docs_build/dev/reports/PR_26171_067-manual-validation-notes.md
-docs_build/dev/reports/PR_26171_067-message-tts-contract-checklist.md
-docs_build/dev/reports/PR_26171_067-parent-child-table-checklist.md
-docs_build/dev/reports/PR_26171_067-tts-profile-emotion-table-foundation.md
-docs_build/dev/reports/PR_26171_067-validation.md
+Changed files:
+- docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/PLAN_PR.md
+- docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md
+- docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md
+- docs_build/dev/reports/codex_review.diff
+- docs_build/dev/reports/codex_changed_files.txt
+- src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js
+- tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs
+- tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
+- tests/playwright/tools/ToolboxRoutePages.spec.mjs
+- toolbox/game-workspace/game-workspace.js
+- toolbox/game-workspace/index.html
+- toolbox/idea-board/index.html
+- toolbox/idea-board/index.js
-# git diff --stat
-docs_build/dev/reports/codex_changed_files.txt | 75 +-
- docs_build/dev/reports/codex_review.diff | 2766 +++++++++-----------
- .../dev/reports/coverage_changed_js_guardrail.txt | 9 +-
- .../dev/reports/playwright_v8_coverage_report.txt | 27 +-
- .../tools/TextToSpeechFunctional.spec.mjs | 42 +-
- tests/tools/Text2SpeechShell.test.mjs | 48 +
- toolbox/text-to-speech/index.html | 40 +-
- toolbox/text-to-speech/text2speech.js | 728 +++++-
- 8 files changed, 2153 insertions(+), 1582 deletions(-)
\ No newline at end of file
+Requirement PASS evidence:
+- PASS: Create Project appears only for Ready ideas. Evidence: Idea Board actions remain Edit/Create Project/Delete for Ready rows and switch to Open in Game Hub/Archive after creation in targeted Playwright.
+- PASS: Create Project uses the shared Game Hub project contract. Evidence: Idea Board calls the existing game-workspace server repository client createGame method; Playwright asserts the createGame repository request occurs.
+- PASS: Create Project sets Idea status to Project. Evidence: targeted Playwright asserts the row status becomes Project.
+- PASS: Project ideas and notes are read-only. Evidence: targeted Playwright asserts no Edit/Delete/Add Note/note actions remain for the Project row.
+- PASS: Project row actions are Open in Game Hub and Archive. Evidence: targeted Playwright asserts exact action labels.
+- PASS: Project ideas cannot be edited or deleted while Project. Evidence: Project rows do not render Edit/Delete actions and deletion guard remains active.
+- PASS: Project ideas cannot add/edit/delete notes. Evidence: expanded Project notes render without Add Note or note actions.
+- PASS: Archived ideas remain hidden by default and show Restore/Delete when visible. Evidence: targeted Playwright archives, shows Archived through the filter, and asserts Restore/Delete.
+- PASS: Game Hub shows creator-facing Project Information. Evidence: Game Hub HTML/JS replaces internal record display with Project Information and targeted Game Hub Playwright passes.
+- PASS: Game Hub shows read-only Source Idea fields. Evidence: Idea Board-to-Game Hub Playwright asserts Idea, Pitch, and Notes from the source idea render in Game Hub.
+- PASS: Game Hub avoids internal IDs, DB/API/mock/debug/seed wording in creator-facing display. Evidence: touched runtime files pass creator-visible text scan and targeted Playwright checks the navigated Game Hub main content.
+- PASS: Open in Game Hub navigates to the related project. Evidence: targeted Playwright waits for /toolbox/game-workspace/index.html?game=lantern-reef-* and asserts the linked project renders.
+- PASS: No Game Journey expansion beyond existing link/reference. Evidence: implementation does not add Game Journey creation or new journey wiring.
+
+Validation evidence:
+- PASS: node --check toolbox/idea-board/index.js
+- PASS: node --check toolbox/game-workspace/game-workspace.js
+- PASS: node --check toolbox/game-workspace/game-workspace-api-client.js
+- PASS: node --check src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js
+- PASS: node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
+- PASS: node --check tests/playwright/tools/ToolboxRoutePages.spec.mjs
+- PASS: node --check tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs
+- PASS: npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line
+- PASS: npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Idea Board launches"
+- PASS: npx playwright test tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Game Hub"
+- PASS: npm run test:workspace-v2
+- PASS: git diff --check
+
+Git workflow:
+- Current branch: codex/pr-26171-044-idea-board-game-hub-project-flow
+- Created branch: codex/pr-26171-044-idea-board-game-hub-project-flow
+- Commit hash: 2a38796f81aae2dceb4151095b8f89a276cd2d32
+- Push result: pushed to origin/codex/pr-26171-044-idea-board-game-hub-project-flow
+- PR URL: https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/22
+- Merge result: to be reported after merge
+- Final main commit: to be reported after returning to main and pulling latest
diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff
index 85677add8..cffbc3823 100644
--- a/docs_build/dev/reports/codex_review.diff
+++ b/docs_build/dev/reports/codex_review.diff
@@ -1,4245 +1,1210 @@
-diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt
-index e0820a401..33a70c4b4 100644
---- a/docs_build/dev/reports/codex_changed_files.txt
-+++ b/docs_build/dev/reports/codex_changed_files.txt
-@@ -1,53 +1,30 @@
--# Codex Changed Files - PR_26171_065-message-studio-parent-child-table-foundation
--
--## Git Status Short
--```text
-+# git status --short
- M docs_build/dev/reports/coverage_changed_js_guardrail.txt
- M docs_build/dev/reports/playwright_v8_coverage_report.txt
-- M tests/playwright/tools/MessagesTool.spec.mjs
-- M toolbox/messages/index.html
-- M toolbox/messages/message-tts-service-registry.js
-- M toolbox/messages/messages.js
--?? docs_build/dev/reports/PR_26171_065-instruction-compliance-checklist.md
--?? docs_build/dev/reports/PR_26171_065-manual-validation-notes.md
--?? docs_build/dev/reports/PR_26171_065-message-studio-parent-child-table-foundation.md
--?? docs_build/dev/reports/PR_26171_065-parent-child-table-checklist.md
--?? docs_build/dev/reports/PR_26171_065-validation.md
--```
--
--## Scoped Diff Stat
--```text
--.../dev/reports/coverage_changed_js_guardrail.txt | 2 +
-- .../dev/reports/playwright_v8_coverage_report.txt | 43 +-
-- tests/playwright/tools/MessagesTool.spec.mjs | 317 +++++---------
-- toolbox/messages/index.html | 29 +-
-- toolbox/messages/message-tts-service-registry.js | 8 +-
-- toolbox/messages/messages.js | 486 ++++++++++++---------
-- 6 files changed, 415 insertions(+), 470 deletions(-)
--```
--
--## Changed Files
--- toolbox/messages/index.html
--- toolbox/messages/messages.js
--- toolbox/messages/message-tts-service-registry.js
--- tests/playwright/tools/MessagesTool.spec.mjs
--- docs_build/dev/reports/coverage_changed_js_guardrail.txt
--- docs_build/dev/reports/playwright_v8_coverage_report.txt
--- docs_build/dev/reports/PR_26171_065-message-studio-parent-child-table-foundation.md
--- docs_build/dev/reports/PR_26171_065-parent-child-table-checklist.md
--- docs_build/dev/reports/PR_26171_065-validation.md
--- docs_build/dev/reports/PR_26171_065-manual-validation-notes.md
--- docs_build/dev/reports/PR_26171_065-instruction-compliance-checklist.md
--- docs_build/dev/reports/codex_review.diff
--- docs_build/dev/reports/codex_changed_files.txt
-+ M tests/playwright/tools/TextToSpeechFunctional.spec.mjs
-+ M tests/tools/Text2SpeechShell.test.mjs
-+ M toolbox/text-to-speech/index.html
-+ M toolbox/text-to-speech/text2speech.js
-+?? docs_build/dev/reports/PR_26171_067-instruction-compliance-checklist.md
-+?? docs_build/dev/reports/PR_26171_067-manual-validation-notes.md
-+?? docs_build/dev/reports/PR_26171_067-message-tts-contract-checklist.md
-+?? docs_build/dev/reports/PR_26171_067-parent-child-table-checklist.md
-+?? docs_build/dev/reports/PR_26171_067-tts-profile-emotion-table-foundation.md
-+?? docs_build/dev/reports/PR_26171_067-validation.md
-
--## Validation
--- PASS: `node --check toolbox\messages\messages.js`.
--- PASS: `node --check toolbox\messages\message-tts-service-registry.js`.
--- PASS: `node --check tests\playwright\tools\MessagesTool.spec.mjs`.
--- PASS: HTML inline style/script/event scan for `toolbox/messages/index.html`.
--- PASS: `npx playwright test tests/playwright/tools/MessagesTool.spec.mjs --project=playwright --workers=1 --reporter=list`.
--- PASS: `npm run test:workspace-v2` (legacy command name; user-facing language is Project Workspace).
-+# git ls-files --others --exclude-standard
-+docs_build/dev/reports/PR_26171_067-instruction-compliance-checklist.md
-+docs_build/dev/reports/PR_26171_067-manual-validation-notes.md
-+docs_build/dev/reports/PR_26171_067-message-tts-contract-checklist.md
-+docs_build/dev/reports/PR_26171_067-parent-child-table-checklist.md
-+docs_build/dev/reports/PR_26171_067-tts-profile-emotion-table-foundation.md
-+docs_build/dev/reports/PR_26171_067-validation.md
-
--## ZIP
--- Path: `tmp/PR_26171_065-message-studio-parent-child-table-foundation_delta.zip`.
-+# git diff --stat
-+.../dev/reports/coverage_changed_js_guardrail.txt | 9 +-
-+ .../dev/reports/playwright_v8_coverage_report.txt | 27 +-
-+ .../tools/TextToSpeechFunctional.spec.mjs | 42 +-
-+ tests/tools/Text2SpeechShell.test.mjs | 48 ++
-+ toolbox/text-to-speech/index.html | 40 +-
-+ toolbox/text-to-speech/text2speech.js | 728 ++++++++++++++++++++-
-+ 6 files changed, 871 insertions(+), 23 deletions(-)
-\ No newline at end of file
-diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff
-index 39d7f84a5..0bec7d9bf 100644
---- a/docs_build/dev/reports/codex_review.diff
-+++ b/docs_build/dev/reports/codex_review.diff
-@@ -1,1576 +1,1322 @@
--diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt
--index 15213f1d6..e0820a401 100644
----- a/docs_build/dev/reports/codex_changed_files.txt
--+++ b/docs_build/dev/reports/codex_changed_files.txt
--@@ -1,78 +1,53 @@
---# PR_26171_042 Codex Changed Files Report
---
---## Instruction Compliance Gate
---
---- Current branch before execution: `main`
---- Required execution branch before PR branch: `main`
---- Branch validation: PASS
---- Clean repository before branch creation: PASS
---- PR owner/parity: PASS, `042` is even and Idea/Tool Display navigation scope maps to PC / Environment 1.
---- Implementation path: PASS, active Idea Board and Theme V2 files only.
---- Validation scope: PASS, targeted Idea Board, targeted Toolbox route for Idea Board, and workspace contract lane because shared Tool Display Mode behavior changed.
---- Required reports: PASS, `docs_build/dev/reports/codex_review.diff` and this report are updated.
---- ZIP requirement: PASS, `tmp/PR_26171_042-idea-board-navigation-fallback-cleanup_delta.zip` is required and produced before final delivery.
---
---## Git Workflow Fields
---
---- Created branch: `codex/pr-26171-042-idea-board-navigation-fallback-cleanup`
---- Push result: PASS, branch pushed to `origin/codex/pr-26171-042-idea-board-navigation-fallback-cleanup`.
---- PR URL: `https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/19`
---- Merge result: recorded in final Codex delivery after GitHub merge returns the merge SHA.
---- Final main commit: recorded in final Codex delivery after returning to `main` and pulling latest.
---- Conflict resolution: PASS, merged latest `origin/main` (`9df4942226b0c1a25cfc9567040fc237d90df8f9`) and resolved conflicts only in singleton generated report artifacts.
---
---## Scoped Files
---
---- `assets/theme-v2/js/tool-display-mode.js`
---- `tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`
---- `tests/playwright/tools/ToolboxRoutePages.spec.mjs`
---- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/PLAN_PR.md`
---- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/BUILD_PR.md`
---- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/APPLY_PR.md`
---- `docs_build/dev/reports/coverage_changed_js_guardrail.txt`
---- `docs_build/dev/reports/playwright_v8_coverage_report.txt`
---- `docs_build/dev/reports/codex_review.diff`
---- `docs_build/dev/reports/codex_changed_files.txt`
---
---## Requirement Evidence
---
---- PASS: Removed creator-visible Tool Display Mode navigation diagnostic fallback. `tool-display-mode.js` now logs the navigation load failure to console only and appends no status paragraph.
---- PASS: Removed visible message `Tool navigation is temporarily unavailable. Refresh the page or try again shortly.`
---- PASS: Idea Board stays usable when registry-backed navigation cannot load. Static/no-registry Playwright path expands notes and adds a note successfully.
---- PASS: Creator-facing UI does not mention server, API, local server, port, registry, snapshot, or implementation details in the navigation fallback area.
---- PASS: Navigation failure does not affect Idea Board table functionality.
---- PASS: API-backed local route validated by targeted Idea Board and Toolbox route Playwright.
---- PASS: Static/no-registry route behavior validated by targeted Idea Board Playwright with the registry snapshot returning no data.
---- PASS: Optional previous/next navigation is omitted when unavailable.
---- PASS: Idea Board lifecycle, Show filter, Create Project, Archive, chevron, and table row editing behavior were not changed.
--+# Codex Changed Files - PR_26171_065-message-studio-parent-child-table-foundation
--+
--+## Git Status Short
--+```text
--+M docs_build/dev/reports/coverage_changed_js_guardrail.txt
--+ M docs_build/dev/reports/playwright_v8_coverage_report.txt
--+ M tests/playwright/tools/MessagesTool.spec.mjs
--+ M toolbox/messages/index.html
--+ M toolbox/messages/message-tts-service-registry.js
--+ M toolbox/messages/messages.js
--+?? docs_build/dev/reports/PR_26171_065-instruction-compliance-checklist.md
--+?? docs_build/dev/reports/PR_26171_065-manual-validation-notes.md
--+?? docs_build/dev/reports/PR_26171_065-message-studio-parent-child-table-foundation.md
--+?? docs_build/dev/reports/PR_26171_065-parent-child-table-checklist.md
--+?? docs_build/dev/reports/PR_26171_065-validation.md
--+```
--+
--+## Scoped Diff Stat
--+```text
--+.../dev/reports/coverage_changed_js_guardrail.txt | 2 +
--+ .../dev/reports/playwright_v8_coverage_report.txt | 43 +-
--+ tests/playwright/tools/MessagesTool.spec.mjs | 317 +++++---------
--+ toolbox/messages/index.html | 29 +-
--+ toolbox/messages/message-tts-service-registry.js | 8 +-
--+ toolbox/messages/messages.js | 486 ++++++++++++---------
--+ 6 files changed, 415 insertions(+), 470 deletions(-)
--+```
--+
--+## Changed Files
--+- toolbox/messages/index.html
--+- toolbox/messages/messages.js
--+- toolbox/messages/message-tts-service-registry.js
--+- tests/playwright/tools/MessagesTool.spec.mjs
--+- docs_build/dev/reports/coverage_changed_js_guardrail.txt
--+- docs_build/dev/reports/playwright_v8_coverage_report.txt
--+- docs_build/dev/reports/PR_26171_065-message-studio-parent-child-table-foundation.md
--+- docs_build/dev/reports/PR_26171_065-parent-child-table-checklist.md
--+- docs_build/dev/reports/PR_26171_065-validation.md
--+- docs_build/dev/reports/PR_26171_065-manual-validation-notes.md
--+- docs_build/dev/reports/PR_26171_065-instruction-compliance-checklist.md
--+- docs_build/dev/reports/codex_review.diff
--+- docs_build/dev/reports/codex_changed_files.txt
--
-- ## Validation
---
---- PASS: `node --check assets/theme-v2/js/tool-display-mode.js`
---- PASS: `node --check toolbox/idea-board/index.js`
---- PASS: `node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`
---- PASS: `node --check tests/playwright/tools/ToolboxRoutePages.spec.mjs`
---- PASS: `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line --timeout=90000`
---- PASS: `npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --project=playwright --workers=1 --reporter=line -g "Idea Board launches" --timeout=90000`
---- PASS: `npm run test:workspace-v2`
---- PASS: `git diff --check`
---- SKIP: Full samples smoke, per user instruction.
---
---## Coverage Evidence
---
---- PASS: `docs_build/dev/reports/playwright_v8_coverage_report.txt` updated.
---- PASS: `docs_build/dev/reports/coverage_changed_js_guardrail.txt` updated.
---- PASS: Changed runtime JS coverage lists `assets/theme-v2/js/tool-display-mode.js` at 64% advisory function coverage.
---
---## ZIP Contents
---
---- `assets/theme-v2/js/tool-display-mode.js`
---- `docs_build/dev/reports/codex_changed_files.txt`
---- `docs_build/dev/reports/codex_review.diff`
---- `docs_build/dev/reports/coverage_changed_js_guardrail.txt`
---- `docs_build/dev/reports/playwright_v8_coverage_report.txt`
---- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/APPLY_PR.md`
---- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/BUILD_PR.md`
---- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/PLAN_PR.md`
---- `tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`
---- `tests/playwright/tools/ToolboxRoutePages.spec.mjs`
--+- PASS: `node --check toolbox\messages\messages.js`.
--+- PASS: `node --check toolbox\messages\message-tts-service-registry.js`.
--+- PASS: `node --check tests\playwright\tools\MessagesTool.spec.mjs`.
--+- PASS: HTML inline style/script/event scan for `toolbox/messages/index.html`.
--+- PASS: `npx playwright test tests/playwright/tools/MessagesTool.spec.mjs --project=playwright --workers=1 --reporter=list`.
--+- PASS: `npm run test:workspace-v2` (legacy command name; user-facing language is Project Workspace).
--+
--+## ZIP
--+- Path: `tmp/PR_26171_065-message-studio-parent-child-table-foundation_delta.zip`.
- diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.txt
--index 242796c4f..6fbeaba58 100644
-+index 6fbeaba58..21de9adb6 100644
- --- a/docs_build/dev/reports/coverage_changed_js_guardrail.txt
- +++ b/docs_build/dev/reports/coverage_changed_js_guardrail.txt
--@@ -7,6 +7,8 @@ Source: Playwright/Chromium built-in V8 coverage from the active Playwright run.
--
-+@@ -6,9 +6,10 @@ Missing changed runtime JS files are WARN, not FAIL.
-+ Source: Playwright/Chromium built-in V8 coverage from the active Playwright run.
+diff --git a/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md
+new file mode 100644
+index 000000000..2fba95562
+--- /dev/null
++++ b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md
+@@ -0,0 +1,17 @@
++# PR_26171_044-idea-board-game-hub-project-flow Apply
++
++## Apply Steps
++
++1. Start from clean `main`.
++2. Pull latest `main`.
++3. Create `codex/pr-26171-044-idea-board-game-hub-project-flow`.
++4. Implement only the BUILD scope.
++5. Run requested validation.
++6. Stage scoped files only.
++7. Commit.
++8. Push branch.
++9. Create PR.
++10. Resolve conflicts if encountered and rerun validation.
++11. Merge after validation passes.
++12. Return to `main` and pull latest.
++13. Produce final report with PR URL, merge result, final main commit, ZIP path, ZIP size, ZIP contents, and requirement PASS evidence.
+diff --git a/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md
+new file mode 100644
+index 000000000..228f7703f
+--- /dev/null
++++ b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md
+@@ -0,0 +1,56 @@
++# PR_26171_044-idea-board-game-hub-project-flow Build
+
- Changed runtime JS files considered:
-- (64%) assets/theme-v2/js/tool-display-mode.js - executed lines 204/204; executed functions 9/14
--+(87%) toolbox/messages/messages.js - executed lines 1233/1233; executed functions 103/118
--+(100%) toolbox/messages/message-tts-service-registry.js - executed lines 42/42; executed functions 6/6
--
-+-(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 204/204; executed functions 9/14
-+-(87%) toolbox/messages/messages.js - executed lines 1233/1233; executed functions 103/118
-+-(100%) toolbox/messages/message-tts-service-registry.js - executed lines 42/42; executed functions 6/6
-++(0%) toolbox/messages/message-tts-service-registry.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
-++(0%) toolbox/messages/messages.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
-++(80%) toolbox/text-to-speech/text2speech.js - executed lines 1518/1518; executed functions 127/158
++## Source Of Truth
+
- Guardrail warnings:
-- (100%) none - no changed runtime JS coverage warnings
-+-(100%) none - no changed runtime JS coverage warnings
-++(0%) toolbox/messages/message-tts-service-registry.js - WARNING: changed runtime JS file missing from coverage; advisory only
-++(0%) toolbox/messages/messages.js - WARNING: changed runtime JS file missing from coverage; advisory only
- diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.txt
--index 17eb943d2..578a19ddc 100644
-+index 578a19ddc..49e94f0d7 100644
- --- a/docs_build/dev/reports/playwright_v8_coverage_report.txt
- +++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt
--@@ -12,45 +12,34 @@ Note: entry percentages use function coverage when available, otherwise line cov
-+@@ -12,34 +12,33 @@ Note: entry percentages use function coverage when available, otherwise line cov
- Note: coverage entries are aggregated across every page/tool where coverageReporter.start(page) and coverageReporter.stop(page) ran.
--
++Use the user request for `PR_26171_044-idea-board-game-hub-project-flow`, `docs_build/dev/PROJECT_INSTRUCTIONS.md`, `docs_build/dev/PROJECT_MULTI_PC.txt`, and this BUILD doc.
+
- Exercised tool entry points detected:
---(72%) Toolbox Index - exercised 10 runtime JS files
--+(83%) Toolbox Index - exercised 4 runtime JS files
-+-(83%) Toolbox Index - exercised 4 runtime JS files
-++(80%) Toolbox Index - exercised 2 runtime JS files
- (0%) Tool Template V2 - not exercised by this Playwright run
---(61%) Theme V2 Shared JS - exercised 7 runtime JS files
--+(56%) Theme V2 Shared JS - exercised 2 runtime JS files
--
-+ (56%) Theme V2 Shared JS - exercised 2 runtime JS files
++## Singular Purpose
+
- Changed runtime JS files covered:
-- (64%) assets/theme-v2/js/tool-display-mode.js - executed lines 204/204; executed functions 9/14
--+(87%) toolbox/messages/messages.js - executed lines 1233/1233; executed functions 103/118
--+(100%) toolbox/messages/message-tts-service-registry.js - executed lines 42/42; executed functions 6/6
--
-+-(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 204/204; executed functions 9/14
-+-(87%) toolbox/messages/messages.js - executed lines 1233/1233; executed functions 103/118
-+-(100%) toolbox/messages/message-tts-service-registry.js - executed lines 42/42; executed functions 6/6
-++(0%) toolbox/messages/message-tts-service-registry.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
-++(0%) toolbox/messages/messages.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
-++(80%) toolbox/text-to-speech/text2speech.js - executed lines 1518/1518; executed functions 127/158
++Complete the Idea Board to Game Hub creator project handoff.
+
- Files with executed line/function counts where available:
---(14%) assets/theme-v2/js/account-auth-service.js - executed lines 64/64; executed functions 1/7
---(14%) assets/theme-v2/js/admin-setup-actions.js - executed lines 55/55; executed functions 1/7
---(18%) assets/theme-v2/js/account-page-data.js - executed lines 150/150; executed functions 3/17
---(20%) assets/theme-v2/js/admin-owner-navigation.js - executed lines 58/58; executed functions 2/10
---(25%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 1/4
---(25%) src/api/session-api-client.js - executed lines 68/68; executed functions 3/12
---(29%) src/engine/input/NormalizedInputRegistry.js - executed lines 341/341; executed functions 6/21
---(33%) src/api/admin-setup-api-client.js - executed lines 13/13; executed functions 1/3
---(33%) src/api/toolbox-votes-api-client.js - executed lines 46/46; executed functions 2/6
---(56%) toolbox/colors/colors.js - executed lines 1848/1848; executed functions 115/204
---(58%) src/api/server-api-client.js - executed lines 167/167; executed functions 11/19
--+(34%) src/engine/audio/TextToSpeechEngine.js - executed lines 412/412; executed functions 15/44
--+(36%) src/api/server-api-client.js - executed lines 167/167; executed functions 5/14
--+(38%) src/api/public-config-client.js - executed lines 209/209; executed functions 10/26
--+(54%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 977/977; executed functions 46/85
--+(58%) toolbox/messages/messages-api-client.js - executed lines 64/64; executed functions 11/19
-+-(34%) src/engine/audio/TextToSpeechEngine.js - executed lines 412/412; executed functions 15/44
-+ (36%) src/api/server-api-client.js - executed lines 167/167; executed functions 5/14
-+ (38%) src/api/public-config-client.js - executed lines 209/209; executed functions 10/26
-+ (54%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 977/977; executed functions 46/85
-+-(58%) toolbox/messages/messages-api-client.js - executed lines 64/64; executed functions 11/19
- (64%) assets/theme-v2/js/tool-display-mode.js - executed lines 204/204; executed functions 9/14
---(64%) toolbox/controls/controls.js - executed lines 610/610; executed functions 36/56
---(65%) src/api/public-config-client.js - executed lines 209/209; executed functions 17/26
---(67%) src/api/game-journey-completion-api-client.js - executed lines 15/15; executed functions 2/3
---(74%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 977/977; executed functions 67/90
---(75%) toolbox/game-workspace/game-workspace.js - executed lines 458/458; executed functions 33/44
---(89%) toolbox/tools-page-accordions.js - executed lines 1156/1156; executed functions 105/118
---(90%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 26/29
---(91%) toolbox/game-design/game-design.js - executed lines 254/254; executed functions 21/23
---(94%) assets/theme-v2/js/marketplace-page.js - executed lines 170/170; executed functions 16/17
---(100%) src/api/marketplace-api-client.js - executed lines 16/16; executed functions 3/3
---(100%) toolbox/colors/palette-api-client.js - executed lines 28/28; executed functions 4/4
---(100%) toolbox/controls/controls-api-client.js - executed lines 33/33; executed functions 5/5
---(100%) toolbox/game-design/game-design-api-client.js - executed lines 13/13; executed functions 2/2
---(100%) toolbox/game-workspace/game-workspace-api-client.js - executed lines 20/20; executed functions 3/3
--+(76%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 22/29
--+(87%) toolbox/messages/messages.js - executed lines 1233/1233; executed functions 103/118
--+(100%) src/engine/audio/TextToSpeechDefaults.js - executed lines 108/108; executed functions 1/1
--+(100%) toolbox/messages/message-tts-service-registry.js - executed lines 42/42; executed functions 6/6
--
-++(71%) src/engine/audio/TextToSpeechEngine.js - executed lines 412/412; executed functions 37/52
-+ (76%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 22/29
-+-(87%) toolbox/messages/messages.js - executed lines 1233/1233; executed functions 103/118
-++(80%) toolbox/text-to-speech/text2speech.js - executed lines 1518/1518; executed functions 127/158
-+ (100%) src/engine/audio/TextToSpeechDefaults.js - executed lines 108/108; executed functions 1/1
-+-(100%) toolbox/messages/message-tts-service-registry.js - executed lines 42/42; executed functions 6/6
++## Exact Targets
+
- Uncovered or low-coverage changed JS files:
-- (100%) none - no low-coverage changed runtime JS files
--
-+-(100%) none - no low-coverage changed runtime JS files
-++(0%) toolbox/messages/message-tts-service-registry.js - WARNING: uncovered changed runtime JS file; advisory only
-++(0%) toolbox/messages/messages.js - WARNING: uncovered changed runtime JS file; advisory only
++- `toolbox/idea-board/index.js`
++- `toolbox/game-workspace/game-workspace.js`
++- Existing targeted Playwright specs for Idea Board and Game Hub/Game Workspace.
++- Any smallest existing shared project or handoff contract file required by the flow.
++- `docs_build/dev/reports/codex_review.diff`
++- `docs_build/dev/reports/codex_changed_files.txt`
+
- Changed JS files considered:
-- (0%) tests/playwright/tools/IdeaBoardTableNotes.spec.mjs - changed JS file not collected as browser runtime coverage
--+(0%) tests/playwright/tools/MessagesTool.spec.mjs - changed JS file not collected as browser runtime coverage
-- (0%) tests/playwright/tools/ToolboxRoutePages.spec.mjs - changed JS file not collected as browser runtime coverage
-- (64%) assets/theme-v2/js/tool-display-mode.js - changed JS file with browser V8 coverage
--+(87%) toolbox/messages/messages.js - changed JS file with browser V8 coverage
--+(100%) toolbox/messages/message-tts-service-registry.js - changed JS file with browser V8 coverage
--diff --git a/tests/playwright/tools/MessagesTool.spec.mjs b/tests/playwright/tools/MessagesTool.spec.mjs
--index b120f19a3..451c4439f 100644
----- a/tests/playwright/tools/MessagesTool.spec.mjs
--+++ b/tests/playwright/tools/MessagesTool.spec.mjs
--@@ -31,7 +31,7 @@ async function jsonRequest(url, options = {}) {
-- test.beforeEach(async ({ page }) => {
-- await installPlaywrightStorageIsolation(page, {
-- lane: "messages-tool",
--- surface: "Message Studio Local API, legacy SQLite technical debt adapter, and Theme V2 tool",
--+ surface: "Message Studio parent/child table, Local API, and Theme V2 tool",
-- });
-+-(0%) tests/playwright/tools/IdeaBoardTableNotes.spec.mjs - changed JS file not collected as browser runtime coverage
-+ (0%) tests/playwright/tools/MessagesTool.spec.mjs - changed JS file not collected as browser runtime coverage
-+-(0%) tests/playwright/tools/ToolboxRoutePages.spec.mjs - changed JS file not collected as browser runtime coverage
-+-(64%) assets/theme-v2/js/tool-display-mode.js - changed JS file with browser V8 coverage
-+-(87%) toolbox/messages/messages.js - changed JS file with browser V8 coverage
-+-(100%) toolbox/messages/message-tts-service-registry.js - changed JS file with browser V8 coverage
-++(0%) tests/playwright/tools/TextToSpeechFunctional.spec.mjs - changed JS file not collected as browser runtime coverage
-++(0%) tests/tools/Text2SpeechShell.test.mjs - changed JS file not collected as browser runtime coverage
-++(0%) toolbox/messages/message-tts-service-registry.js - changed JS file not collected as browser runtime coverage
-++(0%) toolbox/messages/messages.js - changed JS file not collected as browser runtime coverage
-++(80%) toolbox/text-to-speech/text2speech.js - changed JS file with browser V8 coverage
-+diff --git a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs
-+index 17e2c0b62..8c964fecc 100644
-+--- a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs
-++++ b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs
-+@@ -106,7 +106,45 @@ test("Text To Speech page loads and speaks through browser speech synthesis", as
++## Requirements
+
-+ await expect(page.locator("[data-tts-voice-select]")).toContainText("Arcade Voice");
-+ await expect(page.locator("[data-tts-voice-count]")).toHaveText("2");
-+- await expect(page.locator("[data-tts-engine-label]")).toHaveText("Ready");
-++ await expect(page.locator("[data-tts-profile-count]")).toHaveText("3");
-++ await expect(page.locator("[data-tts-emotion-count]")).toHaveText("3");
-++ await expect(page.locator("[data-tts-profile-table]")).toContainText("Default Balanced Profile");
-++ await expect(page.locator("[data-tts-profile-table]")).toContainText("Man Profile 1");
-++ await expect(page.locator("[data-tts-profile-table]")).toContainText("Woman Profile 2");
-++ await expect(page.locator("[data-tts-profile-row]").filter({ hasText: "Default Balanced Profile" }).getByRole("button", { name: "Delete" })).toBeDisabled();
-++ await page.locator("[data-tts-profile-row]").filter({ hasText: "Default Balanced Profile" }).click();
-++ await expect(page.getByRole("heading", { name: "Emotion Settings" })).toBeVisible();
-++ await expect(page.getByRole("columnheader", { name: "Emotion", exact: true })).toBeVisible();
-++ await expect(page.getByRole("columnheader", { name: "SSML-like Preset" })).toBeVisible();
-++ await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Neutral" }).getByRole("button", { name: "Delete" })).toBeDisabled();
-++
-++ await page.getByRole("button", { name: "Add Profile" }).click();
-++ await expect(page.locator("[data-tts-profile-editor='__new__']")).toBeVisible();
-++ await page.locator("[data-tts-profile-editor='__new__'] [data-tts-profile-name]").fill("Creature Profile");
-++ await page.locator("[data-tts-profile-editor='__new__'] [data-tts-profile-gender]").selectOption("neutral");
-++ await page.locator("[data-tts-commit-profile='__new__']").click();
-++ await expect(page.locator("[data-tts-status]")).toHaveText("Saved TTS profile: Creature Profile.");
-++ await expect(page.locator("[data-tts-profile-table]")).toContainText("Creature Profile");
-++ await page.locator("[data-tts-profile-row]").filter({ hasText: "Creature Profile" }).getByRole("button", { name: "Edit Profile" }).click();
-++ await page.locator("[data-tts-profile-editor] [data-tts-profile-name]").fill("Creature Profile Updated");
-++ await page.locator("[data-tts-profile-editor] [data-tts-commit-profile]").click();
-++ await expect(page.locator("[data-tts-status]")).toHaveText("Saved TTS profile: Creature Profile Updated.");
-++ await page.getByRole("button", { name: "Add Emotion" }).click();
-++ await page.locator("[data-tts-emotion-editor='__new__'] [data-tts-emotion-name]").selectOption("urgent");
-++ await page.locator("[data-tts-emotion-editor='__new__'] [data-tts-emotion-pitch]").fill("1.2");
-++ await page.locator("[data-tts-emotion-editor='__new__'] [data-tts-emotion-rate]").fill("1.1");
-++ await page.locator("[data-tts-emotion-editor='__new__'] [data-tts-emotion-volume]").fill("0.8");
-++ await page.locator("[data-tts-emotion-editor='__new__'] [data-tts-emotion-ssml-preset]").selectOption("whisper-ish");
-++ await page.locator("[data-tts-commit-emotion='__new__']").click();
-++ await expect(page.locator("[data-tts-status]")).toHaveText("Saved emotion setting: Urgent.");
-++ await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Urgent" })).toContainText("1.2");
-++ await page.locator("[data-tts-emotion-row]").filter({ hasText: "Urgent" }).getByRole("button", { name: "Edit Emotion" }).click();
-++ await page.locator("[data-tts-emotion-editor] [data-tts-emotion-volume]").fill("0.7");
-++ await page.locator("[data-tts-emotion-editor] [data-tts-commit-emotion]").click();
-++ await expect(page.locator("[data-tts-status]")).toHaveText("Saved emotion setting: Urgent.");
-++ await expect(page.locator("[data-tts-output-summary]")).toContainText("\"contractVersion\": \"tts-profile-emotion-v1\"");
-++ await expect(page.locator("[data-tts-output-summary]")).toContainText("\"name\": \"Creature Profile Updated\"");
-++
-+ await expect(page.locator("[data-tts-gender-select]")).toBeVisible();
-+ await expect(page.locator("[data-tts-language-select]")).toBeVisible();
-+ await expect(page.locator("[data-tts-age-select]")).toBeVisible();
-+@@ -173,7 +211,7 @@ test("Text To Speech shows actionable error when browser speech synthesis is una
-+ const failures = await openTextToSpeechPage(page, { speechAvailable: false });
-+ try {
-+ await expect(page.getByRole("heading", { level: 1, name: "Text To Speech" })).toBeVisible();
-+- await expect(page.locator("[data-tts-engine-label]")).toHaveText("Unavailable");
-++ await expect(page.locator("[data-tts-profile-count]")).toHaveText("3");
-+ await expect(page.locator("[data-tts-engine-status]")).toContainText("SpeechSynthesis is unavailable");
-+ await expect(page.locator("[data-tts-status]")).toContainText("Use a browser with Web Speech API support");
-+ await expect(page.locator("[data-tts-voice-select]")).toContainText("No browser voices available");
-+diff --git a/tests/tools/Text2SpeechShell.test.mjs b/tests/tools/Text2SpeechShell.test.mjs
-+index fbc6ac8e9..51e4f019f 100644
-+--- a/tests/tools/Text2SpeechShell.test.mjs
-++++ b/tests/tools/Text2SpeechShell.test.mjs
-+@@ -3,9 +3,14 @@ import test from "node:test";
++- Create Project appears only for Ready ideas.
++- Clicking Create Project creates or links a Game Hub project using an existing/shared project contract if available.
++- Clicking Create Project sets Idea status to Project.
++- Project ideas and their notes become read-only.
++- Project row actions are Open in Game Hub and Archive.
++- Project ideas cannot be edited or deleted.
++- Project ideas cannot add, edit, or delete notes.
++- Archived ideas remain hidden by default through existing filter behavior and show Restore and Delete when visible.
++- Game Hub displays creator-facing project data only.
++- Game Hub shows Project Information.
++- Game Hub shows a read-only Source Idea section with Idea, Pitch, and Notes.
++- Game Hub must not show internal IDs, DB/API/mock/debug/seed wording, or implementation details.
++- Open in Game Hub from Idea Board navigates to the related Game Hub project view.
++- If existing project handoff, route, or mock adapter wiring is missing, add the smallest fix needed.
++- Do not expand into Game Journey unless required as a stub/reference for the handoff.
+
-+ import {
-+ TTS_MESSAGE_STATUSES,
-++ TTS_PROFILE_CONTRACT_VERSION,
-+ TTS_PROVIDER_ADAPTER_PLAN,
-++ createDefaultTextToSpeechProfiles,
-+ createEmotionProfile,
-++ createMessageStudioTtsProfileOptions,
-+ createSpeechPreviewRequest,
-++ createTextToSpeechProfile,
-++ createTextToSpeechProfileEmotion,
-+ createTtsMessage,
-+ createVoiceProfile,
-+ previewTtsMessage,
-+@@ -59,3 +64,46 @@ test("Text2Speech provider adapter plan keeps browser speech implemented and pai
-+ assert.equal(TTS_PROVIDER_ADAPTER_PLAN[0].status, "implemented");
-+ assert.ok(TTS_PROVIDER_ADAPTER_PLAN.slice(1).every((provider) => provider.status === "planned"));
- });
--
--@@ -134,42 +134,24 @@ async function closeMessagesRun(failures, page) {
-- }
-- }
--
---async function addEmotionProfile(page, name) {
--- await page.locator("[data-messages-emotion-add-row]").click();
--- await page.locator("[data-messages-emotion-editor='__new__'] [data-emotion-name]").fill(name);
--- await page.locator("[data-messages-emotion-editor='__new__'] [data-emotion-volume]").fill("0.9");
--- await page.locator("[data-messages-emotion-editor='__new__'] [data-emotion-pitch]").fill("0.85");
--- await page.locator("[data-messages-emotion-editor='__new__'] [data-emotion-rate]").fill("0.95");
--- await page.locator("[data-messages-emotion-commit='__new__']").click();
---}
---
---async function addTtsProfile(page, name) {
--- await page.locator("[data-messages-tts-add-row]").click();
--- await page.locator("[data-messages-tts-editor='__new__'] [data-tts-name]").fill(name);
--- await page.locator("[data-messages-tts-editor='__new__'] [data-tts-provider]").fill("browser-speech");
--- await page.locator("[data-messages-tts-editor='__new__'] [data-tts-voice]").fill("Test Voice");
--- await page.locator("[data-messages-tts-editor='__new__'] [data-tts-language]").fill("en-US");
--- await page.locator("[data-messages-tts-commit='__new__']").click();
---}
---
---async function addMessageRow(page, values) {
--- await page.locator("[data-messages-add-row]").click();
--+async function addMessage(page, values) {
--+ await page.getByRole("button", { name: "Add Message" }).click();
-- await page.locator("[data-messages-row-editor='__new__'] [data-message-name]").fill(values.name);
--- await page.locator("[data-messages-row-editor='__new__'] [data-message-emotion]").selectOption({ label: values.emotion });
--+ await page.locator("[data-messages-row-editor-details='__new__'] [data-message-emotion]").selectOption({ label: values.emotion });
-- await page.locator("[data-messages-row-editor-details='__new__'] [data-message-text]").fill(values.text);
-- await page.locator("[data-messages-row-editor-details='__new__'] [data-message-notes]").fill(values.notes || "");
-- await page.locator("[data-messages-commit='__new__']").click();
-- }
--
---async function addSegmentRow(page, values) {
--- await page.locator("[data-messages-segment-add-row]").click();
--+async function addPart(page, values) {
--+ await page.getByRole("button", { name: "Add Part" }).click();
-- await page.locator("[data-messages-segment-editor='__new__'] [data-segment-order]").fill(String(values.order));
--- await page.locator("[data-messages-segment-editor='__new__'] [data-segment-emotion]").selectOption({ label: values.emotion });
-- await page.locator("[data-messages-segment-editor='__new__'] [data-segment-text]").fill(values.text);
--+ await page.locator("[data-messages-segment-editor='__new__'] [data-segment-emotion]").selectOption({ label: values.emotion });
-- await page.locator("[data-messages-segment-commit='__new__']").click();
-- }
--
---test("Message Studio uses table governance, validates rows, and persists through the Local API", async ({ page }) => {
--+test("Message Studio renders Messages with child Message Parts and plays ordered parts", async ({ page }) => {
-- const sqlitePath = messagesDbPath();
-- await fs.rm(sqlitePath, { force: true });
-- const failures = await openMessagesPage(page, sqlitePath);
--@@ -179,242 +161,155 @@ test("Message Studio uses table governance, validates rows, and persists through
-- await expect(page.locator(".tool-workspace")).toBeVisible();
-- await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0);
-- await expect(page.locator("[data-messages-category-name]")).toHaveCount(0);
--- await expect(page.locator("[data-messages-category]")).toHaveCount(0);
--- await expect(page.getByRole("button", { name: /delete/i })).toHaveCount(0);
--- await expect(page.locator("[data-messages-persistence-engine]")).toHaveText("Postgres target");
--- await expect(page.locator("[data-messages-tts-service]")).toHaveValue("browser-speech-synthesis");
--- await expect(page.locator("[data-messages-preview-status]")).toHaveText("Select a message row or segment row before testing speech.");
--- await expect(page.locator("[data-messages-test-speech]")).toBeDisabled();
--- await expect(page.locator("[data-messages-preview-message], [data-messages-preview-segments], [data-messages-preview-stop]")).toHaveCount(0);
---
--- await expect(page.locator("[data-messages-emotions]")).toContainText("Calm");
--- await expect(page.locator("[data-messages-emotions]")).toContainText("Urgent");
--+ await expect(page.locator("[data-messages-tts-add-row]")).toHaveCount(0);
--+ await expect(page.getByRole("columnheader", { name: "Message Name" })).toBeVisible();
--+ await expect(page.getByRole("columnheader", { name: "Default TTS Profile" })).toBeVisible();
-- await expect(page.locator("[data-messages-tts-profiles]")).toContainText("Browser Speech Default");
--+ await expect(page.locator("[data-messages-tts-profiles]")).toContainText("Owned by TTS Studio");
--
--- await addEmotionProfile(page, "Robot");
--- await expect(page.locator("[data-messages-log]")).toHaveText("Updated emotion profile Robot.");
--- await expect(page.locator("[data-messages-emotion-row]").filter({ hasText: "Robot" })).toContainText("0.9");
--- await page.locator("[data-messages-emotion-row]").filter({ hasText: "Robot" }).getByRole("button", { name: "Edit" }).click();
--- await page.locator("[data-messages-emotion-editor] [data-emotion-rate]").fill("1.05");
--- await page.locator("[data-messages-emotion-editor] [data-messages-emotion-commit]").click();
--- await page.locator("[data-messages-emotion-row]").filter({ hasText: "Robot" }).getByRole("button", { name: "Disable" }).click();
--- await expect(page.locator("[data-messages-emotion-row]").filter({ hasText: "Robot" })).toContainText("Inactive");
---
--- await addTtsProfile(page, "Arcade Browser Voice");
--- await expect(page.locator("[data-messages-log]")).toHaveText("Updated TTS profile Arcade Browser Voice.");
--- await expect(page.locator("[data-messages-tts-profiles]")).toContainText("Arcade Browser Voice");
--- await expect(page.locator("[data-messages-tts-count]")).toHaveText("3");
--- await page.locator("[data-messages-tts-row]").filter({ hasText: "Arcade Browser Voice" }).getByRole("button", { name: "Edit" }).click();
--- await page.locator("[data-messages-tts-editor] [data-tts-language]").fill("en-GB");
--- await page.locator("[data-messages-tts-editor] [data-messages-tts-commit]").click();
--- await expect(page.locator("[data-messages-tts-row]").filter({ hasText: "Arcade Browser Voice" })).toContainText("Active");
---
--- const ttsProfilesResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/tts-profiles`);
--- expect(ttsProfilesResult.response.ok).toBe(true);
--- expect(ttsProfilesResult.payload.ok).toBe(true);
--- const createdTtsProfile = ttsProfilesResult.payload.data.ttsProfiles.find((profile) => profile.name === "Arcade Browser Voice");
--- expect(createdTtsProfile).toEqual(expect.objectContaining({
--- active: true,
--- language: "en-GB",
--- providerKey: "browser-speech",
--- voiceName: "Test Voice",
--- }));
--- expect(createdTtsProfile.key).toMatch(ULID_PATTERN);
--- expect(createdTtsProfile.createdBy).toMatch(ULID_PATTERN);
--- expect(createdTtsProfile.updatedBy).toMatch(ULID_PATTERN);
---
--- await page.locator("[data-messages-add-row]").click();
--- await expect(page.locator("[data-messages-row-editor='__new__']")).toBeVisible();
--+ await page.getByRole("button", { name: "Add Message" }).click();
-- await page.locator("[data-messages-commit='__new__']").click();
-- await expect(page.locator("[data-messages-validation-card]")).toBeVisible();
-- await expect(page.locator("[data-messages-validation-errors]")).toContainText("Message Name is required.");
-- await expect(page.locator("[data-messages-validation-errors]")).toContainText("Emotion Profile is required.");
-- await expect(page.locator("[data-messages-validation-errors]")).toContainText("Message Text is required.");
---
-- await page.locator("[data-messages-cancel='__new__']").click();
--- await addMessageRow(page, {
--+
--+ await addMessage(page, {
-- emotion: "Urgent",
--- name: "Forest Warning",
--- notes: "Opening forest danger line.",
--- text: "The forest gets darker beyond this point.\nWe are being attacked by bats.",
--+ name: "Bat Encounter",
--+ notes: "Opening combat line.",
--+ text: "Bats drop from the rafters.",
-- });
--- await expect(page.locator("[data-messages-log]")).toHaveText("Updated row Forest Warning.");
--- await expect(page.locator("[data-messages-count]")).toHaveText("1");
--- await expect(page.locator("[data-messages-table]")).toContainText("Forest Warning");
--- await expect(page.locator("[data-messages-selected-text]")).toHaveText("The forest gets darker beyond this point.\nWe are being attacked by bats.");
--+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated row Bat Encounter.");
--+ await expect(page.locator("[data-messages-table]")).toContainText("Bat Encounter");
--+ await expect(page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" })).toContainText("Dialog");
--+ await expect(page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" })).toContainText("0");
--+ await expect(page.locator("[data-message-default-tts-profile]").first()).toHaveValue(/.+/);
--+
--+ const messageRow = page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" });
--+ await messageRow.click();
-- await expect(page.locator("[data-messages-segment-host]")).toBeVisible();
--+ await expect(page.getByRole("heading", { name: "Message Parts" })).toBeVisible();
--+ await expect(page.getByRole("columnheader", { name: "Order" })).toBeVisible();
--+ await expect(page.getByRole("columnheader", { name: "Text" })).toBeVisible();
--+ await expect(page.getByRole("columnheader", { exact: true, name: "TTS Profile" })).toBeVisible();
--
--- const listResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/messages`);
--- expect(listResult.response.ok).toBe(true);
--- expect(listResult.payload.ok).toBe(true);
--- const createdMessage = listResult.payload.data.messages.find((message) => message.name === "Forest Warning");
--- expect(createdMessage).toEqual(expect.objectContaining({
--- active: true,
--- emotionProfileName: "Urgent",
--- messageText: "The forest gets darker beyond this point.\nWe are being attacked by bats.",
--- notes: "Opening forest danger line.",
--- }));
--- expect(createdMessage.key).toMatch(ULID_PATTERN);
--- expect(createdMessage.createdBy).toMatch(ULID_PATTERN);
--- expect(createdMessage.updatedBy).toMatch(ULID_PATTERN);
---
--- await page.locator("[data-messages-segment-add-row]").click();
--+ await page.getByRole("button", { name: "Add Part" }).click();
-- await page.locator("[data-messages-segment-editor='__new__'] [data-segment-order]").fill("");
-- await page.locator("[data-messages-segment-commit='__new__']").click();
--- await expect(page.locator("[data-messages-validation-card]")).toBeVisible();
--- await expect(page.locator("[data-messages-validation-errors]")).toContainText("Segment Text is required.");
--+ await expect(page.locator("[data-messages-validation-errors]")).toContainText("Part Text is required.");
-- await expect(page.locator("[data-messages-validation-errors]")).toContainText("Emotion Profile is required.");
-- await expect(page.locator("[data-messages-validation-errors]")).toContainText("Display Order is required.");
-- await page.locator("[data-messages-segment-cancel='__new__']").click();
--
--- await addSegmentRow(page, {
--+ await addPart(page, {
-- emotion: "Calm",
-- order: 1,
--- text: "The forest gets darker beyond this point.",
--+ text: "Bats drop from the rafters.",
-- });
--- await expect(page.locator("[data-messages-log]")).toHaveText("Updated segment row 1.");
--- await addSegmentRow(page, {
--+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated message part 1.");
--+ await addPart(page, {
-- emotion: "Urgent",
-- order: 2,
--- text: "We are being attacked by bats.",
--+ text: "Keep your torch high.",
-- });
--- await expect(page.locator("[data-messages-log]")).toHaveText("Updated segment row 2.");
--+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated message part 2.");
-- await expect(page.locator("[data-messages-segment-row]")).toHaveCount(2);
--+ await expect(page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" })).toContainText("2");
--
--+ await page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" }).getByRole("button", { name: "Play Message" }).click();
--+ await expect(page.locator("[data-messages-log]")).toHaveText("Play Message queued 2 parts for Bat Encounter.");
-- let speechCalls = await page.evaluate(() => window.__messagesSpeechCalls);
--- expect(speechCalls).toEqual([]);
---
--- await page.locator("[data-messages-preview-tts-profile]").selectOption({ label: "Arcade Browser Voice" });
--- await page.locator("[data-messages-row]").filter({ hasText: "Forest Warning" }).click();
--- await expect(page.locator("[data-messages-speech-test-target]")).toHaveText("Message Row: Forest Warning");
--- await expect(page.locator("[data-messages-test-speech]")).toBeEnabled();
--- await page.locator("[data-messages-test-speech]").click();
--- await expect(page.locator("[data-messages-preview-status]")).toHaveText("Speech test started for Message Row: Forest Warning using Browser Speech Synthesis.");
--- speechCalls = await page.evaluate(() => window.__messagesSpeechCalls);
--+ expect(speechCalls.slice(-2).map((call) => call.text)).toEqual([
--+ "Bats drop from the rafters.",
--+ "Keep your torch high.",
--+ ]);
-- expect(speechCalls.at(-1)).toEqual(expect.objectContaining({
--- lang: "en-GB",
--- pitch: 1.08,
--- rate: 1.15,
--- text: "The forest gets darker beyond this point.\nWe are being attacked by bats.",
--+ lang: "en-US",
-- type: "speak",
-- voiceName: "Test Voice",
--- volume: 1,
-- }));
--
--- await page.locator("[data-messages-segment-row]").filter({ hasText: "The forest gets darker beyond this point." }).click();
--- await expect(page.locator("[data-messages-speech-test-target]")).toHaveText("Segment 1");
--- await page.locator("[data-messages-test-speech]").click();
--- await expect(page.locator("[data-messages-preview-status]")).toHaveText("Speech test started for Segment 1 using Browser Speech Synthesis.");
--+ await page.locator("[data-messages-segment-row]").filter({ hasText: "Keep your torch high." }).getByRole("button", { name: "Play Part" }).click();
--+ await expect(page.locator("[data-messages-log]")).toHaveText("Play Part queued Part 2 using Browser Speech Default.");
-- speechCalls = await page.evaluate(() => window.__messagesSpeechCalls);
-- expect(speechCalls.at(-1)).toEqual(expect.objectContaining({
--- lang: "en-GB",
--- pitch: 1,
--- rate: 1,
--- text: "The forest gets darker beyond this point.",
--+ text: "Keep your torch high.",
-- type: "speak",
-- voiceName: "Test Voice",
--- volume: 1,
-- }));
--
--+ await page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" }).getByRole("button", { name: "Edit Message" }).click();
--+ await page.locator("[data-messages-row-editor] [data-message-name]").fill("Bat Encounter Updated");
--+ await page.locator("[data-messages-row-editor] [data-messages-commit]").click();
--+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated row Bat Encounter Updated.");
--+
--+ await page.locator("[data-messages-segment-row]").filter({ hasText: "Keep your torch high." }).getByRole("button", { name: "Edit Part" }).click();
--+ await page.locator("[data-messages-segment-editor] [data-segment-text]").fill("Keep your torch high and shield raised.");
--+ await page.locator("[data-messages-segment-editor] [data-messages-segment-commit]").click();
--+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated message part 2.");
--+
--+ const listResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/messages`);
--+ expect(listResult.response.ok).toBe(true);
--+ expect(listResult.payload.ok).toBe(true);
--+ const createdMessage = listResult.payload.data.messages.find((message) => message.name === "Bat Encounter Updated");
--+ expect(createdMessage).toEqual(expect.objectContaining({
--+ active: true,
--+ categoryName: "Dialog",
--+ emotionProfileName: "Urgent",
--+ messageText: "Bats drop from the rafters.",
--+ notes: "Opening combat line.",
--+ }));
--+ expect(createdMessage.key).toMatch(ULID_PATTERN);
--+
-- const segmentsResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/segments`);
-- expect(segmentsResult.response.ok).toBe(true);
--- expect(segmentsResult.payload.ok).toBe(true);
-- const createdSegments = segmentsResult.payload.data.segments.filter((segment) => segment.messageKey === createdMessage.key);
--- expect(createdSegments).toHaveLength(2);
-- expect(createdSegments.map((segment) => segment.displayOrder)).toEqual([1, 2]);
--+ expect(createdSegments.map((segment) => segment.segmentText)).toEqual([
--+ "Bats drop from the rafters.",
--+ "Keep your torch high and shield raised.",
--+ ]);
-- createdSegments.forEach((segment) => {
-- expect(segment.key).toMatch(ULID_PATTERN);
-- expect(segment.createdBy).toMatch(ULID_PATTERN);
-- expect(segment.updatedBy).toMatch(ULID_PATTERN);
-- });
--
--- await page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats." }).getByRole("button", { name: "Move Up" }).click();
--- await expect(page.locator("[data-messages-log]")).toHaveText("Segment order updated.");
--- await expect(page.locator("[data-messages-segment-row]").first()).toContainText("We are being attacked by bats.");
---
--- await page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats." }).getByRole("button", { name: "Edit" }).click();
--- await page.locator("[data-messages-segment-editor] [data-segment-text]").fill("We are being attacked by bats right now.");
--- await page.locator("[data-messages-segment-editor] [data-messages-segment-commit]").click();
--- await expect(page.locator("[data-messages-log]")).toHaveText("Updated segment row 1.");
--- await expect(page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats right now." })).toBeVisible();
---
--- await page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats right now." }).getByRole("button", { name: "Disable" }).click();
--- await expect(page.locator("[data-messages-log]")).toHaveText("Disabled segment row 1.");
--- await expect(page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats right now." })).toContainText("Inactive");
---
--- const updatedSegmentsResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/segments`);
--- const disabledSegment = updatedSegmentsResult.payload.data.segments.find((segment) => segment.segmentText === "We are being attacked by bats right now.");
--- expect(disabledSegment).toEqual(expect.objectContaining({
--- active: false,
--- displayOrder: 1,
--- emotionProfileName: "Urgent",
--- messageKey: createdMessage.key,
--- messageName: "Forest Warning",
--- segmentText: "We are being attacked by bats right now.",
--- }));
---
--- await page.locator("[data-messages-row]").filter({ hasText: "Forest Warning" }).getByRole("button", { name: "Edit" }).click();
--- await page.locator("[data-messages-row-editor] [data-message-name]").fill("Forest Warning Updated");
--- await page.locator("[data-messages-row-editor-details] [data-message-text]").fill("The forest gets darker beyond this point.");
--- await page.locator("[data-messages-row-editor] [data-messages-commit]").click();
--- await expect(page.locator("[data-messages-log]")).toHaveText("Updated row Forest Warning Updated.");
--- await expect(page.locator("[data-messages-table]")).toContainText("Forest Warning Updated");
---
--- await page.locator("[data-messages-row]").filter({ hasText: "Forest Warning Updated" }).getByRole("button", { name: "Disable" }).click();
--- await expect(page.locator("[data-messages-log]")).toHaveText("Disabled row Forest Warning Updated.");
--- await expect(page.locator("[data-messages-row]").filter({ hasText: "Forest Warning Updated" })).toContainText("Inactive");
---
--- const updateResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/messages/${createdMessage.key}`);
--- expect(updateResult.payload.data.message).toEqual(expect.objectContaining({
--- active: false,
--- key: createdMessage.key,
--- messageText: "The forest gets darker beyond this point.",
--- name: "Forest Warning Updated",
--- }));
---
--- await page.locator("[data-messages-tts-row]").filter({ hasText: "Arcade Browser Voice" }).getByRole("button", { name: "Disable" }).click();
--- await expect(page.locator("[data-messages-log]")).toHaveText("Disabled TTS profile Arcade Browser Voice.");
--- await expect(page.locator("[data-messages-tts-row]").filter({ hasText: "Arcade Browser Voice" })).toContainText("Inactive");
---
--- for (const url of [
--- `${failures.server.baseUrl}/api/messages/messages/${createdMessage.key}`,
--- `${failures.server.baseUrl}/api/messages/segments/${disabledSegment.key}`,
--- `${failures.server.baseUrl}/api/messages/tts-profiles/${createdTtsProfile.key}`,
--- ]) {
--- const deleteResult = await fetch(url, { method: "DELETE" });
--- expect(deleteResult.status).toBe(404);
--- }
--+ expect(failures.failedRequests).toEqual([]);
--+ expect(failures.pageErrors).toEqual([]);
--+ expect(failures.consoleErrors).toEqual([]);
--+ } finally {
--+ await closeMessagesRun(failures, page);
--+ await fs.rm(sqlitePath, { force: true });
--+ }
-++
-++test("Text2Speech profile contract exposes Message Studio compatible profile options", () => {
-++ const voiceOptions = [{ language: "en-US", label: "Test Voice (en-US)", name: "Test Voice", value: "test-voice" }];
-++ const defaults = createDefaultTextToSpeechProfiles(voiceOptions);
-++ const custom = createTextToSpeechProfile({
-++ emotions: [
-++ createTextToSpeechProfileEmotion({
-++ emotion: "urgent",
-++ pitch: 1.2,
-++ rate: 1.1,
-++ ssmlLikePreset: "whisper-ish",
-++ volume: 0.8,
-++ }),
-++ ],
-++ id: "custom-profile",
-++ name: "Custom Profile",
-++ voice: "test-voice",
-++ voiceName: "Test Voice",
-++ });
-++ const options = createMessageStudioTtsProfileOptions([custom]);
-++
-++ assert.equal(TTS_PROFILE_CONTRACT_VERSION, "tts-profile-emotion-v1");
-++ assert.equal(defaults[0].name, "Default Balanced Profile");
-++ assert.equal(defaults[0].messageStudioUsageCount, 1);
-++ assert.equal(defaults[0].emotions[0].emotionLabel, "Neutral");
-++ assert.equal(defaults[0].emotions[0].messagePartsUsageCount, 1);
-++ assert.deepEqual(options, [{
-++ active: true,
-++ emotionSettings: [{
-++ emotion: "urgent",
-++ emotionLabel: "Urgent",
-++ pitch: 1.2,
-++ rate: 1.1,
-++ ssmlLikePreset: "whisper-ish",
-++ volume: 0.8,
-++ }],
-++ key: "custom-profile",
-++ language: "en-US",
-++ name: "Custom Profile",
-++ providerKey: "browser-speech",
-++ voiceName: "Test Voice",
-++ }]);
- +});
--
--- await failures.server.close();
--- const restartedServer = await startRepoServer();
--- failures.server = restartedServer;
--- const persistedResult = await jsonRequest(`${restartedServer.baseUrl}/api/messages/messages/${createdMessage.key}`);
--- expect(persistedResult.response.ok).toBe(true);
--- expect(persistedResult.payload.data.message).toEqual(expect.objectContaining({
--- active: false,
--- key: createdMessage.key,
--- name: "Forest Warning Updated",
--- messageText: "The forest gets darker beyond this point.",
--- }));
--+test("Message Studio shows actionable playback error when audio engine is unavailable", async ({ page }) => {
--+ const sqlitePath = messagesDbPath();
--+ await fs.rm(sqlitePath, { force: true });
--+ const failures = await openMessagesPage(page, sqlitePath, { speechAvailable: false });
--
--- const persistedSegmentResult = await jsonRequest(`${restartedServer.baseUrl}/api/messages/segments/${disabledSegment.key}`);
--- expect(persistedSegmentResult.response.ok).toBe(true);
--- expect(persistedSegmentResult.payload.data.segment).toEqual(expect.objectContaining({
--- active: false,
--- displayOrder: 1,
--- key: disabledSegment.key,
--- segmentText: "We are being attacked by bats right now.",
--- }));
--+ try {
--+ await addMessage(page, {
--+ emotion: "Urgent",
--+ name: "Bat Encounter",
--+ text: "Bats drop from the rafters.",
--+ });
--+ await page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" }).click();
--+ await addPart(page, {
--+ emotion: "Urgent",
--+ order: 1,
--+ text: "Bats drop from the rafters.",
--+ });
--
--- const persistedTtsProfileResult = await jsonRequest(`${restartedServer.baseUrl}/api/messages/tts-profiles/${createdTtsProfile.key}`);
--- expect(persistedTtsProfileResult.response.ok).toBe(true);
--- expect(persistedTtsProfileResult.payload.data.ttsProfile).toEqual(expect.objectContaining({
--- active: false,
--- key: createdTtsProfile.key,
--- name: "Arcade Browser Voice",
--- providerKey: "browser-speech",
--- voiceName: "Test Voice",
--- }));
--+ await page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" }).getByRole("button", { name: "Play Message" }).click();
--+ await expect(page.locator("[data-messages-validation-card]")).toBeVisible();
--+ await expect(page.locator("[data-messages-validation-errors]")).toContainText("Audio engine is unavailable. Use a browser with SpeechSynthesis support and reload Message Studio.");
--+ await expect(page.locator("[data-messages-log]")).toHaveText("Audio engine is unavailable. Use a browser with SpeechSynthesis support and reload Message Studio.");
--+ await expect(page.locator("[data-messages-preview-status]")).toHaveText("Audio engine is unavailable. Use a browser with SpeechSynthesis support and reload Message Studio.");
--+ expect(await page.evaluate(() => window.__messagesSpeechCalls)).toEqual([]);
--
-- expect(failures.failedRequests).toEqual([]);
-- expect(failures.pageErrors).toEqual([]);
--diff --git a/toolbox/messages/index.html b/toolbox/messages/index.html
--index 2f1945bbe..5df9a0290 100644
----- a/toolbox/messages/index.html
--+++ b/toolbox/messages/index.html
--@@ -39,7 +39,7 @@
-- Review existing rows, select one to update, or add a row from the Message Rows table. Review messages, open a message to manage parts, or add a message from the Messages table. Disable rows instead of deleting game text.Row Workflow
--
Message rows define what is said. Emotion profiles and TTS profiles define future delivery settings only.
--+Messages own game text and ordered parts. Audio playback is delegated to the audio engine.
--| Name | ---Primary Emotion | ---Segments | ---Tags | --+Message Name | --+Type | --Status | --+Parts | --+Default TTS Profile | --Actions | --
|---|
Message row needs required fields.
--@@ -118,7 +118,7 @@ -+Name: None
--Emotion Profile: None
---Segment: None
--+Part: None
--Status: None
--Text:
--No message selected.--@@ -184,7 +181,7 @@ --
Message segments are stored as ordered text with emotion profiles.
--+Message parts are stored as ordered text with emotion profiles.
--This tool stores message text exactly as entered.
--| Profile Name | -++Voice | -++Language | -++Gender | -++Age | -++Emotion Count | -++Status | -++Actions | -++
|---|---|---|---|---|---|---|---|
| Loading TTS profiles. | |||||||