21 Komitmen 2f110357bf ... 63481a9a4c

Pembuat SHA1 Pesan Tanggal
  zhenyu.hu 63481a9a4c docs: design balanced ChatAgent event selection 2 minggu lalu
  zhenyu.hu 32c0f1357e docs: record UI workflow observations 2 minggu lalu
  zhenyu.hu df0e547c11 docs: record professional console merge 2 minggu lalu
  zhenyu.hu 8bec5bc095 docs: close professional console todo 2 minggu lalu
  zhenyu.hu a10beb4eb4 docs: record console verification state 2 minggu lalu
  zhenyu.hu 83c711d23a test: cover EventPrompt identity in both orders 2 minggu lalu
  zhenyu.hu 16608fa0ed docs: plan order-independent EventPrompt identity test 2 minggu lalu
  zhenyu.hu 2dd0c66c35 test: distinguish managed EventPrompt payload 2 minggu lalu
  zhenyu.hu ea21eb1d45 docs: plan managed EventPrompt test identity 2 minggu lalu
  zhenyu.hu 781361cb2b fix: enforce EventPrompt payload singleton 2 minggu lalu
  zhenyu.hu c343337410 docs: plan EventPrompt payload singleton 2 minggu lalu
  zhenyu.hu c1d7f044fd docs: close console interaction follow-up 2 minggu lalu
  zhenyu.hu b9fe38aa3f feat: persist debug console presentation state 2 minggu lalu
  zhenyu.hu 7b5b0fbb31 docs: plan console interaction state 2 minggu lalu
  zhenyu.hu fd9c31eb39 feat: redesign the debug console layout 2 minggu lalu
  zhenyu.hu 75fc2ef0f6 docs: close EventPrompt ordering follow-up 2 minggu lalu
  zhenyu.hu c4c02c0759 fix: preserve EventPrompt ordering 2 minggu lalu
  zhenyu.hu bbedaa2c94 docs: plan EventPrompt order preservation 2 minggu lalu
  zhenyu.hu cf481ab2a6 fix: synchronize EventPrompt with tool mode 2 minggu lalu
  zhenyu.hu c4040be75b docs: plan professional debug console 2 minggu lalu
  zhenyu.hu 8dc3658150 docs: design professional debug console 2 minggu lalu

+ 12 - 4
README.md

@@ -95,10 +95,18 @@ Do not commit secrets. Start the app with:
 uv run uvicorn agent_lab.main:app --reload
 ```
 
-Open `http://127.0.0.1:8000`. The UI can select either invocation mode, enable
-tools/events, configure ChatAgent and applicable EventAgent settings, set event
-loop/concurrency/batch limits, create or restore sessions, replay messages and
-audit data, and inspect per-call, per-turn, and per-session usage.
+Open `http://127.0.0.1:8000`. The professional console keeps sessions and tools
+in the left control rail, gives the conversation the primary workspace, anchors
+round metrics and the composer at the bottom, and places Audit Replay plus usage
+in a collapsible right debug drawer. Control-section and drawer preferences are
+browser-only presentation state and are not included in requests or snapshots.
+
+In `dual_agent`, the read-only EventPrompt is generated from the selected tools
+and appears exactly once in the Chat Agent Prompt List. In `chat_agent_tools`,
+EventPrompt is hidden and excluded from `system_prompts`; switching modes or
+restoring a session/snapshot regenerates the correct state. EventAgent model
+settings are unavailable in direct mode, while shared loop, concurrency, and
+timeout controls remain active.
 
 ## Benchmark CLI
 

+ 188 - 0
docs/plans/2026-07-14-professional-debug-console-ui-design.md

@@ -0,0 +1,188 @@
+# Professional Debug Console UI Design
+
+**Status:** approved for implementation
+
+**Date:** 2026-07-14
+
+## Goal
+
+Refactor the existing static Agent Lab page into a clearer professional debugging console while preserving its current FastAPI, WebSocket, session, tool, prompt, audit, and usage contracts.
+
+The same change fixes EventPrompt state drift:
+
+- in `dual_agent` mode, EventPrompt is visible, read-only, unique, and derived from the currently selected tools;
+- in `chat_agent_tools` mode, EventPrompt is hidden and excluded from outbound `system_prompts`;
+- mode changes, tool changes, session restores, and workspace snapshot restores all produce the same deterministic UI state.
+
+The work is limited to the existing static frontend and focused regression tests. It does not introduce a frontend framework, bundler, backend protocol revision, or new runtime capability.
+
+## Chosen Approach
+
+Keep the current server-rendered static entrypoint and refactor `index.html`, `styles.css`, and `app.js` in place.
+
+This is preferred over introducing React/Vue because the page is an experimental service console rather than a product frontend. A framework migration would add build tooling, dependency cost, and regression surface without improving the core experiment. A CSS-only reskin is also insufficient because the requested behavior needs layout state, drawer state, collapsible controls, and one authoritative EventPrompt synchronization path.
+
+## Information Architecture
+
+### Top status bar
+
+The top bar contains:
+
+- Agent Lab identity;
+- current session identity;
+- connection state;
+- current invocation-mode badge;
+- the control that opens or closes the Audit/Usage drawer.
+
+Connection and mode information remain visible without consuming the chat workspace.
+
+### Left control rail
+
+The left rail groups controls into collapsible sections:
+
+- **Session** — expanded by default;
+- **Tools** — expanded by default;
+- **Agent Config** — collapsed by default;
+- **Snapshot** — collapsed by default.
+
+Collapse state is persisted in browser storage. Existing control IDs and request behavior are preserved where practical so the change remains presentation-focused.
+
+Tools are rendered as compact selectable rows/cards with a selected count and clear All/None actions. Tool selection remains available in both invocation modes because the selected set controls either the EventAgent catalog or ChatAgent provider tools.
+
+### Main chat workspace
+
+The center column is the primary workspace:
+
+- conversation history receives most available width and height;
+- user, assistant, tool/status, warning, and error messages have distinct but restrained visual treatments;
+- streaming and turn state remain visible without adding decorative animation;
+- the composer stays anchored at the bottom of the workspace.
+
+Round metrics are placed directly above the composer as a compact strip. They remain secondary to the conversation and do not move into the debug drawer.
+
+### Audit and usage drawer
+
+Audit Replay and usage summaries move from an always-visible fixed column into a collapsible right drawer.
+
+- Opening the drawer shows audit events and usage information together.
+- Closing it returns the width to the chat workspace.
+- Existing audit detail interactions are preserved.
+- Drawer state is persisted in browser storage.
+
+On narrow screens, the page becomes a stacked layout. Controls, chat, metrics/composer, and the opened debug drawer must remain usable without horizontal page scrolling.
+
+## Visual System
+
+Use a light professional console palette with neutral surfaces, subtle borders, restrained shadows, and a small accent color for active states. The design should feel operational rather than decorative.
+
+Define shared CSS variables for color, spacing, radii, control height, and typography. Buttons have explicit primary, secondary, and danger treatments. Status badges communicate connection, invocation mode, and active selections. Keyboard focus remains visible and color is not the only indicator of state.
+
+The UI keeps the existing browser-native implementation and does not add icon libraries or external assets.
+
+## EventPrompt State Model
+
+### Single source of truth
+
+EventPrompt is generated state, not a normal user-authored prompt item. One frontend refresh function owns all derived tool/mode presentation and performs these steps in a fixed order:
+
+1. read the current invocation mode and selected tools;
+2. update the selected-tool count and mode help/status;
+3. remove every existing generated or legacy EventPrompt entry from the prompt list;
+4. if the mode is `dual_agent`, generate and insert exactly one read-only EventPrompt from the selected tools;
+5. if the mode is `chat_agent_tools`, leave EventPrompt absent and mark EventAgent-only model controls unavailable;
+6. refresh any dependent visibility and disabled states.
+
+Every relevant mutation calls this same function:
+
+- individual tool checkbox change;
+- All/None actions;
+- invocation-mode change;
+- initial application bootstrap;
+- session restoration;
+- workspace snapshot restoration;
+- prompt-workspace restoration.
+
+This replaces the current scattered update calls that can update mode help without updating EventPrompt.
+
+### Restore compatibility and deduplication
+
+Persisted `system_prompts` are currently flat text values and do not preserve whether a value was generated EventPrompt content. Legacy restores can therefore treat an old EventPrompt as a custom prompt and append a newly generated EventPrompt, producing duplicates.
+
+During restore, prompt entries matching the generated EventPrompt marker/shape are discarded before normal custom prompts are rebuilt. The derived-state refresh then inserts at most one current EventPrompt when required. User-authored prompts keep their order and content.
+
+No storage schema migration is required for this UI repair. New saves continue to serialize only prompts that are valid for the current invocation mode.
+
+### Outbound request rules
+
+`collectSystemPrompts()` must obey mode semantics rather than blindly serializing every visible prompt-list item:
+
+- `dual_agent`: include user-authored prompts and the one current generated EventPrompt;
+- `chat_agent_tools`: include user-authored prompts only, even if stale DOM or legacy persisted data contains EventPrompt-like content.
+
+The request builder remains compatible with existing backend contracts. This filtering is a defensive boundary in addition to hiding/removing the generated item in the UI.
+
+### Configuration availability
+
+In `chat_agent_tools` mode, EventAgent model configuration is visually unavailable because EventAgent does not perform the invocation decision. Shared runtime controls such as concurrency and timeout remain editable because direct tool execution still uses the shared event kernel and policies.
+
+Switching back to `dual_agent` restores EventAgent model configuration availability and regenerates EventPrompt from the current tool selection.
+
+## Error and State Handling
+
+- A failed session or snapshot restore retains the existing visible workspace and uses the existing feedback/error surface.
+- Empty tool selection produces a deterministic EventPrompt representing no enabled events rather than retaining stale tool descriptions.
+- UI state changes are synchronous and do not wait for a server round trip.
+- Active-turn guards remain unchanged; the refactor must not weaken protection around session switching or configuration changes during a turn.
+- The drawer and collapsible-section preferences are presentation state only and must not enter session runtime payloads.
+
+## File Boundaries
+
+- `src/agent_lab/presentation/static/index.html`: semantic layout, collapsible groups, top status, drawer toggle, and accessible labels.
+- `src/agent_lab/presentation/static/styles.css`: design tokens, responsive shell, control groups, tool rows, message roles, metrics/composer, and drawer styling.
+- `src/agent_lab/presentation/static/app.js`: drawer/collapse persistence, unified derived-state refresh, restore deduplication, mode-aware prompt collection, and configuration availability.
+- `tests/`: focused static/WebSocket contract regressions using the repository's existing test style.
+- project docs: update UI/runtime guidance after behavior is verified.
+
+Backend runtime, event-kernel, persistence, and WebSocket contracts are not changed unless a failing regression proves that a minimal compatibility adjustment is required. Any such adjustment becomes a separate follow-up todo rather than silently expanding Todo 53.
+
+## Verification
+
+Focused regressions must prove:
+
+- changing one tool immediately rebuilds EventPrompt with only selected tools;
+- All/None actions synchronize the selected count and EventPrompt;
+- loading a session or snapshot cannot create duplicate EventPrompt entries;
+- `chat_agent_tools` hides EventPrompt and excludes it from `system_prompts`;
+- switching back to `dual_agent` restores exactly one current EventPrompt;
+- EventAgent-only model controls and shared execution controls have the correct mode-dependent availability;
+- existing request/session/audit contracts remain intact.
+
+Implementation verification will include:
+
+- focused static and WebSocket tests;
+- `node --check src/agent_lab/presentation/static/app.js`;
+- `uv run pytest`;
+- `git diff --check`;
+- local browser QA at desktop and narrow widths, covering chat, tool selection, mode switching, prompt editing, session restore, drawer behavior, and composer usability.
+
+## Delivery Workflow
+
+After this design is approved:
+
+1. create the detailed Todo 53 implementation plan;
+2. execute in an isolated git worktree;
+3. use tests first for each behavior change;
+4. delegate bounded implementation/review tasks to subagents while the main session owns planning and evaluation;
+5. commit each completed todo with the solved problem and any relevant risk in the commit message;
+6. record review-driven corrections as `53.x` rows beneath Todo 53 instead of rewriting completed todo history;
+7. update project documentation after final verification;
+8. merge the verified result locally into the base branch.
+
+## Out of Scope
+
+- React, Vue, another frontend framework, or a build pipeline;
+- backend API/schema redesign;
+- changing the two invocation-mode semantics;
+- adding new event plugins or benchmark cases;
+- a dark theme, theme switcher, or visual-companion assets;
+- a general prompt persistence schema migration.

