Browse Source

feat: redesign the debug console layout

Problem: chat lacked visual priority and Audit/Usage permanently consumed workspace width.\n\nRisk: existing DOM IDs are retained to preserve runtime bindings.
zhenyu.hu 2 weeks ago
parent
commit
fd9c31eb39

+ 136 - 81
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">
@@ -246,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 {

+ 260 - 8
tests/test_websocket_api.py

@@ -3,6 +3,7 @@ import asyncio
 import logging
 import subprocess
 from collections.abc import AsyncIterator
+from html.parser import HTMLParser
 from pathlib import Path
 
 import httpx
@@ -27,6 +28,99 @@ def _css_rule(css: str, selector: str) -> str:
     return css[start:end]
 
 
+class _HtmlNode:
+    def __init__(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+        self.tag = tag
+        self.attrs = dict(attrs)
+        self.children: list["_HtmlNode"] = []
+        self.text_parts: list[str] = []
+
+    @property
+    def classes(self) -> set[str]:
+        return set(self.attrs.get("class", "").split())
+
+    def descendants(self) -> list["_HtmlNode"]:
+        nodes: list["_HtmlNode"] = []
+        for child in self.children:
+            nodes.append(child)
+            nodes.extend(child.descendants())
+        return nodes
+
+    def text_content(self) -> str:
+        return "".join(
+            [*self.text_parts, *(child.text_content() for child in self.children)]
+        )
+
+
+class _HtmlTreeParser(HTMLParser):
+    _void_tags = {
+        "area",
+        "base",
+        "br",
+        "col",
+        "embed",
+        "hr",
+        "img",
+        "input",
+        "link",
+        "meta",
+        "param",
+        "source",
+        "track",
+        "wbr",
+    }
+
+    def __init__(self) -> None:
+        super().__init__()
+        self.root = _HtmlNode("document", [])
+        self.stack = [self.root]
+
+    def handle_starttag(
+        self,
+        tag: str,
+        attrs: list[tuple[str, str | None]],
+    ) -> None:
+        node = _HtmlNode(tag, attrs)
+        self.stack[-1].children.append(node)
+        if tag not in self._void_tags:
+            self.stack.append(node)
+
+    def handle_startendtag(
+        self,
+        tag: str,
+        attrs: list[tuple[str, str | None]],
+    ) -> None:
+        self.stack[-1].children.append(_HtmlNode(tag, attrs))
+
+    def handle_endtag(self, tag: str) -> None:
+        for index in range(len(self.stack) - 1, 0, -1):
+            if self.stack[index].tag == tag:
+                del self.stack[index:]
+                return
+
+    def handle_data(self, data: str) -> None:
+        self.stack[-1].text_parts.append(data)
+
+
+def _parse_html(html: str) -> _HtmlNode:
+    parser = _HtmlTreeParser()
+    parser.feed(html)
+    return parser.root
+
+
+def _html_node_by_id(root: _HtmlNode, element_id: str) -> _HtmlNode:
+    node = next(
+        (
+            node
+            for node in [root, *root.descendants()]
+            if node.attrs.get("id") == element_id
+        ),
+        None,
+    )
+    assert node is not None, f"missing HTML element #{element_id}"
+    return node
+
+
 def _run_node_json(source: str) -> object:
     completed = subprocess.run(
         ["node", "--input-type=module", "-e", source],
@@ -1361,6 +1455,150 @@ def test_static_session_ui_controls_and_replay_panels_are_available():
         assert f'id="{replay_id}"' in html
 
 
+def test_static_professional_console_has_semantic_three_region_shell():
+    html = Path("src/agent_lab/presentation/static/index.html").read_text()
+    root = _parse_html(html)
+    layout = _html_node_by_id(root, "console-layout")
+    direct_regions = layout.children
+
+    assert layout.tag == "main"
+    assert "layout" in layout.classes
+    assert [(node.tag, node.attrs.get("aria-label")) for node in direct_regions] == [
+        ("aside", "Runtime controls"),
+        ("section", "Conversation workspace"),
+        ("aside", "Audit and usage"),
+    ]
+    assert "controls" in direct_regions[0].classes
+    assert "workspace" in direct_regions[1].classes
+    assert "debug-drawer" in direct_regions[2].classes
+    assert direct_regions[2].attrs["id"] == "debug-drawer"
+
+    assert "Agent Lab" in html
+    assert "Chat and event runtime console" in html
+    for status_id in [
+        "current-session-label",
+        "current-mode-label",
+        "connection-status",
+    ]:
+        assert "status-badge" in _html_node_by_id(root, status_id).classes
+
+    drawer_toggle = _html_node_by_id(root, "toggle-debug-drawer")
+    assert drawer_toggle.attrs["aria-expanded"] == "true"
+    assert drawer_toggle.attrs["aria-controls"] == "debug-drawer"
+
+    workspace = direct_regions[1]
+    assert [child.attrs.get("class") for child in workspace.children] == [
+        "workspace-body",
+        "stats",
+        "composer",
+    ]
+    assert _html_node_by_id(workspace, "messages").attrs["class"] == "messages"
+    assert _html_node_by_id(workspace, "chat-form").tag == "form"
+
+    drawer_ids = {
+        node.attrs["id"]
+        for node in [direct_regions[2], *direct_regions[2].descendants()]
+        if "id" in node.attrs
+    }
+    workspace_ids = {
+        node.attrs["id"]
+        for node in [workspace, *workspace.descendants()]
+        if "id" in node.attrs
+    }
+    for replay_id in [
+        "audit-count",
+        "audit-summary",
+        "open-audit-dialog",
+        "session-usage-summary",
+        "session-usage-turns",
+        "session-usage-calls",
+    ]:
+        assert replay_id in drawer_ids
+        assert replay_id not in workspace_ids
+
+
+def test_static_professional_console_control_sections_have_accessible_defaults():
+    html = Path("src/agent_lab/presentation/static/index.html").read_text()
+    root = _parse_html(html)
+    controls = next(
+        node
+        for node in _html_node_by_id(root, "console-layout").children
+        if "controls" in node.classes
+    )
+    sections = [child for child in controls.children if "control-section" in child.classes]
+    toggles = [
+        node
+        for node in root.descendants()
+        if "control-section-toggle" in node.classes
+    ]
+
+    assert len(sections) == 4
+    assert len(toggles) == 4
+
+    expected_defaults = {
+        "session-controls": ("Session", True),
+        "tool-controls": ("Tools", True),
+        "agent-config-controls": ("Agent Config", False),
+        "snapshot-controls": ("Snapshot", False),
+    }
+    for section, (target_id, (title, expanded)) in zip(
+        sections,
+        expected_defaults.items(),
+        strict=True,
+    ):
+        section_toggles = [
+            node
+            for node in section.children
+            if "control-section-toggle" in node.classes
+        ]
+        assert len(section_toggles) == 1
+        toggle = section_toggles[0]
+        body = _html_node_by_id(section, target_id)
+
+        assert toggle.tag == "button"
+        assert toggle.attrs["type"] == "button"
+        assert toggle.attrs["data-collapse-target"] == target_id
+        assert toggle.attrs["aria-controls"] == target_id
+        assert toggle.attrs["aria-expanded"] == str(expanded).lower()
+        assert title in toggle.text_content()
+        assert ("hidden" in body.attrs) is not expanded
+
+
+def test_static_professional_console_defines_visual_and_responsive_contracts():
+    html = Path("src/agent_lab/presentation/static/index.html").read_text()
+    css = Path("src/agent_lab/presentation/static/styles.css").read_text()
+
+    assert '/static/styles.css?v=20260714-professional-console' in html
+    assert '/static/app.js?v=20260714-professional-console' in html
+    for token in [
+        "--canvas:",
+        "--surface:",
+        "--surface-subtle:",
+        "--border:",
+        "--text:",
+        "--muted:",
+        "--accent:",
+        "--accent-strong:",
+        "--danger:",
+        "--radius:",
+        "--shadow:",
+    ]:
+        assert token in _css_rule(css, ":root")
+
+    assert "button-primary" in html
+    assert "button-secondary" in html
+    assert "button-danger" in html
+    assert ".button-primary" in css
+    assert ".button-secondary" in css
+    assert ".button-danger" in css
+    assert ".status-badge" in css
+    assert ":focus-visible" in css
+    assert ".tool-card:has(input:checked)" in css
+    assert "display: none" in _css_rule(css, "[hidden]")
+    assert "@media (max-width: 900px)" in css
+    assert "\nsection + section {" not in css
+
+
 def test_static_chat_agent_prompt_is_a_single_sortable_list_with_event_instructions():
     html = Path("src/agent_lab/presentation/static/index.html").read_text()
     js = Path("src/agent_lab/presentation/static/app.js").read_text()
@@ -1943,7 +2181,7 @@ def test_static_audit_replay_uses_event_stream_and_detail_panel():
     css = Path("src/agent_lab/presentation/static/styles.css").read_text()
 
     assert '<link rel="stylesheet" href="/static/styles.css?v=' in html
-    assert "tool-mode-session-restore" in html
+    assert "20260714-professional-console" in html
     assert 'id="audit-dialog"' in html
     assert 'id="open-audit-dialog"' in html
     assert 'id="close-audit-dialog"' in html
@@ -1985,13 +2223,27 @@ def test_static_audit_replay_uses_event_stream_and_detail_panel():
 def test_static_layout_keeps_controls_and_composer_fixed_with_scrollable_panes():
     css = Path("src/agent_lab/presentation/static/styles.css").read_text()
 
-    assert "height: calc(100vh - 56px)" in css
-    assert ".layout" in css and "overflow: hidden" in css
-    assert ".controls" in css and "overflow: auto" in css
-    assert ".workspace {\n  display: grid;\n  grid-template-rows: minmax(0, 1fr) auto auto;" in css
-    assert ".workspace-body" in css and "overflow: hidden" in css
-    assert ".messages" in css and "overflow: auto" in css
-    assert ".audit-stream" in css and "overflow: auto" in css
+    layout_rule = _css_rule(css, ".layout")
+    closed_rule = _css_rule(css, ".layout.debug-drawer-closed")
+    workspace_rule = _css_rule(css, ".workspace")
+
+    assert "height: calc(100vh - 64px)" in layout_rule
+    assert (
+        "grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) "
+        "minmax(300px, 380px)"
+    ) in layout_rule
+    assert "overflow: hidden" in layout_rule
+    assert "grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) 0" in closed_rule
+    assert "overflow: auto" in _css_rule(css, ".controls")
+    assert "grid-template-rows: minmax(0, 1fr) auto auto" in workspace_rule
+    assert "overflow: hidden" in _css_rule(css, ".workspace-body")
+    assert "overflow: auto" in _css_rule(css, ".messages")
+    assert "overflow: auto" in _css_rule(css, ".debug-drawer")
+
+    responsive_css = css[css.index("@media (max-width: 900px)") :]
+    assert "height: auto" in responsive_css
+    assert "overflow-x: hidden" in responsive_css
+    assert ".debug-drawer[hidden]" in responsive_css
 
 
 def test_static_app_shows_wait_state_and_immediate_user_echo():