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.
src/agent_lab/presentation/static/app.js: EventPrompt derivation, restore cleanup, mode-aware prompt collection, collapsible sections, debug drawer, status indicators.src/agent_lab/presentation/static/index.html: console layout, status bar, collapsible control sections, right debug drawer, mode-dependent EventAgent groups.src/agent_lab/presentation/static/styles.css: design tokens, console shell, messages, controls, drawer, responsive layout.tests/test_websocket_api.py: focused frontend source-contract regressions.README.md: operator-facing console and EventPrompt behavior.docs/plans/todos.md: close Todo 53 only after all verification succeeds.Files:
tests/test_websocket_api.py:1364-1517src/agent_lab/presentation/static/app.js:85-93,259-320,413-450,477-677,867-897Modify: src/agent_lab/presentation/static/index.html:212-233
[x] Step 1: Write failing source-contract tests
Add these assertions in focused tests:
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
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.
Add:
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.
Implement:
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:
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
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
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."
Files:
tests/test_websocket_api.py:1334-1607src/agent_lab/presentation/static/index.html:7-247Modify: 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:
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
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.
Use this hierarchy:
<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.
Start with these tokens and layout rules:
: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
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
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."
Files:
tests/test_websocket_api.py:1396-1620Modify: src/agent_lab/presentation/static/app.js:1-120 and session lifecycle functions
[x] Step 1: Write failing persistence/status tests
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
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
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
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
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
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."
Files:
README.md:94-102docs/plans/todos.md:117Modify: 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:
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
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.
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.
At width <= 900px, verify vertical stacking, no horizontal scrollbar, usable messages/metrics/composer/tools/modals, and removal of the drawer region when closed.
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
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."
[ ] 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.