+ 402 - 0
docs/plans/todo-53-professional-debug-console-ui.md

@@ -0,0 +1,402 @@
+# Professional Debug Console UI Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Turn the static Agent Lab page into a professional responsive debug console and make EventPrompt deterministically follow invocation mode, selected tools, and restored state.
+
+**Architecture:** Preserve the FastAPI/static-JavaScript boundary and backend contracts. Consolidate tool/mode-derived state in one refresh path, then reorganize the page into a collapsible control rail, primary chat workspace, and optional Audit/Usage drawer with browser-only presentation persistence.
+
+**Tech Stack:** HTML5, CSS, browser-native JavaScript, FastAPI static assets, pytest source-contract tests, Node syntax checking, local browser QA.
+
+---
+
+## File Map
+
+- Modify `src/agent_lab/presentation/static/app.js`: EventPrompt derivation, restore cleanup, mode-aware prompt collection, collapsible sections, debug drawer, status indicators.
+- Modify `src/agent_lab/presentation/static/index.html`: console layout, status bar, collapsible control sections, right debug drawer, mode-dependent EventAgent groups.
+- Modify `src/agent_lab/presentation/static/styles.css`: design tokens, console shell, messages, controls, drawer, responsive layout.
+- Modify `tests/test_websocket_api.py`: focused frontend source-contract regressions.
+- Modify `README.md`: operator-facing console and EventPrompt behavior.
+- Modify `docs/plans/todos.md`: close Todo 53 only after all verification succeeds.
+
+### Task 1: Make EventPrompt a mode-derived singleton
+
+**Files:**
+
+- Modify: `tests/test_websocket_api.py:1364-1517`
+- Modify: `src/agent_lab/presentation/static/app.js:85-93,259-320,413-450,477-677,867-897`
+- Modify: `src/agent_lab/presentation/static/index.html:212-233`
+
+- [x] **Step 1: Write failing source-contract tests**
+
+Add these assertions in focused tests:
+
+```python
+assert 'toolInvocationMode.addEventListener("change", refreshToolModeState)' in js
+assert 'checkbox.addEventListener("change", refreshToolModeState)' in js
+assert "function refreshToolModeState()" in js
+assert "function isGeneratedEventPromptContent(" in js
+assert "function removeEventPromptItems()" in js
+assert 'toolInvocationMode.value === "chat_agent_tools"' in update_prompt
+assert 'toolInvocationMode.value !== "chat_agent_tools"' in collect_prompts
+assert 'prompt.prompt_kind !== "event_agent_system"' in normalize_prompts
+assert "!isGeneratedEventPromptContent(prompt.content)" in normalize_prompts
+assert 'id="event-agent-model-settings"' in html
+```
+
+Slice `update_prompt`, `collect_prompts`, and `normalize_prompts` using the existing `js.index(...)` test style so each assertion is scoped to the correct function.
+
+- [x] **Step 2: Run tests and verify RED**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "event_prompt or prompt_restore or tool_mode" -q
+```
+
+Expected: new assertions fail because there is no unified refresh, generated-content filter, or model-settings container.
+
+- [x] **Step 3: Add conservative legacy-generated-prompt detection**
+
+Add:
+
+```javascript
+const EVENT_PROMPT_PREFIX = "You may request EventAgent work using this text protocol.";
+
+function isGeneratedEventPromptContent(content) {
+  const normalized = String(content || "").trim();
+  return normalized.startsWith(EVENT_PROMPT_PREFIX)
+    && normalized.includes("<agent_events>")
+    && normalized.includes("Use exact event names only");
+}
+
+function removeEventPromptItems() {
+  [...systemPrompts.querySelectorAll(".prompt-item")].forEach((item) => {
+    const content = item.querySelector(".system-prompt")?.value || "";
+    if (item.dataset.promptKind === "event_agent_system"
+      || isGeneratedEventPromptContent(content)) {
+      item.remove();
+    }
+  });
+}
+```
+
+Change `normalizePromptItems()` to discard typed EventPrompt entries and legacy generated EventPrompt text. Insert one blank custom prompt only when no custom prompt remains; do not insert EventPrompt there.
+
+- [x] **Step 4: Centralize mode/tool refresh and outbound filtering**
+
+Implement:
+
+```javascript
+function refreshToolModeState() {
+  updateToolCount();
+  updateToolModeHelp();
+  updateEventAgentConfigAvailability();
+  updateEventPromptItem(availableTools);
+}
+
+function updateEventPromptItem(tools) {
+  removeEventPromptItems();
+  if (toolInvocationMode.value === "chat_agent_tools") {
+    return;
+  }
+  const selected = new Set(selectedTools());
+  const eventPrompt = ensureEventPromptItem();
+  eventPrompt.querySelector(".system-prompt").value = buildAvailableEventsPrompt(
+    tools.filter((tool) => selected.has(tool.name)),
+  );
+}
+```
+
+Bind mode change, checkbox change, All/None, bootstrap, `applySelectedTools()`, and `restoreWorkspaceSnapshot()` to `refreshToolModeState()`. Restore mode/prompts/tools first, then refresh once.
+
+Implement defensive collection:
+
+```javascript
+function collectSystemPrompts() {
+  const includeEventPrompt = toolInvocationMode.value !== "chat_agent_tools";
+  return buildPromptItems()
+    .filter((item) => item.prompt_kind !== "event_agent_system" || includeEventPrompt)
+    .filter((item) => includeEventPrompt || !isGeneratedEventPromptContent(item.content))
+    .map((item) => item.content)
+    .filter(Boolean);
+}
+```
+
+Wrap EventAgent model, temperature, max tokens, system prompt, and extra-body controls in `#event-agent-model-settings`. Disable that fieldset only in `chat_agent_tools`; keep loop, parallelism, and timeout controls outside it and enabled.
+
+- [x] **Step 5: Verify GREEN**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "event_prompt or prompt_restore or tool_mode or tools_ui" -q
+node --check src/agent_lab/presentation/static/app.js
+git diff --check
+```
+
+Expected: focused tests pass; Node and diff checks are clean.
+
+- [x] **Step 6: Commit Task 1**
+
+```bash
+git add tests/test_websocket_api.py src/agent_lab/presentation/static/app.js src/agent_lab/presentation/static/index.html
+git commit -m "fix: synchronize EventPrompt with tool mode" -m "Problem: EventPrompt could remain stale, duplicate after restore, or be sent in chat_agent_tools mode.\n\nRisk: legacy generated prompts are matched conservatively by established protocol markers."
+```
+
+### Task 2: Build the professional console layout and visual system
+
+**Files:**
+
+- Modify: `tests/test_websocket_api.py:1334-1607`
+- Modify: `src/agent_lab/presentation/static/index.html:7-247`
+- Modify: `src/agent_lab/presentation/static/styles.css:1-end`
+
+- [x] **Step 1: Write failing layout tests**
+
+Require these IDs and structural markers while retaining every existing runtime control ID:
+
+```python
+for control_id in [
+    "current-session-label",
+    "current-mode-label",
+    "toggle-debug-drawer",
+    "debug-drawer",
+    "session-controls",
+    "tool-controls",
+    "agent-config-controls",
+    "snapshot-controls",
+]:
+    assert f'id="{control_id}"' in html
+assert html.count('class="control-section-toggle"') == 4
+assert 'aria-controls="debug-drawer"' in html
+assert "--surface:" in css
+assert ".layout.debug-drawer-closed" in css
+assert "@media (max-width: 900px)" in css
+```
+
+Update the existing fixed-layout test to expect the new left/workspace/drawer shell instead of `.workspace-body` containing a permanent replay column.
+
+- [x] **Step 2: Run tests and verify RED**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "professional_console or layout_keeps_controls" -q
+```
+
+Expected: failure because the new shell and CSS tokens do not exist.
+
+- [x] **Step 3: Restructure the HTML shell**
+
+Use this hierarchy:
+
+```html
+<header class="topbar">
+  <div class="brand-block">
+    <h1>Agent Lab</h1>
+    <span>Chat and event runtime console</span>
+  </div>
+  <div class="top-status" aria-label="Runtime status">
+    <span id="current-session-label" class="status-badge">No session</span>
+    <span id="current-mode-label" class="status-badge">Dual Agent</span>
+    <span id="connection-status" class="status-badge status-neutral">Idle</span>
+    <button id="toggle-debug-drawer" type="button"
+      aria-expanded="true" aria-controls="debug-drawer">Hide Debug</button>
+  </div>
+</header>
+```
+
+Immediately after the header, create `main#console-layout.layout` with exactly three direct children in order: `aside.controls[aria-label="Runtime controls"]`, `section.workspace[aria-label="Conversation workspace"]`, and `aside#debug-drawer.debug-drawer[aria-label="Audit and usage"]`. Create four `.control-section` blocks in the controls child. Each toggle uses `data-collapse-target`, `aria-expanded`, and `aria-controls`; Session and Tools start expanded, Agent Config and Snapshot collapsed. Move Audit Replay and Session Usage into the drawer. Keep metrics above the anchored composer, preserve dialogs and all current IDs, and version both assets as `20260714-professional-console`.
+
+- [x] **Step 4: Implement the visual system and responsive layout**
+
+Start with these tokens and layout rules:
+
+```css
+:root {
+  --canvas: #f3f5f7;
+  --surface: #ffffff;
+  --surface-subtle: #f8fafc;
+  --border: #d9e0e7;
+  --text: #17212b;
+  --muted: #647180;
+  --accent: #176b87;
+  --accent-strong: #0f536a;
+  --danger: #b42318;
+  --radius: 10px;
+  --shadow: 0 8px 24px rgb(23 33 43 / 8%);
+}
+
+.layout {
+  display: grid;
+  grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) minmax(300px, 380px);
+  height: calc(100vh - 64px);
+  min-height: 0;
+  overflow: hidden;
+}
+
+.layout.debug-drawer-closed {
+  grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) 0;
+}
+```
+
+Add explicit primary/secondary/danger buttons, `:focus-visible`, compact selected tool cards, restrained role-specific messages, metrics/composer boundaries, drawer overflow, and `[hidden]` collapse rules. At `max-width: 900px`, allow body scrolling, stack the three regions, remove locked heights, and prevent horizontal overflow.
+
+- [x] **Step 5: Verify GREEN**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "static_ui or professional_console or layout or audit_replay or agent_config" -q
+node --check src/agent_lab/presentation/static/app.js
+git diff --check
+```
+
+Expected: focused static tests and checks pass.
+
+- [x] **Step 6: Commit Task 2**
+
+```bash
+git add tests/test_websocket_api.py src/agent_lab/presentation/static/index.html src/agent_lab/presentation/static/styles.css
+git commit -m "feat: redesign the debug console layout" -m "Problem: chat lacked visual priority and Audit/Usage permanently consumed workspace width.\n\nRisk: existing DOM IDs are retained to preserve runtime bindings."
+```
+
+### Task 3: Persist presentation state and synchronize top status
+
+**Files:**
+
+- Modify: `tests/test_websocket_api.py:1396-1620`
+- Modify: `src/agent_lab/presentation/static/app.js:1-120 and session lifecycle functions`
+
+- [x] **Step 1: Write failing persistence/status tests**
+
+```python
+assert 'const CONSOLE_UI_STORAGE_KEY = "agent-lab-console-ui-v1"' in js
+assert "function initializeConsoleUi()" in js
+assert "function setDebugDrawerOpen(" in js
+assert "function setControlSectionExpanded(" in js
+assert "localStorage.getItem(CONSOLE_UI_STORAGE_KEY)" in js
+assert "localStorage.setItem(CONSOLE_UI_STORAGE_KEY" in js
+assert "function updateTopbarStatus()" in js
+assert "current-session-label" in js
+assert "current-mode-label" in js
+assert "CONSOLE_UI_STORAGE_KEY" not in build_request
+```
+
+- [x] **Step 2: Run tests and verify RED**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "presentation_state or topbar_status" -q
+```
+
+Expected: failure because the presentation-state functions do not exist.
+
+- [x] **Step 3: Implement resilient browser-only UI state**
+
+```javascript
+const CONSOLE_UI_STORAGE_KEY = "agent-lab-console-ui-v1";
+
+function readConsoleUiState() {
+  try {
+    return JSON.parse(localStorage.getItem(CONSOLE_UI_STORAGE_KEY) || "{}");
+  } catch (_error) {
+    return {};
+  }
+}
+
+function writeConsoleUiState(state) {
+  try {
+    localStorage.setItem(CONSOLE_UI_STORAGE_KEY, JSON.stringify(state));
+  } catch (_error) {
+    // Presentation preferences must never block the runtime.
+  }
+}
+```
+
+`setControlSectionExpanded(button, expanded, { persist = true } = {})` updates `aria-expanded`, target `hidden`, and `sections[targetId]`. `setDebugDrawerOpen(open, { persist = true } = {})` updates layout class, drawer `hidden`, button text/ARIA, and `debug_drawer_open`. `initializeConsoleUi()` applies stored state over markup defaults and binds controls. None of these values enter snapshots or requests.
+
+- [x] **Step 4: Synchronize topbar session/mode context**
+
+```javascript
+function updateTopbarStatus() {
+  document.querySelector("#current-session-label").textContent = currentSessionId
+    ? `Session ${currentSessionId.slice(0, 8)}`
+    : "No session";
+  document.querySelector("#current-mode-label").textContent =
+    toolInvocationMode.value === "chat_agent_tools"
+      ? "ChatAgent Tools"
+      : "Dual Agent";
+}
+```
+
+Add `updateTopbarStatus();` to `refreshToolModeState()`, then call it after bootstrap, session creation/load, `session_started`, and any path replacing `currentSessionId`. Preserve active-turn and stale-socket guards.
+
+- [x] **Step 5: Verify GREEN**
+
+```bash
+uv run pytest tests/test_websocket_api.py -q
+node --check src/agent_lab/presentation/static/app.js
+git diff --check
+```
+
+Expected: all WebSocket/static tests pass with only the existing deprecation warning.
+
+- [x] **Step 6: Commit Task 3**
+
+```bash
+git add tests/test_websocket_api.py src/agent_lab/presentation/static/app.js
+git commit -m "feat: persist debug console presentation state" -m "Problem: drawer, control-section, session, and mode context were not synchronized.\n\nRisk: localStorage failures are ignored so UI preferences cannot block runtime requests."
+```
+
+### Task 4: Document, visually verify, and close Todo 53
+
+**Files:**
+
+- Modify: `README.md:94-102`
+- Modify: `docs/plans/todos.md:117`
+- Modify: `docs/plans/todo-53-professional-debug-console-ui.md`
+
+- [x] **Step 1: Update README operator guidance**
+
+Document the left control rail, primary chat workspace, anchored metrics/composer, collapsible Audit/Usage drawer, browser-only presentation state, and these rules:
+
+```markdown
+In `dual_agent`, the read-only EventPrompt is generated from selected tools and
+appears once in Chat Agent Prompt List. In `chat_agent_tools`, EventPrompt is
+hidden and excluded from `system_prompts`; mode changes and session/snapshot
+restores regenerate the correct state. EventAgent model settings are unavailable
+in direct mode, while shared loop, concurrency, and timeout controls remain active.
+```
+
+- [x] **Step 2: Run the complete automated gate**
+
+```bash
+uv run pytest
+node --check src/agent_lab/presentation/static/app.js
+git diff --check
+uv run python -c "from agent_lab.main import app; print(app.title)"
+```
+
+Expected: full pytest passes with only the known Starlette/httpx warning; Node/diff/import checks succeed.
+
+- [x] **Step 3: Run desktop browser QA (waived by user on 2026-07-15)**
+
+Start `uv run uvicorn agent_lab.main:app --host 127.0.0.1 --port 8000`, then verify section defaults/persistence, drawer width recovery, tools/All/None/count, mode badge, exactly one selected-tool EventPrompt in dual mode, no EventPrompt in direct mode, model-only disabled state, restore deduplication, chat streaming, audit dialog, and usage rendering.
+
+- [x] **Step 4: Run narrow browser QA (waived by user on 2026-07-15)**
+
+At width `<= 900px`, verify vertical stacking, no horizontal scrollbar, usable messages/metrics/composer/tools/modals, and removal of the drawer region when closed.
+
+- [x] **Step 5: Observe and update Todo 53**
+
+If a defect exists, keep 53 `in_progress` and add a `53.x` row directly below it with a focused plan. Otherwise mark 53 `done`, record exact test counts and browser evidence, and mark this plan's checkboxes complete.
+
+- [x] **Step 6: Commit Task 4**
+
+```bash
+git add README.md docs/plans/todos.md docs/plans/todo-53-professional-debug-console-ui.md
+git commit -m "docs: close professional console todo" -m "Problem: the new console and EventPrompt contract need durable guidance and verification evidence."
+```
+
+- [x] **Step 7: Final clean-state check and local merge**
+
+Run `git status --short` and `git log -5 --oneline`. Merge the verified feature branch locally into `codex/agent-lab-mvp`, then rerun the full automated gate on the merged branch.
+
+## Workflow Evolution Observation
+
+- Scoped subagents were effective for TDD implementation and independent review, but some could not write shared Git metadata. The main session should preserve their verified unstaged/staged changes and perform the scoped commit rather than waiting indefinitely.
+- Browser availability is an execution-environment capability. When it is unavailable, record the exact automated/HTTP evidence and an explicit user waiver; never report visual QA as passed.
+- These findings are recorded in this task report rather than expanding `AGENTS.md`: the existing plan-act-observe-update and per-todo commit rules already cover the durable workflow, while the observed limitations are tool-specific.

