Phase 13: Web UI Expansion¶
Implementation note: This phase depends on Phases 01-12 being complete. The web UI (Phase 07) currently provides basic gameplay — menu, turn execution with streaming, and slash commands typed as text. This phase closes the gap with the terminal CLI and adds web-native features that the terminal cannot offer. NiceGUI remains the framework. All new features must use the existing engine APIs (
run_turn,save_manager,git_save,creator) — no engine changes unless explicitly noted.
Overview¶
The terminal CLI (Phase 04) provides a rich set of features beyond basic gameplay: quick-action commands, character memory inspection, save versioning with undo/history/diff, game creation, playtest launching, and diagnostics viewing. The web UI currently requires users to type slash commands for all of these, has no visual game state display, and is missing several features entirely (game creation, playtesting, diagnostics, settings).
This phase expands the web UI across 9 steps (starting with a refactoring step):
| Step | File | Summary | Depends On |
|---|---|---|---|
| 00 | 00-Refactoring.md | MUST BE FIRST. Split god classes, extract shared commands, modularize components | — |
| 01 | 01-GameplayToolbar.md | Header toolbar with quick-action buttons, turn info display, input improvements | 00 |
| 02 | 02-GameStateSidebar.md | Collapsible sidebar: character cards, chapter progress, memory viewer | 00, 01 |
| 03 | 03-SaveManagementAndHistory.md | Enhanced saves table, visual save-as dialog, turn history timeline, peek/diff viewer | 00, 01 |
| 04 | 04-GameCreationWizard.md | Multi-step game creation wizard using the creator agent | 00 |
| 05 | 05-SettingsAndConfiguration.md | Settings page for LLM config, model selection, display preferences | 00 |
| 06 | 06-PlaytestDashboard.md | Launch, monitor, and review playtests from the web | 00, 05 |
| 07 | 07-DiagnosticsViewer.md | Call log viewer, token usage charts, error browser, diagnostics file viewer | 00, 06 |
| 08 | 08-PolishAndSafety.md | Multi-tab locking, refresh recovery, mobile layout, keyboard shortcuts, error retry | 00-07 |
File Layout¶
After Step 00 refactoring, the web UI has a modular architecture. Steps 01-08 add to this structure.
src/theact/commands/ # Shared command logic (Step 00)
__init__.py
logic.py # Pure functions, no UI imports
types.py # CommandResult dataclass
src/theact/web/
__init__.py # start_web() — unchanged
__main__.py # CLI args — unchanged
styles.py # Colors — unchanged
# Core architecture (Step 00)
app.py # Slim: page routing only
menu.py # MenuBuilder class
session.py # GameplaySession — thin orchestrator
state.py # GameSessionState — shared observable state
turn_runner.py # TurnRunner — wraps run_turn()
streaming.py # StreamRenderer — routes tokens to UI
command_router.py # CommandRouter — dispatches commands
commands.py # Thin web rendering layer over commands/logic
# Components package (Step 00)
components/
__init__.py # Re-exports for backward compat
turn_card.py # Turn card creation and info bar (Step 01 adds info bar)
message_blocks.py # StreamingTextBlock, narrator/character/player blocks
thinking_panel.py # Thinking panel component
system_message.py # System message card
static_turn.py # render_static_turn() for past turns
dialogs.py # Reusable confirmation/input dialogs
html_utils.py # HTML escaping, tables, render_result()
# Feature modules (Steps 01-08)
toolbar.py # Step 01: quick-action button bar
sidebar.py # Step 02: game state sidebar panel
history.py # Step 03: turn history browser
creator_wizard.py # Step 04: game creation wizard page
settings.py # Step 05: settings page
playtest_dashboard.py # Step 06: playtest launcher and report viewer
diagnostics_viewer.py # Step 07: call log and diagnostics browser
safety.py # Step 08: file locking, session recovery
tests/
test_command_logic.py # Step 00: shared command logic tests
web/
test_toolbar.py # Step 01
test_sidebar.py # Step 02
test_history.py # Step 03
test_creator_wizard.py # Step 04
test_settings.py # Step 05
test_playtest_dashboard.py # Step 06
test_diagnostics.py # Step 07
test_safety.py # Step 08
What This Phase Does NOT Do¶
- No engine changes. All new features call existing APIs (
run_turn,save_manager,git_save,creator,playtest). - No new LLM agents. The game creation wizard and playtest dashboard call existing agent code.
- No changes to the terminal CLI. The CLI is unaffected by this phase.
- No changes to game file formats. YAML structures remain identical.
- No authentication or multi-user support. The web UI remains single-user, local-first.
Architecture Principles¶
- Thin UI layer. Web components call existing Python APIs. No business logic in the web layer.
- Shared observable state.
GameSessionState(fromstate.py) is the single source of truth. All components (sidebar, toolbar, history) read from it and register listeners. No stale references. - Modular components. Each component type lives in its own file under
components/. Reusable utilities (dialogs.py,html_utils.py) prevent duplication. - Shared command logic.
commands/logic.pycontains pure functions with no UI imports. Both CLI and web are thin rendering layers over this shared logic. - Session as orchestrator.
GameplaySessiondelegates toTurnRunner,StreamRenderer, andCommandRouter. It does not contain business logic. - NiceGUI patterns. Use
ui.refreshablefor reactive updates,ui.timerfor polling,app.storage.tabfor session persistence. - Progressive enhancement. Each step adds independently useful features. The web UI remains functional if any step is incomplete.
Build Order Recommendation¶
Step 00 (refactoring) MUST come first. It restructures the codebase so all subsequent steps work cleanly.
Then: Steps 01-03 form the core gameplay improvements and should be built in order. Steps 04 and 05 are independent and can be parallelized. Steps 06-07 benefit from Step 05. Step 08 is a polish pass that should come last.
Execution Guidance for Implementing Agents¶
Playwright Validation Workflow¶
After implementing each step (or significant sub-feature), the executor agent MUST manually validate the result using the Playwright MCP browser tools. The workflow is:
- Start the dev server if not already running:
uv run scripts/dev_server.py start --port 8111 - Navigate to the relevant page using
browser_navigatetohttp://localhost:8111 - Take a screenshot with
browser_take_screenshotto visually verify the UI - Interact with the new feature using
browser_click,browser_fill_form,browser_snapshot, etc. - Verify behavior — check that buttons work, dialogs open, data displays correctly
- Document issues — if anything looks wrong, fix it before moving on
This is not optional. NiceGUI's rendering of Quasar components has quirks (spacing, dark mode colors, icon rendering) that are only visible in the browser. Screenshot verification catches layout issues, missing styles, and broken interactions that unit tests cannot.
Convert Manual Testing to Regression Tests¶
Every issue discovered during Playwright manual validation MUST be captured as a test:
- During manual testing, when you discover that something doesn't work or looks wrong, fix it.
- After fixing, write a Playwright browser test that would have caught the issue. Add it to the step's test file (e.g.,
tests/web/test_toolbar.py). - Also consider: are there similar issues that could occur in related components? Write preventive tests for those too.
- Test pattern: use
data-testidattributes on key elements for stable Playwright selectors. The existing tests intests/web/test_gameplay.pyshow the pattern.
The goal is that manual Playwright validation drives the creation of automated regression tests, so the same class of issue never recurs. Each step should end with more tests than the plan originally specified.
Subagent Communication Protocol¶
When the executor agent delegates work to subagents (e.g., "implement toolbar.py", "write test file"), the executor MUST:
-
Ask subagents to report back ideas and concerns. Include this instruction in every subagent prompt:
"After completing your implementation, report back: - Any concerns about the approach or potential issues you noticed - Ideas for improvements or alternatives you considered - Any assumptions you made that the main agent should verify - Edge cases or error scenarios you encountered but didn't handle"
-
Review subagent reports before proceeding. The executor should read the concerns/ideas and decide whether to act on them, adjust the plan, or note them for later steps.
-
Do not ignore subagent feedback. If a subagent raises a valid concern (e.g., "NiceGUI's
ui.menudoesn't anchor properly toui.input"), the executor should investigate and potentially adjust the implementation rather than blindly merging. -
Subagents should not silently swallow problems. If a subagent encounters an API that doesn't exist, a NiceGUI component that behaves differently than expected, or a data model field with a different name, they must report it — not guess or work around it silently.
Per-Step Implementation Checklist¶
For each step, the executor agent should follow this sequence:
- Read the step plan file and all referenced source files
- Create a task list from the plan's sections
- Implement (directly or via subagents with the communication protocol above)
- Run existing tests:
uv run pytest tests/ -v(catch regressions early) - Manually validate with Playwright MCP (screenshot + interaction)
- Convert any manual findings into automated tests
- Run the full test suite including new tests
- Run
uv run prek run --all-filesfor lint/format - Commit the step's changes with a descriptive message
Reusable Context for Implementing Agents¶
When implementing any step, the agent should first read: 1. CLAUDE.md — project constraints (tiny prompts, one task per call, YAML everywhere) 2. docs/plans/README.md — cross-cutting notes 3. This file (docs/plans/13-WebUIExpansion/README.md) — phase overview 4. The specific step file (e.g., 01-GameplayToolbar.md) 5. Current web UI code: src/theact/web/ (all files) 6. Current web tests: tests/web/ (all files)
Key APIs the Web UI Calls¶
Turn engine (src/theact/engine/turn.py):
async def run_turn(
game: LoadedGame, player_input: str, llm_config: LLMConfig,
on_token: StreamCallback | None = None,
call_log: LLMCallLog | None = None, debug: bool = False,
) -> TurnResult
Save manager (src/theact/io/save_manager.py):
def list_games(games_dir=None) -> list[GameMeta]
def list_saves(saves_dir=None) -> list[dict] # {id, game_title, turn, last_modified}
def create_save(game_id, save_id, player_name, ...) -> Path
def load_save(save_id, saves_dir=None) -> LoadedGame
Git versioning (src/theact/versioning/git_save.py):
def undo(save_path, steps=1) -> int # returns new turn number
def get_history(save_path) -> list[TurnInfo] # {turn, commit_hash, message, timestamp}
def save_as(save_path, new_save_id, saves_dir=None) -> Path
def peek_at_turn(save_path, turn_number) -> dict[str, str] # filename -> contents
def diff_turns(save_path, turn_a, turn_b) -> str # unified diff
Game creation (src/theact/creator/session.py):
Playtest (src/theact/playtest/runner.py):
class PlaytestRunner:
def __init__(self, config: PlaytestConfig)
async def run(self) -> PlaytestReport
LLM call logging (src/theact/llm/call_log.py):
class LLMCallLog:
def summary() -> dict
def agent_summary() -> dict[str, dict]
def records_for_turn(turn) -> list[LLMCallRecord]
Key Data Models¶
LoadedGame (the central in-memory state):
meta: GameMeta # id, title, description, characters list, chapters list
world: World # setting, tone, rules
characters: dict[str, Character] # keyed by file stem
chapters: dict[str, Chapter] # keyed by chapter id
state: GameState # player_name, current_chapter, turn, beats_hit, flags, rolling_summary
conversation: list[ConversationEntry] # turn, role, character, content
memories: dict[str, CharacterMemory] # keyed by file stem: character, summary, key_facts
chapter_summaries: list[ChapterSummary]
save_path: Path
TurnResult (returned by run_turn):
turn: int
narrator: NarratorOutput # narration, responding_characters, mood
characters: list[CharacterResponse] # character, response, thinking
memory_diffs: list[MemoryDiff] # character, old/new summary, old/new facts
game_state: GameStateResult | None # beats_hit, completed, reasoning
chapter_advanced: bool
new_chapter: str | None
summary_updated: bool