+ 38 - 0
docs/plans/todo-53.1-event-prompt-order-preservation.md

@@ -0,0 +1,38 @@
+# Todo 53.1: EventPrompt Order Preservation
+
+## Problem
+
+Commit `cf481ab` correctly made EventPrompt derived state, but `removeEventPromptItems()` deletes it and `ensureEventPromptItem()` appends the replacement. Tool changes, mode round trips, and restored prompt lists therefore move EventPrompt to the end and change serialized system-message order.
+
+## Scope
+
+- Preserve the first generated EventPrompt's position relative to retained custom prompts.
+- Remember that insertion position while `chat_agent_tools` hides EventPrompt.
+- Restore the saved typed/legacy EventPrompt position when loading prompt items.
+- Continue removing duplicate generated EventPrompt items and preserving ordinary custom prompts.
+- Do not add frontend dependencies or change backend contracts.
+
+## TDD Steps
+
+1. Add a failing regression in `tests/test_websocket_api.py` that requires an explicit EventPrompt insertion-position state/helper, captures position before removal, and reinserts with `insertBefore` instead of unconditional append.
+2. Run the focused test and confirm it fails for the current append-only implementation.
+3. Implement the smallest position-preserving logic in `src/agent_lab/presentation/static/app.js`.
+4. Verify tool refresh, `dual_agent -> chat_agent_tools -> dual_agent`, and restore paths use the same remembered position.
+5. Run:
+
+   ```bash
+   uv run pytest tests/test_websocket_api.py -k "event_prompt or prompt_restore or tool_mode" -q
+   node --check src/agent_lab/presentation/static/app.js
+   git diff --check
+   ```
+
+6. Commit with the solved problem and ordering risk, then repeat spec and code-quality reviews.
+
+## Acceptance
+
+- EventPrompt content and visibility remain mode/tool-derived.
+- At most one EventPrompt exists.
+- Refreshing tools does not move EventPrompt.
+- Hiding and showing EventPrompt restores its previous insertion position.
+- Restoring typed or legacy prompt data derives the insertion position from the first generated EventPrompt while discarding duplicates.
+- Custom prompt order and outbound request contracts remain unchanged.

+ 33 - 0
docs/plans/todo-53.2-console-interaction-state.md

@@ -0,0 +1,33 @@
+# Todo 53.2: Console Interaction State
+
+## Problem
+
+Commit `fd9c31e` introduced the professional console shell, but the new collapsed Agent Config/Snapshot sections and Hide Debug button have no JavaScript handlers. The intermediate UI therefore makes controls unreachable and cannot close the drawer.
+
+## Scope
+
+- Bind all `.control-section-toggle` buttons and update `aria-expanded` plus target `hidden` state.
+- Bind `#toggle-debug-drawer`, update `#console-layout.debug-drawer-closed`, drawer `hidden`, button text, and ARIA state.
+- Persist section and drawer preferences in a dedicated browser-only localStorage record.
+- Treat storage parse/write failures as non-fatal presentation failures.
+- Update `#current-session-label` and `#current-mode-label` from existing runtime state.
+- Keep presentation state out of workspace snapshots and WebSocket requests.
+- Preserve EventPrompt, active-turn, stale-socket, session, and backend contracts.
+
+## TDD Steps
+
+1. Add failing tests in `tests/test_websocket_api.py` that execute the interaction helpers with lightweight DOM/localStorage fakes, not only source substring checks.
+2. Confirm RED for unreachable sections/drawer and missing topbar synchronization.
+3. Add the smallest interaction state implementation to `src/agent_lab/presentation/static/app.js`.
+4. Verify bootstrap, mode refresh, session create/load/start, and session reset paths update topbar status.
+5. Run focused/full WebSocket tests, Node syntax, and diff checks.
+6. Commit and repeat specification plus code-quality review.
+
+## Acceptance
+
+- Agent Config and Snapshot can be expanded from their default collapsed state.
+- Drawer open/closed state changes layout and visibility consistently on desktop and narrow layouts.
+- Reload restores the last section/drawer preferences when storage is valid.
+- Invalid or unavailable storage does not block the runtime.
+- Topbar mode and abbreviated session labels stay current.
+- No console UI preference appears in request or snapshot payloads.

+ 28 - 0
docs/plans/todo-53.3-event-prompt-payload-singleton.md

@@ -0,0 +1,28 @@
+# Todo 53.3: EventPrompt Payload Singleton
+
+## Problem
+
+The DOM refresh path removes duplicate generated EventPrompt items, but `collectSystemPrompts()` in `dual_agent` currently serializes every prompt item. A stale or user-edited custom item containing the complete generated EventPrompt signature can therefore be sent alongside the managed EventPrompt.
+
+## Scope
+
+- At the outbound collection boundary, discard generated-signature custom items in both invocation modes.
+- In `dual_agent`, retain only the managed `event_agent_system` item plus ordinary custom prompts.
+- In `chat_agent_tools`, retain ordinary custom prompts only and send no EventPrompt.
+- Preserve prompt order and content for ordinary custom items.
+- Do not alter restore, EventPrompt positioning, backend contracts, or UI layout.
+
+## TDD Steps
+
+1. Change the existing Node behavior test so `dual_agent` expects exactly one generated prompt instead of accepting a generated-signature custom duplicate.
+2. Run the focused test and confirm RED against the current collector.
+3. Apply the minimal filter in `collectSystemPrompts()`.
+4. Verify ordinary custom prompts and managed EventPrompt order remain unchanged.
+5. Run focused/full WebSocket tests, Node syntax, diff checks, and independent review.
+
+## Acceptance
+
+- `dual_agent` serializes ordinary custom prompts and at most one managed EventPrompt.
+- `chat_agent_tools` serializes ordinary custom prompts and zero EventPrompt content.
+- A generated-signature custom item cannot bypass the outbound boundary.
+- EventPrompt visibility, ordering, restore deduplication, and tool-derived content remain unchanged.

+ 20 - 0
docs/plans/todo-53.4-managed-event-prompt-test-identity.md

@@ -0,0 +1,20 @@
+# Todo 53.4: Managed EventPrompt Test Identity
+
+## Problem
+
+The Todo 53.3 behavior test gives the managed EventPrompt and generated-signature custom impostor identical content. Its expected output therefore proves only that one string remains, not that the collector retained the managed item.
+
+## Scope
+
+- Change only `tests/test_websocket_api.py`.
+- Use two different strings that both satisfy the conservative generated EventPrompt signature.
+- Assign one to the custom impostor and the other to the managed `event_agent_system` item.
+- Assert `dual_agent` retains the managed content and rejects the impostor.
+- Assert `chat_agent_tools` rejects both generated contents while preserving ordinary custom prompts.
+- Do not change production code.
+
+## Verification
+
+- Run the focused EventPrompt test and the full WebSocket test file.
+- Run Node syntax and diff checks.
+- Repeat specification and code-quality review for Todo 53.3/53.4 together.

+ 19 - 0
docs/plans/todo-53.5-event-prompt-order-independent-identity.md

@@ -0,0 +1,19 @@
+# Todo 53.5: EventPrompt Order-Independent Identity
+
+## Problem
+
+Todo 53.4 distinguishes managed and impostor content, but tests only the ordering where the impostor precedes the managed item. An incorrect collector that keeps the last generated-signature item could still pass.
+
+## Scope
+
+- Change only the EventPrompt payload behavior test.
+- Run the same assertions with managed-before-impostor and managed-after-impostor prompt arrays.
+- In both orderings, `dual_agent` must retain ordinary custom prompts plus the managed content.
+- In both orderings, `chat_agent_tools` must retain ordinary custom prompts only.
+- Do not change production code.
+
+## Verification
+
+- Focused payload test passes for both orderings.
+- Full WebSocket test file, Node syntax, and diff checks pass.
+- Repeat the bounded code-quality review.

+ 41 - 0
docs/plans/todo-53.6-browser-visual-qa.md

@@ -0,0 +1,41 @@
+# Todo 53.6: Browser Visual QA
+
+**Status:** waived by user on 2026-07-15
+
+## Original Blocker
+
+The local application starts and serves the new assets, but the current Codex browser runtime reports an empty browser list. Desktop and narrow-screen visual/interaction QA therefore cannot be performed in this session.
+
+Chrome was checked separately on 2026-07-15: Chrome was running, the ChatGPT Chrome Extension was installed and enabled, and the Native Host manifest was correct, but extension communication remained unavailable. The user then explicitly instructed the task to skip browser acceptance. No browser result is represented as having passed.
+
+## Verified Before Blocker
+
+- Full suite: `617 passed`, one existing Starlette/httpx deprecation warning.
+- `node --check src/agent_lab/presentation/static/app.js` passes.
+- `git diff --check` passes.
+- Application import prints `Agent Lab`.
+- HTTP smoke at `http://127.0.0.1:8010/` returns the new `console-layout`, `debug-drawer`, `20260714-professional-console` assets, and console interaction functions.
+
+## Required Browser Checks
+
+### Desktop
+
+- Session and Tools start expanded; Agent Config and Snapshot start collapsed and can be opened.
+- Drawer opens/closes, releases chat width, and persists after reload.
+- Tool selection, All/None, count, mode badge, and EventPrompt remain synchronized.
+- `dual_agent` shows exactly one selected-tool EventPrompt.
+- `chat_agent_tools` hides EventPrompt and disables only EventAgent model-specific settings.
+- Switching back restores exactly one EventPrompt at its prior position.
+- Session/snapshot restore cannot duplicate EventPrompt.
+- Chat send, streaming, audit dialog, and usage rendering remain usable.
+
+### Narrow screen (`<= 900px`)
+
+- Controls, workspace, and open drawer stack vertically.
+- No horizontal page scrollbar appears.
+- Messages, metrics, composer, tool selection, and modal controls remain usable.
+- Closing the drawer removes its stacked region.
+
+## Closeout Decision
+
+The desktop and narrow-screen checks above were waived rather than executed. Todo 53 may close using the fresh automated gate, HTTP asset smoke, and independent code reviews as the accepted evidence. Any later visual defect must be recorded as a new follow-up todo instead of rewriting the completed Todo 53 history.

+ 8 - 0
docs/plans/todos.md

@@ -114,3 +114,11 @@
 | 52.11 | done | `docs/plans/todo-52.11-bundle-interrupt-and-path-safety.md` | Preserve complete bundles across post-symlink interrupts and reject unsafe report timestamp path components. | Interruptions cannot leave dangling public bundles; explicit timestamps stay inside output_dir; fsync tests verify both files and both directories. |
 | 52.12 | done | `docs/plans/todo-52.12-benchmark-metric-integrity-and-parallel-proof.md` | Exclude consumer backpressure from model elapsed, reject missing token usage, and prove parallel handlers overlap. | Model latency measures provider-await time; absent usage cannot appear as zero cost; the parallel case fails unless both handlers start concurrently. |
 | 52.13 | done | `docs/plans/todo-52.13-provider-cleanup-exception-priority.md` | Preserve active provider/cancellation exceptions when iterator cleanup also fails. | Provider failures and cancellation retain priority; cleanup failures propagate only when no earlier exception is active; timing attempts are still recorded. |
+| 53 | done | `docs/plans/todo-53-professional-debug-console-ui.md` | Redesign the static UI as a professional debug console and make EventPrompt visibility/content follow invocation mode and selected tools. | Final automated gate passes (`617 passed`, one existing warning); JS syntax, diff, import, HTTP asset smoke, and independent reviews pass. Desktop/narrow browser QA was explicitly waived by the user on 2026-07-15. |
+| 53.1 | done | `docs/plans/todo-53.1-event-prompt-order-preservation.md` | Preserve the user-selected EventPrompt position while mode/tool refreshes still deduplicate and regenerate its content. | RED: `3 failed, 5 passed`; GREEN: focused `8 passed`, full WebSocket file `84 passed`; Node syntax and diff checks pass; behavior tests cover refresh, mode round trip, restore position, and duplicate cleanup. |
+| 53.2 | done | `docs/plans/todo-53.2-console-interaction-state.md` | Make the new collapsible control sections and Audit/Usage drawer operable, persistent, and synchronized with session/mode status. | RED: `6 failed, 9 passed`; GREEN: focused `15 passed`, full WebSocket file `94 passed`; Node/diff checks and independent spec/code-quality reviews pass. |
+| 53.3 | done | `docs/plans/todo-53.3-event-prompt-payload-singleton.md` | Enforce the EventPrompt singleton at the outbound payload boundary in both invocation modes, including stale or user-edited generated-signature custom prompts. | RED: `1 failed, 4 passed`; GREEN: focused `5 passed`, full WebSocket file `94 passed`; collector includes managed EventPrompt only in dual mode. |
+| 53.4 | done | `docs/plans/todo-53.4-managed-event-prompt-test-identity.md` | Make the payload-singleton regression distinguish the managed EventPrompt from a generated-signature custom impostor. | Focused `1 passed`, full WebSocket file `94 passed`; mutation check proves the test fails if the impostor survives instead of the managed item. |
+| 53.5 | done | `docs/plans/todo-53.5-event-prompt-order-independent-identity.md` | Prove EventPrompt payload selection depends on managed prompt identity rather than whether it appears before or after a generated-signature impostor. | Focused `1 passed`, full WebSocket file `94 passed`; both item orderings pass and bounded code-quality review is approved. |
+| 53.6 | done | `docs/plans/todo-53.6-browser-visual-qa.md` | Complete desktop and narrow-screen visual/interaction QA before closing Todo 53 and merging locally. | Browser QA was explicitly waived by the user on 2026-07-15 after Chrome extension communication remained unavailable; automated and HTTP verification remain the accepted closeout evidence. |
+| 54 | pending | `docs/superpowers/specs/2026-07-15-chat-agent-event-prompt-design.md` | Balance ChatAgent text-event recall and false positives with a decision-oriented EventPrompt while preserving the existing protocol and invocation-mode boundaries. | TDD prompt-contract and compatibility tests pass; focused/full suites, JS syntax, app import, and diff checks pass; live A/B evidence is recorded when a provider target is available. |

+ 226 - 0
docs/superpowers/specs/2026-07-15-chat-agent-event-prompt-design.md

@@ -0,0 +1,226 @@
+# ChatAgent Event Selection Prompt Design
+
+**Date:** 2026-07-15
+**Status:** approved for implementation planning
+
+## Context
+
+In `dual_agent` mode, ChatAgent selects EventAgent capabilities by appending
+event names inside an `<agent_events>` block after its visible reply. The
+current generated prompt explains the catalog and output format, but treats
+event selection as optional and does not define when an event is required.
+
+Recent local audit evidence shows the practical failure mode: ChatAgent says it
+will adjust volume or check weather, but emits no event. Examples include
+`声音调到90`, `调大一点音量`, and later weather requests. The model therefore
+produces a plausible conversational promise without causing the real action.
+
+The generated prompt currently exists in two equivalent construction paths:
+
+- `ToolRegistry.chat_event_system_message()` supplies the runtime fallback for
+  API clients that did not submit an EventPrompt.
+- `buildAvailableEventsPrompt()` generates the read-only EventPrompt shown and
+  submitted by the browser console.
+
+Both paths must retain the same behavioral contract. The text-event parser and
+the `<agent_events>` wire format are already stable and are not the source of
+the observed missed triggers.
+
+## Goals
+
+- Balance event recall against false positives.
+- Require an event whenever ChatAgent promises or claims a supported external
+  action.
+- Trigger low-risk, read-only, or reversible capabilities from clear natural
+  intent, even when the user does not phrase it as an imperative.
+- Ask one concise clarification before ambiguous or incomplete higher-impact
+  actions.
+- Keep ordinary conversation, hypothetical discussion, unsupported requests,
+  and capability questions event-free.
+- Preserve the existing event block protocol, parser, EventAgent argument
+  resolution, event kernel, and invocation-mode boundaries.
+- Continue recognizing old generated EventPrompts when restoring sessions or
+  workspace snapshots.
+
+## Non-goals
+
+- No separate intent-classification model call.
+- No provider-native tool schema changes.
+- No EventAgent argument protocol changes; ChatAgent still emits names only.
+- No event-kernel, handler, session, WebSocket, or active-turn behavior change.
+- No guarantee that every stochastic model follows the prompt perfectly; live
+  evaluation measures adherence but does not replace deterministic contracts.
+
+## Approaches Considered
+
+### 1. Decision-oriented prompt only
+
+Replace the weak optional wording with explicit selection rules, consistency
+invariants, and compact positive and negative examples. Keep the current
+protocol and model-call count.
+
+This is the selected approach because it directly addresses the observed
+failure, has the smallest runtime scope, and can be evaluated independently
+before introducing new architecture.
+
+### 2. Metadata-driven trigger policies
+
+Expose trigger and confirmation metadata through the event catalog and render
+event-specific policy dynamically. This scales better for a large heterogeneous
+registry, but requires domain, catalog API, frontend, and compatibility changes.
+It remains a possible follow-up if prompt-only evaluation shows that generic
+rules are insufficient.
+
+### 3. Separate event-intent classifier
+
+Run a dedicated classification step before ChatAgent response generation. This
+could improve isolation but adds latency, token cost, another failure mode, and
+coordination complexity. It is not justified before measuring the prompt-only
+approach.
+
+## Selected Prompt Contract
+
+The generated EventPrompt will identify itself as protocol version 2 and use
+imperative rules. Its dynamic catalog remains name plus description only.
+
+The prompt must communicate these rules:
+
+1. Decide whether the user's latest intent requires any available event.
+2. When a supported real-world action or fresh external-data request is clear,
+   emit the matching event.
+3. If the visible reply says or implies that ChatAgent will perform, has
+   performed, is checking, is changing, is scheduling, or is ending something,
+   the same reply must append the corresponding event. ChatAgent must never
+   promise a supported action without emitting its event.
+4. Low-risk, read-only, and reversible actions may be inferred from clear
+   natural intent. `声音太小了` can select `device.volume.adjust`, and
+   `明天天气怎么样` can select `knowledge.web.search`.
+5. For ambiguous or incomplete higher-impact actions, ask one concise
+   clarification and emit no event yet. A complete and unambiguous request does
+   not require redundant confirmation.
+6. Do not emit events for ordinary conversation, hypothetical statements,
+   discussion about a capability, unsupported behavior, or a request that does
+   not match an available event.
+7. Multiple independent clear intents may emit multiple exact event names, one
+   per line.
+8. The event block is machine-control metadata, not part of the visible reply.
+   It is allowed even when another prompt prohibits Markdown, lists, JSON, or
+   other visible formatting.
+9. When no event is selected, omit the event block entirely.
+10. Never include parameters, explanations, bullets, code fences, or invented
+    names inside the block.
+
+The generated shape will remain:
+
+```text
+Event selection protocol v2.
+Available events:
+- event.name: description
+...
+[decision rules and compact examples]
+First write the user-facing reply normally. If one or more events are selected,
+append exactly this block after the visible reply:
+<agent_events>
+event_name
+</agent_events>
+Use exact available event names only, one per line. Do not include parameters.
+```
+
+## Decision Examples
+
+The prompt will include a small, generic set of examples that match the current
+production-shaped capabilities without teaching argument extraction:
+
+| User intent | Expected selection behavior |
+| --- | --- |
+| `声音太小了` | Reply naturally and emit `device.volume.adjust`. |
+| `声音调到90` | Reply naturally and emit `device.volume.adjust`. |
+| `明天天气怎么样` | Reply naturally and emit `knowledge.web.search`. |
+| `你回复太慢了` | No event; response latency is not device volume. |
+| `你能调音量吗` | No event; this asks about capability rather than requesting action. |
+| `提醒我开会` | Ask for missing scheduling details and emit no event. |
+| `我明天下午三点要开会` | No event; this is a statement unless the user requests scheduling. |
+| A complete request to create a schedule | Emit `calendar.schedule.create`. |
+| An explicit goodbye or session-ending request | Emit `session.terminate`. |
+| Ordinary greeting or emotional conversation | No event. |
+
+Examples are selection guidance only. EventAgent remains responsible for
+arguments and validation.
+
+## Data Flow and Compatibility
+
+The browser continues to generate a read-only EventPrompt from selected tools.
+In `dual_agent`, exactly one managed EventPrompt is serialized. In
+`chat_agent_tools`, generated EventPrompt content remains excluded and provider
+tool schemas remain the only capability-selection surface.
+
+The runtime continues to inject its registry-generated prompt only when no
+event instructions are already present. The Python and JavaScript prompt
+builders will use the same version marker, rule ordering, examples, catalog
+format, and output suffix.
+
+`isGeneratedEventPromptContent()` must recognize both:
+
+- the legacy prefix `You may request EventAgent work using this text protocol.`;
+- the new `Event selection protocol v2.` prefix.
+
+Recognition remains conservative: a known prefix alone is insufficient. The
+content must also contain the event block markers and the exact-name/no-
+parameters instruction. This preserves cleanup of historical generated prompts
+without deleting ordinary custom prompts that merely mention event syntax.
+
+## Verification Design
+
+Implementation will use TDD.
+
+Deterministic tests will first fail against the current prompt and then prove:
+
+- the backend prompt contains the version marker, mandatory action/event
+  consistency rule, low-risk inference rule, higher-impact clarification rule,
+  negative boundaries, exact output block, and compact examples;
+- the frontend prompt contains the same behavioral anchors and catalog output;
+- old and new generated EventPrompt signatures are both recognized;
+- ordinary custom prompts containing partial markers remain retained;
+- payload singleton and invocation-mode filtering behavior remain unchanged;
+- runtime fallback injection still supplies exactly one catalog and no provider
+  tools in `dual_agent`;
+- parser behavior is unchanged.
+
+Focused verification commands will cover the prompt/runtime tests and frontend
+source/behavior tests, followed by JavaScript syntax, the full suite, app import,
+and diff checks.
+
+If a local live provider target and API key are available, run a fresh A/B
+evaluation with the same model and parameters. At minimum, evaluate the ten
+decision examples above across old and new prompts and record both missed
+required events and false positives. Existing historical SQLite sessions are
+evidence for the problem but must not be reused as an A/B input source because
+their accumulated context and prompt order differ.
+
+## Risks and Controls
+
+- Stronger rules may increase false positives. Negative examples and the
+  capability-question/hypothetical boundaries control this risk.
+- Generic risk language may not scale to future events. If live evaluation
+  exposes event-specific ambiguity, follow up with metadata-driven policy
+  rather than continually expanding a monolithic prompt.
+- Prompt duplication between Python and JavaScript can drift. Tests assert the
+  shared behavioral anchors and exact protocol suffix in both paths.
+- Historical snapshots may contain the old generated prompt. Dual-signature
+  detection preserves deduplication and outbound singleton guarantees.
+- Other system prompts may prohibit visible formatting. The v2 prompt explicitly
+  defines the event block as hidden machine metadata so those instructions do
+  not suppress event emission.
+
+## Success Criteria
+
+- Direct or naturally expressed volume requests and fresh-data questions cause
+  the matching event instead of an unsupported verbal promise.
+- Response-latency complaints, capability questions, ordinary chat, and
+  incomplete scheduling requests do not produce unrelated events.
+- No protocol, parser, provider-tool mode, EventAgent argument, or event-kernel
+  regression occurs.
+- Old and new EventPrompt snapshots remain safely deduplicated.
+- Deterministic verification passes, and any available live A/B evidence records
+  both recall and false-positive outcomes rather than reporting only successful
+  triggers.

+ 212 - 16
src/agent_lab/presentation/static/app.js

@@ -25,8 +25,10 @@ const eventConfigDialog = document.querySelector("#event-config-dialog");
 const auditDialog = document.querySelector("#audit-dialog");
 const toolInvocationMode = document.querySelector("#tool-invocation-mode");
 const toolModeHelp = document.querySelector("#tool-mode-help");
+const eventAgentModelSettings = document.querySelector("#event-agent-model-settings");
 const WORKSPACE_SNAPSHOTS_STORAGE_KEY = "agent-lab.workspace-snapshots.v1";
 const LEGACY_PROMPT_SETS_STORAGE_KEY = "agent-lab.prompt-sets.v1";
+const CONSOLE_UI_STORAGE_KEY = "agent-lab-console-ui-v1";
 
 let socket = null;
 let activeAssistant = null;
@@ -43,6 +45,7 @@ let auditEntries = [];
 let selectedAuditKey = null;
 let liveAuditSequence = 0;
 let turnActive = false;
+let eventPromptInsertionIndex = 0;
 
 bindClick("#add-system-prompt", () => {
   systemPrompts.append(createSystemPrompt({ content: "" }));
@@ -82,21 +85,124 @@ bindClick("#select-all-tools", () => {
 bindClick("#clear-tools", () => {
   setAllTools(false);
 });
-toolInvocationMode.addEventListener("change", updateToolModeHelp);
+toolInvocationMode.addEventListener("change", refreshToolModeState);
 
 chatForm.addEventListener("submit", (event) => {
   event.preventDefault();
   runDebugSession();
 });
 
+initializeConsoleUi();
 initializePromptList();
-updateToolModeHelp();
 refreshWorkspaceSnapshotSelector();
 loadTools();
 loadSessions({ preserveStatus: true });
 renderAuditReplay([]);
 renderSessionUsage({ calls: [], turns: [], session: {} });
 
+function readConsoleUiState() {
+  try {
+    const state = JSON.parse(localStorage.getItem(CONSOLE_UI_STORAGE_KEY) || "{}");
+    return state && typeof state === "object" && !Array.isArray(state) ? state : {};
+  } catch (error) {
+    return {};
+  }
+}
+
+function writeConsoleUiState(state) {
+  try {
+    localStorage.setItem(CONSOLE_UI_STORAGE_KEY, JSON.stringify(state));
+  } catch (error) {
+    // Presentation preferences are optional when storage is unavailable.
+  }
+}
+
+function setControlSectionExpanded(button, expanded, { persist = true } = {}) {
+  const targetId = button.dataset.collapseTarget;
+  const target = targetId ? document.querySelector(`#${targetId}`) : null;
+  if (!target) {
+    return;
+  }
+  button.setAttribute("aria-expanded", String(expanded));
+  target.hidden = !expanded;
+  if (persist) {
+    const state = readConsoleUiState();
+    const sections = state.sections && typeof state.sections === "object"
+      ? state.sections
+      : {};
+    writeConsoleUiState({
+      ...state,
+      sections: { ...sections, [targetId]: Boolean(expanded) },
+    });
+  }
+}
+
+function setDebugDrawerOpen(open, { persist = true } = {}) {
+  const layout = document.querySelector("#console-layout");
+  const drawer = document.querySelector("#debug-drawer");
+  const button = document.querySelector("#toggle-debug-drawer");
+  if (layout) {
+    layout.classList.toggle("debug-drawer-closed", !open);
+  }
+  if (drawer) {
+    drawer.hidden = !open;
+  }
+  if (button) {
+    button.setAttribute("aria-expanded", String(open));
+    button.textContent = open ? "Hide Debug" : "Show Debug";
+  }
+  if (persist) {
+    writeConsoleUiState({
+      ...readConsoleUiState(),
+      debug_drawer_open: Boolean(open),
+    });
+  }
+}
+
+function initializeConsoleUi() {
+  const state = readConsoleUiState();
+  document.querySelectorAll(".control-section-toggle").forEach((button) => {
+    const targetId = button.dataset.collapseTarget;
+    const storedExpanded = state.sections && typeof state.sections[targetId] === "boolean"
+      ? state.sections[targetId]
+      : button.getAttribute("aria-expanded") === "true";
+    setControlSectionExpanded(button, storedExpanded, { persist: false });
+    button.addEventListener("click", () => {
+      setControlSectionExpanded(
+        button,
+        button.getAttribute("aria-expanded") !== "true",
+      );
+    });
+  });
+
+  const drawerButton = document.querySelector("#toggle-debug-drawer");
+  if (drawerButton) {
+    const storedOpen = typeof state.debug_drawer_open === "boolean"
+      ? state.debug_drawer_open
+      : drawerButton.getAttribute("aria-expanded") === "true";
+    setDebugDrawerOpen(storedOpen, { persist: false });
+    drawerButton.addEventListener("click", () => {
+      setDebugDrawerOpen(drawerButton.getAttribute("aria-expanded") !== "true");
+    });
+  }
+  updateTopbarStatus();
+}
+
+function updateTopbarStatus() {
+  const sessionLabel = document.querySelector("#current-session-label");
+  const modeLabel = document.querySelector("#current-mode-label");
+  if (sessionLabel) {
+    sessionLabel.textContent = currentSessionId
+      ? `Session ${currentSessionId.slice(0, 8)}`
+      : "No session";
+  }
+  if (modeLabel) {
+    modeLabel.textContent = toolInvocationMode.value === "chat_agent_tools"
+      ? "ChatAgent Tools"
+      : "Dual Agent";
+  }
+}
+
 function openChatConfig() {
   chatConfigDialog.showModal();
 }
@@ -180,6 +286,7 @@ async function createSession() {
       }),
     });
     currentSessionId = session.id;
+    updateTopbarStatus();
     sessionTitle.value = session.title;
     await loadSessions({ preserveStatus: true });
     await loadSessionReplay(session.id, { preserveStatus: true });
@@ -202,6 +309,7 @@ async function loadSelectedSession() {
   closeSocket();
   setSessionStatus("Loading session");
   currentSessionId = sessionId;
+  updateTopbarStatus();
   await loadSessionReplay(sessionId);
 }
 
@@ -261,7 +369,7 @@ function renderTools(tools) {
   if (!tools.length) {
     toolList.textContent = "No tools available";
     updateToolCount();
-    updateEventPromptItem(tools);
+    refreshToolModeState();
     return;
   }
 
@@ -280,7 +388,7 @@ function renderTools(tools) {
     checkbox.checked = true;
     checkbox.addEventListener("change", () => {
       updateToolCount();
-      updateEventPromptItem(availableTools);
+      refreshToolModeState();
     });
 
     label.append(checkbox, ` ${tool.name}`);
@@ -302,7 +410,7 @@ function renderTools(tools) {
     pendingEnabledTools = null;
   }
   updateToolCount();
-  updateEventPromptItem(tools);
+  refreshToolModeState();
 }
 
 function formatRequiredParameters(tool) {
@@ -317,7 +425,7 @@ function setAllTools(checked) {
     checkbox.checked = checked;
   });
   updateToolCount();
-  updateEventPromptItem(availableTools);
+  refreshToolModeState();
 }
 
 function updateToolCount() {
@@ -435,8 +543,26 @@ function restoreWorkspaceSnapshot(snapshot) {
     eventAgent.batch_timeout_seconds ?? 15;
   const enabledTools = eventAgent.enabled_tools || [];
   pendingEnabledTools = applySelectedTools(enabledTools) ? null : enabledTools;
-  updateEventPromptItem(availableTools);
+  refreshToolModeState();
+}
+
+function refreshToolModeState() {
+  const isChatAgentTools = toolInvocationMode.value === "chat_agent_tools";
+  updateTopbarStatus();
   updateToolModeHelp();
+  eventAgentModelSettings
+    .querySelectorAll("input, textarea, select, button")
+    .forEach((control) => {
+      control.disabled = isChatAgentTools;
+    });
+  const capturedEventPromptIndex = captureEventPromptInsertionIndex();
+  if (capturedEventPromptIndex !== null) {
+    eventPromptInsertionIndex = capturedEventPromptIndex;
+  }
+  removeGeneratedEventPromptItems();
+  if (!isChatAgentTools) {
+    updateEventPromptItem(availableTools);
+  }
 }
 
 function updateToolModeHelp() {
@@ -475,12 +601,15 @@ function restoreExtraBody(prefix, extraBody = {}) {
 }
 
 function restoreSystemPrompts(prompts) {
+  const restoredEventPromptIndex = deriveEventPromptInsertionIndex(prompts);
+  if (restoredEventPromptIndex !== null) {
+    eventPromptInsertionIndex = restoredEventPromptIndex;
+  }
   systemPrompts.textContent = "";
   const promptItems = normalizePromptItems(prompts);
   promptItems.forEach((prompt) => {
     systemPrompts.append(createSystemPrompt(prompt));
   });
-  ensureEventPromptItem();
 }
 
 function restorePreMessages(messages) {
@@ -573,7 +702,7 @@ function applySelectedTools(toolNames) {
     checkbox.checked = selected.has(checkbox.value);
   });
   updateToolCount();
-  updateEventPromptItem(availableTools);
+  refreshToolModeState();
   return true;
 }
 
@@ -582,7 +711,7 @@ function initializePromptList() {
     bindPromptItemControls(item);
     bindPromptDrag(item);
   });
-  ensureEventPromptItem();
+  refreshToolModeState();
 }
 
 function buildPromptItems() {
@@ -600,7 +729,13 @@ function buildCustomPromptItems() {
 }
 
 function collectSystemPrompts() {
-  return buildPromptItems()
+  const promptItems = buildPromptItems();
+  const serializablePrompts = promptItems.filter((item) => (
+    item.prompt_kind === "event_agent_system"
+      ? toolInvocationMode.value === "dual_agent"
+      : !isGeneratedEventPromptContent(item.content)
+  ));
+  return serializablePrompts
     .map((item) => item.content)
     .filter(Boolean);
 }
@@ -609,10 +744,7 @@ function normalizePromptItems(prompts) {
   const items = Array.isArray(prompts) ? prompts : [];
   const normalized = items
     .map((prompt) => normalizePromptItem(prompt))
-    .filter((prompt) => prompt.prompt_kind === "event_agent_system" || prompt.content);
-  if (!normalized.some((prompt) => prompt.prompt_kind === "event_agent_system")) {
-    normalized.push({ prompt_kind: "event_agent_system", content: "" });
-  }
+    .filter((prompt) => isRetainedCustomPrompt(prompt));
   if (!normalized.some((prompt) => prompt.prompt_kind === "custom")) {
     normalized.unshift({ prompt_kind: "custom", role: "system", content: "" });
   }
@@ -636,6 +768,42 @@ function normalizePromptItem(prompt) {
   };
 }
 
+function deriveEventPromptInsertionIndex(prompts) {
+  const items = Array.isArray(prompts) ? prompts : [];
+  let retainedPromptCount = 0;
+  for (const prompt of items) {
+    const normalized = normalizePromptItem(prompt);
+    if (
+      normalized.prompt_kind === "event_agent_system"
+      || isGeneratedEventPromptContent(normalized.content)
+    ) {
+      return retainedPromptCount;
+    }
+    if (isRetainedCustomPrompt(normalized)) {
+      retainedPromptCount += 1;
+    }
+  }
+  return null;
+}
+
+function isRetainedCustomPrompt(prompt) {
+  return prompt.prompt_kind !== "event_agent_system"
+    && Boolean(prompt.content)
+    && !isGeneratedEventPromptContent(prompt.content);
+}
+
+function isGeneratedEventPromptContent(content) {
+  const normalized = typeof content === "string" ? content.trim() : "";
+  return normalized.startsWith(
+    "You may request EventAgent work using this text protocol.",
+  )
+    && normalized.includes("<agent_events>")
+    && normalized.includes("</agent_events>")
+    && normalized.includes(
+      "Use exact event names only, one per line. Do not include parameters.",
+    );
+}
+
 function buildAvailableEventsPrompt(tools) {
   const eventLines = tools.map((tool) => `- ${tool.name}: ${tool.description || "No description"}`);
   if (!eventLines.length) {
@@ -662,6 +830,31 @@ function updateEventPromptItem(tools) {
   eventPrompt.querySelector(".system-prompt").value = prompt;
 }
 
+function isGeneratedEventPromptItem(item) {
+  const content = item.querySelector(".system-prompt").value;
+  return item.dataset.promptKind === "event_agent_system"
+    || isGeneratedEventPromptContent(content);
+}
+
+function captureEventPromptInsertionIndex() {
+  let retainedPromptCount = 0;
+  for (const item of systemPrompts.querySelectorAll(".prompt-item")) {
+    if (isGeneratedEventPromptItem(item)) {
+      return retainedPromptCount;
+    }
+    retainedPromptCount += 1;
+  }
+  return null;
+}
+
+function removeGeneratedEventPromptItems() {
+  systemPrompts.querySelectorAll(".prompt-item").forEach((item) => {
+    if (isGeneratedEventPromptItem(item)) {
+      item.remove();
+    }
+  });
+}
+
 function ensureEventPromptItem() {
   const existing = systemPrompts.querySelector('[data-prompt-kind="event_agent_system"]');
   if (existing) {
@@ -672,7 +865,9 @@ function ensureEventPromptItem() {
     prompt_kind: "event_agent_system",
     content: "",
   });
-  systemPrompts.append(item);
+  const promptItems = [...systemPrompts.querySelectorAll(".prompt-item")];
+  const insertionTarget = promptItems[eventPromptInsertionIndex] || null;
+  systemPrompts.insertBefore(item, insertionTarget);
   return item;
 }
 
@@ -907,6 +1102,7 @@ function handleServerMessage(message) {
   if (message.type === "session_started") {
     if (message.session_id) {
       currentSessionId = message.session_id;
+      updateTopbarStatus();
       loadSessions({ preserveStatus: true });
     }
     appendLog("session", "Session started");

+ 148 - 91
src/agent_lab/presentation/static/index.html

@@ -4,107 +4,137 @@
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <title>Agent Lab</title>
-    <link rel="stylesheet" href="/static/styles.css?v=20260713-tool-mode-session-restore" />
+    <link rel="stylesheet" href="/static/styles.css?v=20260714-professional-console" />
   </head>
   <body>
     <header class="topbar">
-      <h1>Agent Lab</h1>
-      <div class="top-actions">
-        <div id="connection-status" class="status">Idle</div>
+      <div class="brand-block">
+        <h1>Agent Lab</h1>
+        <span>Chat and event runtime console</span>
+      </div>
+      <div class="top-status" aria-label="Runtime status">
+        <span id="current-session-label" class="status-badge">No session</span>
+        <span id="current-mode-label" class="status-badge">Dual Agent</span>
+        <span id="connection-status" class="status-badge status-neutral">Idle</span>
+        <button
+          id="toggle-debug-drawer"
+          class="button-secondary"
+          type="button"
+          aria-expanded="true"
+          aria-controls="debug-drawer"
+        >Hide Debug</button>
       </div>
     </header>
 
-    <main class="layout">
-      <aside class="panel controls">
-        <section class="session-section">
-          <div class="section-head">
-            <h2>Sessions</h2>
-            <button id="refresh-sessions" type="button">Refresh</button>
-          </div>
-          <label>Title <input id="session-title" placeholder="debug session" /></label>
-          <label>
-            Saved Session
-            <select id="session-list"></select>
-          </label>
-          <div class="button-row two">
-            <button id="new-session" type="button">New</button>
-            <button id="load-session" type="button">Load</button>
+    <main id="console-layout" class="layout">
+      <aside class="controls" aria-label="Runtime controls">
+        <section class="control-section">
+          <button
+            class="control-section-toggle"
+            type="button"
+            data-collapse-target="session-controls"
+            aria-controls="session-controls"
+            aria-expanded="true"
+          >
+            <span>Session</span>
+          </button>
+          <div id="session-controls" class="control-section-body">
+            <div class="section-head">
+              <span class="section-kicker">Runtime identity</span>
+              <button id="refresh-sessions" class="button-secondary button-compact" type="button">Refresh</button>
+            </div>
+            <label>Title <input id="session-title" placeholder="debug session" /></label>
+            <label>
+              Saved Session
+              <select id="session-list"></select>
+            </label>
+            <div class="button-row two">
+              <button id="new-session" class="button-primary" type="button">New</button>
+              <button id="load-session" class="button-secondary" type="button">Load</button>
+            </div>
+            <div id="session-status" class="session-status">Ready</div>
           </div>
-          <div id="session-status" class="session-status">Ready</div>
         </section>
 
-        <section class="tools-section">
-          <div class="section-head">
-            <h2>Tools <span id="tool-count">0 selected</span></h2>
-            <div class="button-row compact">
-              <button id="select-all-tools" type="button">All</button>
-              <button id="clear-tools" type="button">None</button>
+        <section class="control-section">
+          <button
+            class="control-section-toggle"
+            type="button"
+            data-collapse-target="tool-controls"
+            aria-controls="tool-controls"
+            aria-expanded="true"
+          >
+            <span>Tools</span>
+            <span id="tool-count" class="status-badge">0 selected</span>
+          </button>
+          <div id="tool-controls" class="control-section-body">
+            <div class="section-head">
+              <span class="section-kicker">Enabled runtime tools</span>
+              <div class="button-row compact">
+                <button id="select-all-tools" class="button-secondary button-compact" type="button">All</button>
+                <button id="clear-tools" class="button-secondary button-compact" type="button">None</button>
+              </div>
             </div>
+            <div id="tool-list" class="stack">Loading tools...</div>
           </div>
-          <div id="tool-list" class="stack">Loading tools...</div>
         </section>
 
-        <section>
-          <h2>Agent Config</h2>
-          <label>
-            Tool Invocation Mode
-            <select id="tool-invocation-mode">
-              <option value="dual_agent" selected>Dual Agent — fast reply + EventAgent</option>
-              <option value="chat_agent_tools">ChatAgent Tools — native provider tools</option>
-            </select>
-          </label>
-          <p id="tool-mode-help">
-            Dual Agent may use the EventAgent model, system prompt, and extra body for missing-argument fallback.
-          </p>
-          <div class="button-row two">
-            <button id="open-chat-config-panel" type="button">Chat Agent</button>
-            <button id="open-event-config-panel" type="button">Event Agent</button>
+        <section class="control-section">
+          <button
+            class="control-section-toggle"
+            type="button"
+            data-collapse-target="agent-config-controls"
+            aria-controls="agent-config-controls"
+            aria-expanded="false"
+          >
+            <span>Agent Config</span>
+          </button>
+          <div id="agent-config-controls" class="control-section-body" hidden>
+            <label>
+              Tool Invocation Mode
+              <select id="tool-invocation-mode">
+                <option value="dual_agent" selected>Dual Agent — fast reply + EventAgent</option>
+                <option value="chat_agent_tools">ChatAgent Tools — native provider tools</option>
+              </select>
+            </label>
+            <p id="tool-mode-help" class="control-help">
+              Dual Agent may use the EventAgent model, system prompt, and extra body for missing-argument fallback.
+            </p>
+            <div class="button-row two">
+              <button id="open-chat-config-panel" class="button-secondary" type="button">Chat Agent</button>
+              <button id="open-event-config-panel" class="button-secondary" type="button">Event Agent</button>
+            </div>
           </div>
         </section>
 
-        <section class="snapshot-section">
-          <h2>Workspace Snapshot</h2>
-          <label>Name <input id="workspace-snapshot-name" placeholder="debug workspace" /></label>
-          <label>
-            Saved Snapshot
-            <select id="saved-workspace-snapshots"></select>
-          </label>
-          <div class="button-row">
-            <button id="save-workspace-snapshot" type="button">Save</button>
-            <button id="load-workspace-snapshot" type="button">Load</button>
-            <button id="delete-workspace-snapshot" type="button">Delete</button>
+        <section class="control-section">
+          <button
+            class="control-section-toggle"
+            type="button"
+            data-collapse-target="snapshot-controls"
+            aria-controls="snapshot-controls"
+            aria-expanded="false"
+          >
+            <span>Snapshot</span>
+          </button>
+          <div id="snapshot-controls" class="control-section-body" hidden>
+            <label>Name <input id="workspace-snapshot-name" placeholder="debug workspace" /></label>
+            <label>
+              Saved Snapshot
+              <select id="saved-workspace-snapshots"></select>
+            </label>
+            <div class="button-row">
+              <button id="save-workspace-snapshot" class="button-primary" type="button">Save</button>
+              <button id="load-workspace-snapshot" class="button-secondary" type="button">Load</button>
+              <button id="delete-workspace-snapshot" class="button-danger" type="button">Delete</button>
+            </div>
           </div>
         </section>
-
       </aside>
 
-      <section class="workspace">
+      <section class="workspace" aria-label="Conversation workspace">
         <div class="workspace-body">
           <div id="messages" class="messages"></div>
-          <aside class="replay-panel">
-            <section class="audit-launch-section">
-              <div class="section-head">
-                <h2>Audit Replay</h2>
-                <span id="audit-count" class="meta-count">0 entries</span>
-              </div>
-              <p id="audit-summary" class="audit-summary">No audit entries</p>
-              <button id="open-audit-dialog" type="button">Open Audit Replay</button>
-            </section>
-            <section class="session-usage-section">
-              <h2>Session Usage</h2>
-              <div id="session-usage-summary" class="usage-summary"></div>
-              <div class="usage-groups">
-                <div>
-                  <h3>Turns</h3>
-                  <div id="session-usage-turns" class="replay-list compact"></div>
-                </div>
-                <div>
-                  <h3>Calls</h3>
-                  <div id="session-usage-calls" class="replay-list compact"></div>
-                </div>
-              </div>
-            </section>
-          </aside>
         </div>
 
         <div class="stats">
@@ -116,9 +146,34 @@
 
         <form id="chat-form" class="composer">
           <textarea id="user-message" rows="3" placeholder="Send a debug message"></textarea>
-          <button id="run-button" type="submit">Run</button>
+          <button id="run-button" class="button-primary" type="submit">Run</button>
         </form>
       </section>
+
+      <aside id="debug-drawer" class="debug-drawer" aria-label="Audit and usage">
+        <section class="drawer-section audit-launch-section">
+          <div class="section-head">
+            <h2>Audit Replay</h2>
+            <span id="audit-count" class="meta-count">0 entries</span>
+          </div>
+          <p id="audit-summary" class="audit-summary">No audit entries</p>
+          <button id="open-audit-dialog" class="button-primary" type="button">Open Audit Replay</button>
+        </section>
+        <section class="drawer-section session-usage-section">
+          <h2>Session Usage</h2>
+          <div id="session-usage-summary" class="usage-summary"></div>
+          <div class="usage-groups">
+            <div>
+              <h3>Turns</h3>
+              <div id="session-usage-turns" class="replay-list compact"></div>
+            </div>
+            <div>
+              <h3>Calls</h3>
+              <div id="session-usage-calls" class="replay-list compact"></div>
+            </div>
+          </div>
+        </section>
+      </aside>
     </main>
 
     <dialog id="audit-dialog" class="modal audit-modal">
@@ -216,19 +271,21 @@
           <button id="close-event-config" type="button">Close</button>
         </div>
         <section>
-          <label>Model <input id="event-model" placeholder="Same as ChatAgent" /></label>
-          <label>Temperature <input id="event-temperature" type="number" min="0" max="2" step="0.1" value="0.2" /></label>
-          <label>Max Tokens <input id="event-max-tokens" type="number" min="1" step="1" value="800" /></label>
-          <label>System Prompt <textarea id="event-system-prompt" rows="5" placeholder="Optional EventAgent system prompt"></textarea></label>
+          <div id="event-agent-model-settings">
+            <label>Model <input id="event-model" placeholder="Same as ChatAgent" /></label>
+            <label>Temperature <input id="event-temperature" type="number" min="0" max="2" step="0.1" value="0.2" /></label>
+            <label>Max Tokens <input id="event-max-tokens" type="number" min="1" step="1" value="800" /></label>
+            <label>System Prompt <textarea id="event-system-prompt" rows="5" placeholder="Optional EventAgent system prompt"></textarea></label>
+            <fieldset>
+              <legend>Extra Body</legend>
+              <label class="checkbox"><input id="event-thinking-disabled" type="checkbox" checked /> Disable thinking</label>
+              <label class="checkbox"><input id="event-enable-search" type="checkbox" /> Enable search</label>
+              <label class="checkbox"><input id="event-forced-search" type="checkbox" /> Forced search</label>
+            </fieldset>
+          </div>
           <label>Max Loops <input id="max-event-loops" type="number" min="1" step="1" value="1" /></label>
           <label>Max Parallel <input id="max-parallel-events" type="number" min="1" max="16" step="1" value="4" /></label>
           <label>Batch Timeout (seconds) <input id="batch-timeout-seconds" type="number" min="0.1" max="120" step="0.1" value="15" /></label>
-          <fieldset>
-            <legend>Extra Body</legend>
-            <label class="checkbox"><input id="event-thinking-disabled" type="checkbox" checked /> Disable thinking</label>
-            <label class="checkbox"><input id="event-enable-search" type="checkbox" /> Enable search</label>
-            <label class="checkbox"><input id="event-forced-search" type="checkbox" /> Forced search</label>
-          </fieldset>
         </section>
       </form>
     </dialog>
@@ -244,6 +301,6 @@
       </div>
     </template>
 
-    <script src="/static/app.js?v=20260713-tool-mode-session-restore"></script>
+    <script src="/static/app.js?v=20260714-professional-console"></script>
   </body>
 </html>

+ 364 - 143
src/agent_lab/presentation/static/styles.css

@@ -1,6 +1,17 @@
 :root {
-  color: #1f2933;
-  background: #eef2f5;
+  --canvas: #f3f5f7;
+  --surface: #ffffff;
+  --surface-subtle: #f8fafc;
+  --border: #d9e0e7;
+  --text: #17212b;
+  --muted: #647180;
+  --accent: #176b87;
+  --accent-strong: #0f536a;
+  --danger: #b42318;
+  --radius: 10px;
+  --shadow: 0 8px 24px rgb(23 33 43 / 8%);
+  color: var(--text);
+  background: var(--canvas);
   font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
 }
 
@@ -9,12 +20,18 @@
 }
 
 body {
+  background: var(--canvas);
+  color: var(--text);
   height: 100vh;
   margin: 0;
   min-height: 100vh;
   overflow: hidden;
 }
 
+[hidden] {
+  display: none !important;
+}
+
 button,
 input,
 select,
@@ -23,16 +40,57 @@ textarea {
 }
 
 button {
-  border: 0;
-  border-radius: 6px;
-  background: #0f766e;
-  color: white;
+  align-items: center;
+  background: var(--accent);
+  border: 1px solid var(--accent);
+  border-radius: 7px;
+  color: #ffffff;
   cursor: pointer;
+  display: inline-flex;
+  font-weight: 650;
+  justify-content: center;
+  min-height: 36px;
   padding: 8px 12px;
 }
 
-button:hover {
-  background: #115e59;
+.button-primary {
+  background: var(--accent);
+  border-color: var(--accent);
+  color: #ffffff;
+}
+
+.button-primary:hover,
+button:not([class]):hover {
+  background: var(--accent-strong);
+  border-color: var(--accent-strong);
+}
+
+.button-secondary {
+  background: var(--surface);
+  border-color: var(--border);
+  color: var(--text);
+}
+
+.button-secondary:hover {
+  background: var(--surface-subtle);
+  border-color: #aeb9c4;
+}
+
+.button-danger {
+  background: #ffffff;
+  border-color: #e6b5b0;
+  color: var(--danger);
+}
+
+.button-danger:hover {
+  background: #fff5f4;
+  border-color: var(--danger);
+}
+
+.button-compact {
+  font-size: 12px;
+  min-height: 30px;
+  padding: 5px 9px;
 }
 
 button:disabled {
@@ -40,78 +98,156 @@ button:disabled {
   opacity: 0.55;
 }
 
+button:focus-visible,
+input:focus-visible,
+select:focus-visible,
+textarea:focus-visible,
+[tabindex]:focus-visible {
+  outline: 3px solid rgb(23 107 135 / 24%);
+  outline-offset: 2px;
+}
+
 .topbar {
   align-items: center;
-  background: #ffffff;
-  border-bottom: 1px solid #d7dee5;
+  background: var(--surface);
+  border-bottom: 1px solid var(--border);
   display: flex;
   justify-content: space-between;
-  min-height: 56px;
-  padding: 0 20px;
+  min-height: 64px;
+  padding: 8px 18px;
+}
+
+.brand-block {
+  display: grid;
+  gap: 2px;
 }
 
-.topbar h1 {
-  font-size: 18px;
+.brand-block h1 {
+  font-size: 17px;
+  letter-spacing: -0.01em;
   margin: 0;
 }
 
-.top-actions {
+.brand-block span {
+  color: var(--muted);
+  font-size: 12px;
+}
+
+.top-status {
   align-items: center;
   display: flex;
   flex-wrap: wrap;
-  gap: 10px;
+  gap: 8px;
   justify-content: flex-end;
 }
 
-.status {
-  background: #e0f2f1;
-  border: 1px solid #b2dfdb;
+.status-badge {
+  background: #eaf4f7;
+  border: 1px solid #c8e0e7;
   border-radius: 999px;
-  color: #0f766e;
-  font-size: 13px;
-  padding: 4px 10px;
+  color: var(--accent-strong);
+  font-size: 11px;
+  font-weight: 700;
+  line-height: 1;
+  padding: 5px 8px;
+  white-space: nowrap;
+}
+
+.status-neutral {
+  background: var(--surface-subtle);
+  border-color: var(--border);
+  color: var(--muted);
 }
 
 .layout {
   display: grid;
-  gap: 16px;
-  grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
-  height: calc(100vh - 56px);
+  grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) minmax(300px, 380px);
+  height: calc(100vh - 64px);
   min-height: 0;
   overflow: hidden;
-  padding: 16px;
 }
 
-.panel,
-.workspace {
-  background: #ffffff;
-  border: 1px solid #d7dee5;
-  border-radius: 8px;
+.layout.debug-drawer-closed {
+  grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) 0;
 }
 
 .controls {
+  background: var(--surface);
+  border-right: 1px solid var(--border);
   min-height: 0;
   overflow: auto;
-  padding: 16px;
+  overscroll-behavior: contain;
 }
 
-section + section {
-  border-top: 1px solid #e4e9ee;
-  margin-top: 18px;
-  padding-top: 18px;
+.control-section {
+  border-bottom: 1px solid var(--border);
+}
+
+.control-section-toggle {
+  background: var(--surface);
+  border: 0;
+  border-radius: 0;
+  color: var(--text);
+  justify-content: space-between;
+  min-height: 48px;
+  padding: 10px 14px;
+  text-align: left;
+  width: 100%;
+}
+
+.control-section-toggle:hover {
+  background: var(--surface-subtle);
+}
+
+.control-section-toggle > span:first-child {
+  font-size: 13px;
+  font-weight: 750;
+  letter-spacing: 0.01em;
+}
+
+.control-section-toggle::after {
+  border-bottom: 2px solid var(--muted);
+  border-right: 2px solid var(--muted);
+  content: "";
+  height: 7px;
+  margin-left: 8px;
+  transform: rotate(45deg);
+  transition: transform 120ms ease;
+  width: 7px;
+}
+
+.control-section-toggle[aria-expanded="false"]::after {
+  transform: rotate(-45deg);
+}
+
+.control-section-body {
+  padding: 0 14px 14px;
+}
+
+.control-section-body > :first-child {
+  margin-top: 0;
 }
 
 h2 {
-  font-size: 14px;
-  margin: 0 0 10px;
+  font-size: 13px;
+  margin: 0;
 }
 
 .section-head {
   align-items: center;
   display: flex;
+  gap: 8px;
   justify-content: space-between;
 }
 
+.section-kicker {
+  color: var(--muted);
+  font-size: 11px;
+  font-weight: 650;
+  letter-spacing: 0.04em;
+  text-transform: uppercase;
+}
+
 .stack {
   display: grid;
   gap: 10px;
@@ -133,21 +269,20 @@ h2 {
   grid-template-columns: repeat(2, minmax(0, 1fr));
 }
 
-.session-section .section-head button {
-  background: #eef2f5;
-  border: 1px solid #cbd5df;
-  color: #1f2933;
-  font-size: 12px;
-  padding: 5px 8px;
-}
-
 .session-status {
-  color: #52616f;
+  color: var(--muted);
   font-size: 12px;
   margin-top: 8px;
   min-height: 16px;
 }
 
+.control-help {
+  color: var(--muted);
+  font-size: 12px;
+  line-height: 1.45;
+  margin: 10px 0 0;
+}
+
 label {
   display: grid;
   gap: 6px;
@@ -155,14 +290,14 @@ label {
 }
 
 fieldset {
-  border: 1px solid #d7dee5;
-  border-radius: 6px;
+  border: 1px solid var(--border);
+  border-radius: 7px;
   margin: 12px 0 0;
   padding: 8px 10px 10px;
 }
 
 legend {
-  color: #52616f;
+  color: var(--muted);
   font-size: 12px;
   padding: 0 4px;
 }
@@ -180,9 +315,10 @@ legend {
 input,
 select,
 textarea {
-  border: 1px solid #cbd5df;
-  border-radius: 6px;
-  color: #1f2933;
+  background: var(--surface);
+  border: 1px solid #bdc8d2;
+  border-radius: 7px;
+  color: var(--text);
   padding: 8px;
   width: 100%;
 }
@@ -198,9 +334,9 @@ textarea {
 }
 
 .prompt-item {
-  background: #fbfcfd;
-  border: 1px solid #d7dee5;
-  border-radius: 6px;
+  background: var(--surface-subtle);
+  border: 1px solid var(--border);
+  border-radius: 7px;
   cursor: grab;
   padding: 8px;
 }
@@ -226,16 +362,16 @@ textarea {
 }
 
 .prompt-title {
-  color: #52616f;
+  color: var(--muted);
   font-size: 12px;
   font-weight: 600;
 }
 
 .prompt-type {
-  background: #e0f2f1;
-  border: 1px solid #b2dfdb;
+  background: #eaf4f7;
+  border: 1px solid #c8e0e7;
   border-radius: 999px;
-  color: #0f766e;
+  color: var(--accent-strong);
   font-size: 11px;
   font-weight: 700;
   line-height: 1;
@@ -251,16 +387,16 @@ textarea {
 }
 
 .prompt-actions button {
-  background: #eef2f5;
-  border: 1px solid #cbd5df;
-  color: #1f2933;
+  background: var(--surface);
+  border: 1px solid var(--border);
+  color: var(--text);
   font-size: 12px;
   line-height: 1.1;
   padding: 4px 7px;
 }
 
 .prompt-actions button:hover {
-  background: #e4e9ee;
+  background: var(--surface-subtle);
 }
 
 .prompt-item textarea {
@@ -270,10 +406,12 @@ textarea {
 }
 
 .event-prompt textarea {
-  background: #f7f9fb;
+  background: var(--surface-subtle);
 }
 
 .workspace {
+  background: var(--surface);
+  border-right: 1px solid var(--border);
   display: grid;
   grid-template-rows: minmax(0, 1fr) auto auto;
   min-height: 0;
@@ -283,7 +421,7 @@ textarea {
 
 .workspace-body {
   display: grid;
-  grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
+  grid-template-rows: minmax(0, 1fr);
   min-height: 0;
   overflow: hidden;
 }
@@ -294,19 +432,25 @@ textarea {
   gap: 10px;
   min-height: 0;
   overflow: auto;
-  padding: 16px;
+  overscroll-behavior: contain;
+  padding: 20px clamp(16px, 3vw, 36px);
 }
 
-.replay-panel {
-  border-left: 1px solid #e4e9ee;
-  display: grid;
-  gap: 16px;
-  grid-template-rows: auto minmax(0, 1fr);
+.debug-drawer {
+  background: var(--surface-subtle);
   min-height: 0;
-  overflow: hidden;
+  min-width: 0;
+  overflow: auto;
+  overscroll-behavior: contain;
   padding: 16px;
 }
 
+.drawer-section + .drawer-section {
+  border-top: 1px solid var(--border);
+  margin-top: 18px;
+  padding-top: 18px;
+}
+
 .audit-launch-section {
   display: grid;
   gap: 10px;
@@ -317,10 +461,10 @@ textarea {
 }
 
 .audit-summary {
-  background: #f7f9fb;
-  border: 1px solid #e4e9ee;
-  border-radius: 6px;
-  color: #52616f;
+  background: var(--surface);
+  border: 1px solid var(--border);
+  border-radius: 7px;
+  color: var(--muted);
   font-size: 12px;
   line-height: 1.45;
   margin: 0;
@@ -343,50 +487,56 @@ textarea {
 
 .session-usage-section {
   min-height: 0;
-  overflow: auto;
 }
 
 .meta-count {
-  color: #697586;
+  color: var(--muted);
   font-size: 12px;
 }
 
 .message {
+  background: var(--surface-subtle);
+  border: 1px solid var(--border);
   border-radius: 8px;
+  color: var(--text);
   line-height: 1.5;
-  max-width: 900px;
+  max-width: min(86%, 900px);
   padding: 10px 12px;
   white-space: pre-wrap;
   word-break: break-word;
 }
 
-.assistant {
-  background: #edf7f6;
-  border: 1px solid #b7ddda;
+.message.assistant {
+  background: #f5fafb;
+  border-color: #c8e0e7;
 }
 
-.user {
+.message.user {
   align-self: flex-end;
-  background: #eff6ff;
-  border: 1px solid #bfdbfe;
+  background: #eef6f9;
+  border-color: #b9d7e0;
 }
 
-.session {
-  background: #f4f6f8;
-  color: #52616f;
+.message.session {
+  align-self: center;
+  background: transparent;
+  border-color: transparent;
+  color: var(--muted);
+  font-size: 12px;
+  padding-block: 4px;
 }
 
-.event {
-  background: #fff7ed;
-  border: 1px solid #fed7aa;
+.message.event {
+  background: #fff9f2;
+  border-color: #ead8bc;
 }
 
-.tool {
-  background: #f0fdf4;
-  border: 1px solid #bbf7d0;
+.message.tool {
+  background: #f4faf6;
+  border-color: #c9dfcf;
 }
 
-.audit {
+.message.audit {
   background: #eef2ff;
   border: 1px solid #c7d2fe;
   color: #3730a3;
@@ -411,10 +561,10 @@ textarea {
 }
 
 .replay-row {
-  background: #f7f9fb;
-  border: 1px solid #e4e9ee;
-  border-radius: 6px;
-  color: #1f2933;
+  background: var(--surface);
+  border: 1px solid var(--border);
+  border-radius: 7px;
+  color: var(--text);
   font-size: 12px;
   line-height: 1.45;
   padding: 8px;
@@ -424,10 +574,10 @@ textarea {
 
 .audit-event {
   align-items: stretch;
-  background: #ffffff;
-  border: 1px solid #d7dee5;
-  border-radius: 6px;
-  color: #1f2933;
+  background: var(--surface);
+  border: 1px solid var(--border);
+  border-radius: 7px;
+  color: var(--text);
   display: grid;
   gap: 8px;
   grid-template-columns: 14px minmax(0, 1fr);
@@ -444,7 +594,7 @@ textarea {
 }
 
 .audit-event.is-selected {
-  box-shadow: inset 3px 0 0 #0f766e;
+  box-shadow: inset 3px 0 0 var(--accent);
 }
 
 .audit-marker {
@@ -490,7 +640,7 @@ textarea {
 }
 
 .audit-kind-chat .audit-marker::before {
-  background: #0f766e;
+  background: var(--accent);
 }
 
 .audit-event-body {
@@ -511,9 +661,9 @@ textarea {
 }
 
 .audit-detail {
-  background: #fbfcfd;
-  border: 1px solid #e4e9ee;
-  border-radius: 6px;
+  background: var(--surface-subtle);
+  border: 1px solid var(--border);
+  border-radius: 7px;
   display: block;
   margin-top: 10px;
   min-height: 0;
@@ -529,7 +679,7 @@ textarea {
 
 .audit-detail-title,
 .audit-detail-block-title {
-  color: #1f2933;
+  color: var(--text);
   font-size: 13px;
   line-height: 1.25;
   margin: 0;
@@ -683,9 +833,9 @@ textarea {
 }
 
 .usage-summary div {
-  background: #f7f9fb;
-  border: 1px solid #e4e9ee;
-  border-radius: 6px;
+  background: var(--surface);
+  border: 1px solid var(--border);
+  border-radius: 7px;
   display: grid;
   gap: 2px;
   padding: 8px;
@@ -708,35 +858,51 @@ textarea {
   margin-top: 14px;
 }
 
-.error {
+.message.error {
   background: #fef2f2;
-  border: 1px solid #fecaca;
-  color: #991b1b;
+  border-color: #f3b7b2;
+  color: var(--danger);
 }
 
 .tool-card {
-  border: 1px solid #e4e9ee;
-  border-radius: 6px;
-  padding: 10px;
+  background: var(--surface);
+  border: 1px solid var(--border);
+  border-radius: 7px;
+  padding: 8px 9px;
+}
+
+.tool-card:has(input:checked) {
+  background: #f1f8fa;
+  border-color: #8fbcc9;
+  box-shadow: inset 3px 0 0 var(--accent);
+}
+
+.tool-card .checkbox {
+  font-size: 12px;
+  font-weight: 700;
+  margin-top: 0;
 }
 
 .tool-description,
 .tool-required {
-  color: #52616f;
-  font-size: 12px;
-  line-height: 1.4;
-  margin-top: 6px;
+  color: var(--muted);
+  font-size: 11px;
+  line-height: 1.35;
+  margin-top: 4px;
 }
 
 #tool-count {
-  color: #697586;
-  font-size: 12px;
-  font-weight: 400;
+  font-size: 11px;
+}
+
+#tool-list {
+  margin-top: 10px;
 }
 
 .modal {
-  border: 0;
-  border-radius: 8px;
+  border: 1px solid var(--border);
+  border-radius: var(--radius);
+  box-shadow: var(--shadow);
   max-height: min(720px, calc(100vh - 48px));
   max-width: min(760px, calc(100vw - 32px));
   padding: 0;
@@ -755,12 +921,19 @@ textarea {
 }
 
 .modal-shell {
+  background: var(--surface);
   display: grid;
   max-height: min(720px, calc(100vh - 48px));
   overflow: auto;
   padding: 16px;
 }
 
+.modal-shell > section + section {
+  border-top: 1px solid var(--border);
+  margin-top: 18px;
+  padding-top: 18px;
+}
+
 .audit-modal .modal-shell {
   grid-template-rows: auto minmax(0, 1fr);
   height: 100%;
@@ -774,6 +947,16 @@ textarea {
   justify-content: space-between;
 }
 
+.modal-head button {
+  background: var(--surface);
+  border-color: var(--border);
+  color: var(--text);
+}
+
+.modal-head button:hover {
+  background: var(--surface-subtle);
+}
+
 .audit-modal-body {
   display: grid;
   gap: 16px;
@@ -782,21 +965,21 @@ textarea {
 }
 
 .stats {
-  border-top: 1px solid #e4e9ee;
+  border-top: 1px solid var(--border);
   display: grid;
   gap: 1px;
   grid-template-columns: repeat(4, minmax(0, 1fr));
 }
 
 .stats div {
-  background: #f7f9fb;
+  background: var(--surface-subtle);
   display: grid;
   gap: 4px;
   padding: 10px 12px;
 }
 
 .stats span {
-  color: #697586;
+  color: var(--muted);
   font-size: 12px;
 }
 
@@ -805,45 +988,83 @@ textarea {
 }
 
 .composer {
-  border-top: 1px solid #e4e9ee;
+  background: var(--surface);
+  border-top: 1px solid var(--border);
   display: grid;
   gap: 12px;
   grid-template-columns: minmax(0, 1fr) auto;
   padding: 12px;
 }
 
-@media (max-width: 860px) {
+.composer textarea {
+  max-height: 240px;
+  min-height: 72px;
+}
+
+@media (max-width: 900px) {
+  html,
   body {
     height: auto;
-    overflow: auto;
+    max-width: 100%;
+    overflow-x: hidden;
   }
 
-  .layout {
-    grid-template-columns: 1fr;
+  body {
+    min-height: 100vh;
+    overflow-y: auto;
+  }
+
+  .topbar {
+    align-items: flex-start;
+    flex-wrap: wrap;
+    gap: 10px;
+  }
+
+  .top-status {
+    justify-content: flex-start;
+  }
+
+  .layout,
+  .layout.debug-drawer-closed {
+    grid-template-columns: minmax(0, 1fr);
     height: auto;
     overflow: visible;
   }
 
+  .controls,
+  .debug-drawer {
+    border-bottom: 1px solid var(--border);
+    border-right: 0;
+    max-height: none;
+    overflow: visible;
+  }
+
   .workspace {
-    min-height: 70vh;
+    border-bottom: 1px solid var(--border);
+    border-right: 0;
+    overflow: visible;
+  }
+
+  .workspace-body {
+    min-height: 420px;
+    overflow: visible;
+  }
+
+  .messages {
     overflow: visible;
   }
 
   .stats,
   .composer,
   .pre-message,
-  .workspace-body,
   .audit-modal-body,
   .usage-summary,
   .button-row {
     grid-template-columns: 1fr;
   }
 
-  .replay-panel {
-    border-left: 0;
-    border-top: 1px solid #e4e9ee;
-    grid-template-rows: auto auto;
-    overflow: visible;
+  .debug-drawer[hidden] {
+    display: none !important;
   }
 
   .audit-modal .modal-shell {

File diff ditekan karena terlalu besar
+ 1120 - 0
tests/test_websocket_api.py


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini