@geminixiang/mikan 1.0.0-beta.72 → 1.0.0-beta.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1414,197 +1414,95 @@ async function serveConversationEventDelete(res, body, services, token) {
1414
1414
  }
1415
1415
  // ── Utilities ──────────────────────────────────────────────────────────────────
1416
1416
  const esc = escapeHtml;
1417
- // ── HTML ───────────────────────────────────────────────────────────────────────
1418
- const adminViewBody = `<nav class="tab-nav" role="tablist" aria-label="Admin sections">
1419
- <button class="tab-btn active" role="tab" aria-selected="true" aria-controls="panel-conversation" data-tab="conversation">Conversation</button>
1420
- <button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-global" data-tab="global">Global</button>
1421
- </nav>
1422
-
1423
- <div class="tab-panel active" id="panel-conversation">
1424
- <details class="card sect" id="sect-settings" data-section="settings" open>
1425
- <summary class="sect-head">
1426
- <div class="sect-title"><span class="sect-caret" aria-hidden="true">▸</span><div>
1427
- <p class="eyebrow">Settings</p>
1428
- <h2 class="card-title">模型 / Thinking / Auto-reply / Workspace mount</h2>
1429
- </div></div>
1430
- <button class="refresh-btn" onclick="event.stopPropagation(); loadSettings()">↻</button>
1431
- </summary>
1432
- <div class="sect-body">
1433
- <div id="settings-content"><div class="loading-msg">Loading…</div></div>
1434
- </div>
1435
- </details>
1436
-
1437
- <details class="card sect" id="sect-workspace" data-section="workspace">
1438
- <summary class="sect-head">
1439
- <div class="sect-title"><span class="sect-caret" aria-hidden="true">▸</span><div>
1440
- <p class="eyebrow">Workspace</p>
1441
- <h2 class="card-title">檔案瀏覽 (只讀)</h2>
1442
- </div></div>
1443
- <button class="refresh-btn" onclick="event.stopPropagation(); loadWorkspace()">↻</button>
1444
- </summary>
1445
- <div class="sect-body">
1446
- <div class="workspace-split">
1447
- <div id="workspace-tree" class="workspace-tree"><div class="loading-msg">Loading…</div></div>
1448
- <div id="workspace-preview" class="workspace-preview"><div class="placeholder-msg">Click a file to preview</div></div>
1449
- </div>
1450
- </div>
1451
- </details>
1452
-
1453
- <details class="card sect" id="sect-skills" data-section="skills">
1454
- <summary class="sect-head">
1455
- <div class="sect-title"><span class="sect-caret" aria-hidden="true">▸</span><div>
1456
- <p class="eyebrow">Skills</p>
1457
- <h2 class="card-title">可用的 skills</h2>
1458
- </div></div>
1459
- <div class="sect-head-actions">
1460
- <button class="primary-action-btn" onclick="event.stopPropagation(); openSkillDialog('conversation')">+ New skill</button>
1461
- <button class="refresh-btn" onclick="event.stopPropagation(); loadSkills()">↻</button>
1462
- </div>
1463
- </summary>
1464
- <div class="sect-body">
1465
- <div class="workspace-split">
1466
- <div id="skills-content" class="workspace-tree"><div class="loading-msg">Loading…</div></div>
1467
- <div id="skills-preview" class="workspace-preview"><div class="placeholder-msg">Click a skill to preview SKILL.md</div></div>
1468
- </div>
1469
- </div>
1470
- </details>
1471
-
1472
- <details class="card sect" id="sect-mcp" data-section="mcp">
1473
- <summary class="sect-head">
1474
- <div class="sect-title"><span class="sect-caret" aria-hidden="true">▸</span><div>
1475
- <p class="eyebrow">MCP Servers</p>
1476
- <h2 class="card-title">此對話的 MCP servers</h2>
1477
- </div></div>
1478
- <button class="refresh-btn" onclick="event.stopPropagation(); loadMcpServers()">↻</button>
1479
- </summary>
1480
- <div class="sect-body">
1481
- <div id="mcp-conv-msg" class="status-msg" style="display:none"></div>
1482
- <div id="mcp-conv-content"><div class="loading-msg">Loading…</div></div>
1483
- </div>
1484
- </details>
1485
-
1486
- <details class="card sect" id="sect-vault" data-section="vault">
1487
- <summary class="sect-head">
1488
- <div class="sect-title"><span class="sect-caret" aria-hidden="true">▸</span><div>
1489
- <p class="eyebrow">Vault</p>
1490
- <h2 class="card-title">該對話的憑證</h2>
1491
- </div></div>
1492
- <button class="primary-action-btn" onclick="event.stopPropagation(); openLogin()">Open in new tab ↗</button>
1493
- </summary>
1494
- <div class="sect-body">
1495
- <div id="vault-link-result" class="link-result" style="display:none"></div>
1496
- <p class="card-desc">Opens the one-time credential form for this conversation's vault in a new tab.</p>
1497
- </div>
1498
- </details>
1499
-
1500
- <details class="card sect" id="sect-events" data-section="events">
1501
- <summary class="sect-head">
1502
- <div class="sect-title"><span class="sect-caret" aria-hidden="true">▸</span><div>
1503
- <p class="eyebrow">Events</p>
1504
- <h2 class="card-title">關聯此對話的 events</h2>
1505
- </div></div>
1506
- <button class="refresh-btn" onclick="event.stopPropagation(); loadConversationEvents()">↻</button>
1507
- </summary>
1508
- <div class="sect-body">
1509
- <div id="events-content"><div class="loading-msg">Loading…</div></div>
1417
+ const ICONS = {
1418
+ settings: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
1419
+ workspace: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg>`,
1420
+ skills: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5V5a2 2 0 0 1 2-2h11a1 1 0 0 1 1 1v14"/><path d="M6 17h13v4H6.5A2.5 2.5 0 0 1 4 18.5v0A2.5 2.5 0 0 1 6.5 16H20"/></svg>`,
1421
+ mcp: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="1" fill="currentColor" stroke="none"/><circle cx="7" cy="16.5" r="1" fill="currentColor" stroke="none"/></svg>`,
1422
+ vault: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
1423
+ events: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></svg>`,
1424
+ session: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>`,
1425
+ overview: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8" rx="1.5"/><rect x="13" y="3" width="8" height="5" rx="1.5"/><rect x="13" y="11" width="8" height="10" rx="1.5"/><rect x="3" y="14" width="8" height="7" rx="1.5"/></svg>`,
1426
+ usage: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V3"/><path d="M3 21h18"/><path d="M7 17V11M12 17V7M17 17v-4"/></svg>`,
1427
+ };
1428
+ /** One entry in the settings rail: an icon, a label, and the pane id it activates. */
1429
+ function railLink(id, label, icon, active = false) {
1430
+ const svg = ICONS[icon];
1431
+ return `<button class="rail-link${active ? " active" : ""}" type="button" data-pane="${id}" aria-current="${active ? "page" : "false"}">${svg}<span>${esc(label)}</span></button>`;
1432
+ }
1433
+ /**
1434
+ * One settings pane: an eyebrow-free header (icon lives in the rail, not
1435
+ * repeated here) with a title, a one-line description, and header actions,
1436
+ * followed by body content. Every pane shares this shape so scanning the
1437
+ * page feels like one continuous settings surface instead of assembled
1438
+ * cards — the visual language `frontend-design` calls for here is a
1439
+ * quiet, editorial settings app, not a dashboard of boxes.
1440
+ */
1441
+ function settingsPane(id, title, description, actions, body) {
1442
+ return `<section class="pane" id="pane-${id}" data-pane="${id}">
1443
+ <header class="pane-head">
1444
+ <div><h2 class="pane-title">${esc(title)}</h2><p class="pane-desc">${esc(description)}</p></div>
1445
+ <div class="pane-actions">${actions}</div>
1446
+ </header>
1447
+ <div class="pane-body">${body}</div>
1448
+ </section>`;
1449
+ }
1450
+ const adminViewBody = `<div class="settings-shell">
1451
+ <aside class="settings-rail" aria-label="Admin sections">
1452
+ <div class="rail-scope" role="tablist" aria-label="Scope">
1453
+ <button class="rail-scope-btn active" role="tab" aria-selected="true" aria-controls="panel-conversation" data-tab="conversation">This conversation</button>
1454
+ <button class="rail-scope-btn" role="tab" aria-selected="false" aria-controls="panel-global" data-tab="global">Workspace</button>
1510
1455
  </div>
1511
- </details>
1512
-
1513
- <details class="card sect" id="sect-session" data-section="session">
1514
- <summary class="sect-head">
1515
- <div class="sect-title"><span class="sect-caret" aria-hidden="true">▸</span><div>
1516
- <p class="eyebrow">Session View</p>
1517
- <h2 class="card-title">對話歷史檢視</h2>
1518
- </div></div>
1519
- <button class="primary-action-btn" onclick="event.stopPropagation(); openSessionView()">Open in new tab ↗</button>
1520
- </summary>
1521
- <div class="sect-body">
1522
- <div id="session-link-result" class="link-result" style="display:none"></div>
1523
- <p class="card-desc">Opens the session timeline for this conversation in a new tab.</p>
1456
+ <nav class="rail-nav" id="rail-nav-conversation" data-scope="conversation">
1457
+ ${railLink("settings", "Settings", "settings", true)}
1458
+ ${railLink("workspace", "Workspace", "workspace")}
1459
+ ${railLink("skills", "Skills", "skills")}
1460
+ ${railLink("mcp", "MCP servers", "mcp")}
1461
+ ${railLink("vault", "Vault", "vault")}
1462
+ ${railLink("events", "Events", "events")}
1463
+ ${railLink("session", "Session view", "session")}
1464
+ </nav>
1465
+ <nav class="rail-nav" id="rail-nav-global" data-scope="global" hidden>
1466
+ ${railLink("g-overview", "All conversations", "overview", true)}
1467
+ ${railLink("g-usage", "Token usage", "usage")}
1468
+ ${railLink("g-settings", "Defaults", "settings")}
1469
+ ${railLink("g-mcp", "MCP servers", "mcp")}
1470
+ ${railLink("g-skills", "Skills", "skills")}
1471
+ ${railLink("g-events", "Events", "events")}
1472
+ </nav>
1473
+ </aside>
1474
+
1475
+ <div class="settings-panels">
1476
+ <div class="tab-panel active" id="panel-conversation">
1477
+ ${settingsPane("settings", "Settings", "Model, thinking level, auto-reply, and workspace visibility for this conversation.", '<button class="refresh-btn" onclick="loadSettings()">↻</button>', '<div id="settings-content"><div class="loading-msg">Loading…</div></div>')}
1478
+
1479
+ ${settingsPane("workspace", "Workspace", "Read-only browser for this conversation's files on disk.", '<button class="refresh-btn" onclick="loadWorkspace()">↻</button>', '<div class="workspace-split"><div id="workspace-tree" class="workspace-tree"><div class="loading-msg">Loading…</div></div><div id="workspace-preview" class="workspace-preview"><div class="placeholder-msg">Click a file to preview</div></div></div>')}
1480
+
1481
+ ${settingsPane("skills", "Skills", "Instructions the agent can load for specific tasks in this conversation.", '<button class="primary-action-btn" onclick="openSkillDialog(\'conversation\')">+ New skill</button><button class="refresh-btn" onclick="loadSkills()">↻</button>', '<div class="workspace-split"><div id="skills-content" class="workspace-tree"><div class="loading-msg">Loading…</div></div><div id="skills-preview" class="workspace-preview"><div class="placeholder-msg">Click a skill to preview SKILL.md</div></div></div>')}
1482
+
1483
+ ${settingsPane("mcp", "MCP servers", "External tools this conversation can call during a run.", '<button class="refresh-btn" onclick="loadMcpServers()">↻</button>', '<div id="mcp-conv-msg" class="status-msg" style="display:none"></div><div id="mcp-conv-content"><div class="loading-msg">Loading…</div></div>')}
1484
+
1485
+ ${settingsPane("vault", "Vault", "Credentials scoped to this conversation.", '<button class="primary-action-btn" onclick="openLogin()">Open in new tab ↗</button>', '<div id="vault-link-result" class="link-result" style="display:none"></div><p class="card-desc">Opens the one-time credential form for this conversation\'s vault in a new tab.</p>')}
1486
+
1487
+ ${settingsPane("events", "Events", "Scheduled and one-shot events tied to this conversation.", '<button class="refresh-btn" onclick="loadConversationEvents()">↻</button>', '<div id="events-content"><div class="loading-msg">Loading…</div></div>')}
1488
+
1489
+ ${settingsPane("session", "Session view", "The full message and tool-call timeline for this conversation.", '<button class="primary-action-btn" onclick="openSessionView()">Open in new tab ↗</button>', '<div id="session-link-result" class="link-result" style="display:none"></div><p class="card-desc">Opens the session timeline for this conversation in a new tab.</p>')}
1524
1490
  </div>
1525
- </details>
1526
- </div>
1527
1491
 
1528
- <div class="tab-panel" id="panel-global">
1529
- <section class="card sect">
1530
- <header class="sect-head">
1531
- <div>
1532
- <p class="eyebrow">All Conversations</p>
1533
- <h2 class="card-title">所有對話</h2>
1534
- </div>
1535
- <button class="refresh-btn" onclick="loadAllConversations()">↻</button>
1536
- </header>
1537
- <div id="all-conv-content"><div class="loading-msg">Loading…</div></div>
1538
- </section>
1539
-
1540
- <section class="card sect">
1541
- <header class="sect-head">
1542
- <div>
1543
- <p class="eyebrow">Token Usage</p>
1544
- </div>
1545
- <button class="refresh-btn" onclick="loadTokenUsage()">↻</button>
1546
- </header>
1547
- <h2 class="card-subtitle" style="margin-bottom:10px">Top 20 sessions</h3>
1548
- <div id="session-usage-content"><div class="loading-msg">Loading…</div></div>
1549
- <h2 class="card-subtitle" style="margin:24px 0 10px">Usage timeline</h3>
1550
- <div class="timeline-controls">
1551
- <label>Conversation
1552
- <select id="timeline-conv" onchange="loadUsageTimeline()"></select>
1553
- </label>
1554
- </div>
1555
- <div id="usage-timeline-content"><div class="loading-msg">Loading…</div></div>
1556
- </section>
1557
-
1558
- <section class="card sect">
1559
- <header class="sect-head">
1560
- <div>
1561
- <p class="eyebrow">Global Settings</p>
1562
- <h2 class="card-title">全域預設</h2>
1563
- </div>
1564
- <button class="refresh-btn" onclick="loadGlobalSettings()">↻</button>
1565
- </header>
1566
- <div id="global-settings-content"><div class="loading-msg">Loading…</div></div>
1567
- </section>
1568
-
1569
- <section class="card sect">
1570
- <header class="sect-head">
1571
- <div>
1572
- <p class="eyebrow">Global MCP Servers</p>
1573
- <h2 class="card-title">所有對話都可用的 MCP servers</h2>
1574
- </div>
1575
- <button class="refresh-btn" onclick="loadMcpServers()">↻</button>
1576
- </header>
1577
- <div id="mcp-global-msg" class="status-msg" style="display:none"></div>
1578
- <div id="mcp-global-content"><div class="loading-msg">Loading…</div></div>
1579
- </section>
1580
-
1581
- <section class="card sect">
1582
- <header class="sect-head">
1583
- <div>
1584
- <p class="eyebrow">Global Skills</p>
1585
- <h2 class="card-title">全域 skills</h2>
1586
- </div>
1587
- <div class="sect-head-actions">
1588
- <button class="primary-action-btn" onclick="openSkillDialog('global')">+ New global skill</button>
1589
- <button class="refresh-btn" onclick="loadGlobalSkills()">↻</button>
1590
- </div>
1591
- </header>
1592
- <div class="workspace-split">
1593
- <div id="global-skills-content" class="workspace-tree"><div class="loading-msg">Loading…</div></div>
1594
- <div id="global-skills-preview" class="workspace-preview"><div class="placeholder-msg">Click a skill to preview SKILL.md</div></div>
1492
+ <div class="tab-panel" id="panel-global">
1493
+ ${settingsPane("g-overview", "All conversations", "Every registered conversation across every platform.", '<button class="refresh-btn" onclick="loadAllConversations()">↻</button>', '<div id="all-conv-content"><div class="loading-msg">Loading…</div></div>')}
1494
+
1495
+ ${settingsPane("g-usage", "Token usage", "Spend and volume across every conversation.", '<button class="refresh-btn" onclick="loadTokenUsage()">↻</button>', '<h3 class="card-subtitle">Top 20 sessions</h3><div id="session-usage-content"><div class="loading-msg">Loading…</div></div><h3 class="card-subtitle" style="margin-top:24px">Usage timeline</h3><div class="timeline-controls"><label>Conversation<select id="timeline-conv" onchange="loadUsageTimeline()"></select></label></div><div id="usage-timeline-content"><div class="loading-msg">Loading…</div></div>')}
1496
+
1497
+ ${settingsPane("g-settings", "Workspace defaults", "The fallback model, sandbox limits, and Slack behavior every conversation inherits.", '<button class="refresh-btn" onclick="loadGlobalSettings()">↻</button>', '<div id="global-settings-content"><div class="loading-msg">Loading…</div></div>')}
1498
+
1499
+ ${settingsPane("g-mcp", "MCP servers", "External tools available to every conversation in this workspace.", '<button class="refresh-btn" onclick="loadMcpServers()">↻</button>', '<div id="mcp-global-msg" class="status-msg" style="display:none"></div><div id="mcp-global-content"><div class="loading-msg">Loading…</div></div>')}
1500
+
1501
+ ${settingsPane("g-skills", "Skills", "Shared instructions available to every conversation in this workspace.", '<button class="primary-action-btn" onclick="openSkillDialog(\'global\')">+ New global skill</button><button class="refresh-btn" onclick="loadGlobalSkills()">↻</button>', '<div class="workspace-split"><div id="global-skills-content" class="workspace-tree"><div class="loading-msg">Loading…</div></div><div id="global-skills-preview" class="workspace-preview"><div class="placeholder-msg">Click a skill to preview SKILL.md</div></div></div>')}
1502
+
1503
+ ${settingsPane("g-events", "Global events", "Every scheduled event across the whole workspace.", '<button class="refresh-btn" onclick="loadEvents()">↻</button>', '<div id="global-events-content"><div class="loading-msg">Loading…</div></div>')}
1595
1504
  </div>
1596
- </section>
1597
-
1598
- <section class="card sect">
1599
- <header class="sect-head">
1600
- <div>
1601
- <p class="eyebrow">Global Events</p>
1602
- <h2 class="card-title">全域 events.json</h2>
1603
- </div>
1604
- <button class="refresh-btn" onclick="loadEvents()">↻</button>
1605
- </header>
1606
- <div id="global-events-content"><div class="loading-msg">Loading…</div></div>
1607
- </section>
1505
+ </div>
1608
1506
  </div>
1609
1507
 
1610
1508
  <dialog id="mcp-install-dialog" class="mcp-dialog" aria-labelledby="mcp-dialog-title">
@@ -1791,98 +1689,117 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
1791
1689
  return sections.join('');
1792
1690
  }
1793
1691
 
1794
- // ── Tab switching ────────────────────────────────────────────────────────────
1692
+ // ── Scope tabs (rail top switch) ────────────────────────────────────────────
1795
1693
 
1796
- const tabBtns = document.querySelectorAll('.tab-btn');
1694
+ const scopeBtns = document.querySelectorAll('.rail-scope-btn');
1797
1695
  const tabPanels = document.querySelectorAll('.tab-panel');
1696
+ const railNavConversation = document.getElementById('rail-nav-conversation');
1697
+ const railNavGlobal = document.getElementById('rail-nav-global');
1798
1698
 
1799
1699
  function switchTab(tabId) {
1800
- tabBtns.forEach((btn) => {
1700
+ scopeBtns.forEach((btn) => {
1801
1701
  const active = btn.dataset.tab === tabId;
1802
1702
  btn.classList.toggle('active', active);
1803
1703
  btn.setAttribute('aria-selected', active ? 'true' : 'false');
1804
1704
  });
1805
1705
  tabPanels.forEach((panel) => panel.classList.toggle('active', panel.id === 'panel-' + tabId));
1706
+ railNavConversation.hidden = tabId !== 'conversation';
1707
+ railNavGlobal.hidden = tabId !== 'global';
1806
1708
  if (tabId === 'global') initGlobal();
1709
+ const firstPane = (tabId === 'global' ? railNavGlobal : railNavConversation).querySelector('.rail-link');
1710
+ if (firstPane) selectPane(tabId, firstPane.dataset.pane);
1807
1711
  }
1808
- tabBtns.forEach((btn) => btn.addEventListener('click', () => switchTab(btn.dataset.tab)));
1712
+ scopeBtns.forEach((btn) => btn.addEventListener('click', () => switchTab(btn.dataset.tab)));
1809
1713
 
1810
- // ── Conversation switcher ───────────────────────────────────────────────────
1714
+ // ── Rail navigation (single active pane per scope) ──────────────────────────
1811
1715
 
1812
- async function initConvSwitcher() {
1813
- const sel = document.getElementById('conv-switcher');
1814
- try {
1815
- const data = await apiGet('/admin/api/conversations');
1816
- sel.innerHTML = data.conversations.map((c) => {
1817
- const key = c.platform + ':' + c.conversationId;
1818
- const label = (c.label || c.conversationId) + (c.running ? ' (running)' : '');
1819
- const selected = key === defaultConversationKey ? ' selected' : '';
1820
- return '<option value="' + escAttr(key) + '"' + selected + '>' + escHtml(label) + '</option>';
1821
- }).join('');
1822
- sel.addEventListener('change', () => setActiveConversation(sel.value));
1823
- } catch (err) {
1824
- // ignore; conversation selector stays empty
1825
- }
1826
- }
1827
-
1828
- // Sections load lazily: only when their <details> is (or becomes) open,
1829
- // so switching conversations or opening the admin page doesn't fire every
1830
- // API at once. sectionLoaded is cleared on conversation switch so an
1831
- // already-open section refetches for the new scope.
1832
- //
1833
1716
  // Looked up by name (not collected into an object up front) because the
1834
1717
  // loaders below are const declarations further down this same script;
1835
1718
  // referencing them before their own line runs would hit the temporal
1836
- // dead zone. By the time this function is actually called (after a
1837
- // <details> toggle, always after the whole script has executed), they're
1838
- // all initialized.
1839
- function sectionLoader(key) {
1719
+ // dead zone. By the time this function is actually called (after a rail
1720
+ // click, always after the whole script has executed), they're all
1721
+ // initialized.
1722
+ function paneLoader(key) {
1840
1723
  switch (key) {
1841
1724
  case 'settings': return loadSettings;
1842
1725
  case 'workspace': return loadWorkspace;
1843
1726
  case 'skills': return loadSkills;
1844
1727
  case 'mcp': return loadMcpServers;
1845
1728
  case 'events': return loadConversationEvents;
1729
+ case 'g-overview': return loadAllConversations;
1730
+ case 'g-usage': return loadTokenUsage;
1731
+ case 'g-settings': return loadGlobalSettings;
1732
+ case 'g-mcp': return loadMcpServers;
1733
+ case 'g-skills': return loadGlobalSkills;
1734
+ case 'g-events': return loadEvents;
1846
1735
  default: return undefined;
1847
1736
  }
1848
1737
  }
1849
- const sectionLoaded = new Set();
1738
+ const paneLoaded = new Set();
1850
1739
 
1851
- function conversationSectionEls() {
1852
- return document.querySelectorAll('#panel-conversation > details[data-section]');
1853
- }
1854
-
1855
- function ensureSectionLoaded(key) {
1856
- if (sectionLoaded.has(key)) return;
1857
- const loader = sectionLoader(key);
1740
+ function ensurePaneLoaded(key) {
1741
+ if (paneLoaded.has(key)) return;
1742
+ const loader = paneLoader(key);
1858
1743
  if (!loader) return;
1859
- sectionLoaded.add(key);
1744
+ paneLoaded.add(key);
1860
1745
  loader();
1861
1746
  }
1862
1747
 
1863
- function initSections() {
1864
- conversationSectionEls().forEach((el) => {
1865
- const key = el.dataset.section;
1866
- const stored = localStorage.getItem('admin-sect-' + key);
1867
- if (stored !== null) el.open = stored === '1';
1868
- if (el.open) ensureSectionLoaded(key);
1869
- el.addEventListener('toggle', () => {
1870
- localStorage.setItem('admin-sect-' + key, el.open ? '1' : '0');
1871
- if (el.open) ensureSectionLoaded(key);
1748
+ function selectPane(scope, key) {
1749
+ const nav = scope === 'global' ? railNavGlobal : railNavConversation;
1750
+ const panel = document.getElementById(scope === 'global' ? 'panel-global' : 'panel-conversation');
1751
+ nav.querySelectorAll('.rail-link').forEach((btn) => {
1752
+ const active = btn.dataset.pane === key;
1753
+ btn.classList.toggle('active', active);
1754
+ btn.setAttribute('aria-current', active ? 'page' : 'false');
1755
+ });
1756
+ panel.querySelectorAll('.pane').forEach((pane) => {
1757
+ pane.classList.toggle('active', pane.dataset.pane === key);
1758
+ });
1759
+ ensurePaneLoaded(key);
1760
+ }
1761
+
1762
+ function initRailNav() {
1763
+ document.querySelectorAll('.rail-link').forEach((btn) => {
1764
+ btn.addEventListener('click', () => {
1765
+ const nav = btn.closest('.rail-nav');
1766
+ const scope = nav.dataset.scope;
1767
+ selectPane(scope, btn.dataset.pane);
1872
1768
  });
1873
1769
  });
1770
+ // The first rail link in each scope starts active in the markup, so its
1771
+ // pane needs the matching 'active' class and its data loaded up front.
1772
+ selectPane('conversation', 'settings');
1773
+ }
1774
+
1775
+ // ── Conversation switcher ───────────────────────────────────────────────────
1776
+
1777
+ async function initConvSwitcher() {
1778
+ const sel = document.getElementById('conv-switcher');
1779
+ try {
1780
+ const data = await apiGet('/admin/api/conversations');
1781
+ sel.innerHTML = data.conversations.map((c) => {
1782
+ const key = c.platform + ':' + c.conversationId;
1783
+ const label = (c.label || c.conversationId) + (c.running ? ' (running)' : '');
1784
+ const selected = key === defaultConversationKey ? ' selected' : '';
1785
+ return '<option value="' + escAttr(key) + '"' + selected + '>' + escHtml(label) + '</option>';
1786
+ }).join('');
1787
+ sel.addEventListener('change', () => setActiveConversation(sel.value));
1788
+ } catch (err) {
1789
+ // ignore; conversation selector stays empty
1790
+ }
1874
1791
  }
1875
1792
 
1876
1793
  function setActiveConversation(key) {
1877
1794
  activeConversationKey = key;
1878
1795
  const sel = document.getElementById('conv-switcher');
1879
1796
  if (sel && sel.value !== key) sel.value = key;
1880
- // Data is scoped to the previous conversation; drop it so any open
1881
- // section refetches, and closed sections load fresh on next expand.
1882
- sectionLoaded.clear();
1883
- conversationSectionEls().forEach((el) => {
1884
- if (el.open) ensureSectionLoaded(el.dataset.section);
1885
- });
1797
+ // Data is scoped to the previous conversation; drop the conversation
1798
+ // panes (not the global ones — those aren't conversation-scoped) so
1799
+ // the active pane refetches for the new scope.
1800
+ const activeLink = railNavConversation.querySelector('.rail-link.active');
1801
+ ['settings', 'workspace', 'skills', 'mcp', 'events'].forEach((key) => paneLoaded.delete(key));
1802
+ if (activeLink) ensurePaneLoaded(activeLink.dataset.pane);
1886
1803
  openLogin(true);
1887
1804
  openSessionView(true);
1888
1805
  }
@@ -2661,17 +2578,10 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
2661
2578
 
2662
2579
  // ── Global section ──────────────────────────────────────────────────────────
2663
2580
 
2664
- let globalLoaded = false;
2665
- function initGlobal() {
2666
- if (globalLoaded) return;
2667
- globalLoaded = true;
2668
- loadAllConversations();
2669
- loadTokenUsage();
2670
- loadGlobalSettings();
2671
- loadGlobalSkills();
2672
- loadEvents();
2673
- loadMcpServers();
2674
- }
2581
+ // Switching to the Workspace scope only needs to (re)select its first
2582
+ // pane; every pane still loads lazily through paneLoader/ensurePaneLoaded
2583
+ // the first time it becomes active, same as the conversation scope.
2584
+ function initGlobal() {}
2675
2585
 
2676
2586
  async function loadAllConversations() {
2677
2587
  const container = document.getElementById('all-conv-content');
@@ -2923,10 +2833,10 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
2923
2833
 
2924
2834
  // ── Init ─────────────────────────────────────────────────────────────────────
2925
2835
 
2926
- // Sections load on demand (see initSections); loadSettingsPanel already
2836
+ // Panes load on demand (see ensurePaneLoaded); loadSettingsPanel already
2927
2837
  // calls loadModels() itself the first time it's needed.
2928
2838
  initConvSwitcher();
2929
- initSections();
2839
+ initRailNav();
2930
2840
  `;
2931
2841
  function renderAdminPage(token) {
2932
2842
  const userLabel = token.platformUserName ?? token.platformUserId;
@@ -2959,28 +2869,93 @@ function renderAdminErrorPage(message) {
2959
2869
  }
2960
2870
  // ── Styles ─────────────────────────────────────────────────────────────────────
2961
2871
  const adminViewStyles = `
2962
- .tab-nav {
2963
- display: flex; gap: 6px; padding: 6px;
2964
- border: 1px solid var(--border); border-radius: 16px;
2965
- background: rgba(255,255,255,0.72); backdrop-filter: blur(8px);
2966
- overflow-x: auto; scrollbar-width: none;
2967
- }
2968
- .tab-nav::-webkit-scrollbar { display: none; }
2969
- .tab-btn {
2970
- flex: 1; min-width: 80px; padding: 10px 16px;
2971
- border: none; border-radius: 10px; background: transparent;
2872
+ /* The shared shell caps at 960px for the narrower session/login portals;
2873
+ the sidebar + single-pane layout here reads cramped at that width, so
2874
+ admin alone widens it. */
2875
+ .shell { max-width: 1180px; }
2876
+
2877
+ /* ── Settings shell: persistent rail + single active pane ───────────── */
2878
+
2879
+ .settings-shell {
2880
+ display: grid;
2881
+ grid-template-columns: 216px 1fr;
2882
+ align-items: start;
2883
+ gap: 22px;
2884
+ }
2885
+
2886
+ .settings-rail {
2887
+ position: sticky;
2888
+ top: 28px;
2889
+ display: flex;
2890
+ flex-direction: column;
2891
+ gap: 16px;
2892
+ }
2893
+
2894
+ .rail-scope {
2895
+ display: flex;
2896
+ padding: 3px;
2897
+ border: 1px solid var(--border);
2898
+ border-radius: 11px;
2899
+ background: rgba(0,0,0,0.03);
2900
+ gap: 2px;
2901
+ }
2902
+ .rail-scope-btn {
2903
+ flex: 1;
2904
+ padding: 7px 8px;
2905
+ border: none;
2906
+ border-radius: 8px;
2907
+ background: transparent;
2972
2908
  color: var(--muted);
2973
- font: 500 0.88rem/1.2 'DM Sans', sans-serif;
2974
- cursor: pointer; white-space: nowrap;
2909
+ font: 600 0.72rem/1.2 'DM Sans', sans-serif;
2910
+ letter-spacing: 0.01em;
2911
+ cursor: pointer;
2975
2912
  transition: background 140ms, color 140ms;
2976
2913
  }
2977
- .tab-btn:hover { background: rgba(0,0,0,0.04); color: var(--text); }
2978
- .tab-btn.active { background: var(--text); color: #fafafa; font-weight: 600; }
2979
- .tab-btn:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
2980
-
2981
- .tab-panel { display: none; flex-direction: column; gap: 14px; }
2914
+ .rail-scope-btn:hover { color: var(--text); }
2915
+ .rail-scope-btn.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
2916
+ .rail-scope-btn:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
2917
+
2918
+ .rail-nav { display: flex; flex-direction: column; gap: 1px; }
2919
+ .rail-nav[hidden] { display: none; }
2920
+ .rail-link {
2921
+ display: flex; align-items: center; gap: 10px;
2922
+ padding: 8px 10px; border: none; border-radius: 9px;
2923
+ background: transparent; color: var(--muted); text-align: left;
2924
+ font: 500 0.86rem/1.2 'DM Sans', sans-serif; cursor: pointer;
2925
+ transition: background 120ms, color 120ms;
2926
+ }
2927
+ .rail-link svg { flex-shrink: 0; opacity: 0.75; }
2928
+ .rail-link:hover { background: rgba(0,0,0,0.045); color: var(--text); }
2929
+ .rail-link:hover svg { opacity: 1; }
2930
+ .rail-link.active {
2931
+ background: var(--text); color: #fafafa; font-weight: 600;
2932
+ }
2933
+ .rail-link.active svg { opacity: 1; }
2934
+ .rail-link:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }
2935
+
2936
+ .settings-panels { min-width: 0; }
2937
+ .tab-panel { display: none; flex-direction: column; gap: 0; }
2982
2938
  .tab-panel.active { display: flex; }
2983
2939
 
2940
+ /* ── Panes: one continuous surface, not stacked cards ────────────────── */
2941
+
2942
+ .pane { display: none; padding: 30px 0; border-bottom: 1px solid var(--border); }
2943
+ .pane:last-child { border-bottom: none; }
2944
+ .pane.active { display: block; animation: pane-in 180ms ease; }
2945
+ @keyframes pane-in { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
2946
+
2947
+ .pane-head {
2948
+ display: flex; align-items: flex-start; justify-content: space-between;
2949
+ gap: 16px; margin-bottom: 22px; flex-wrap: wrap;
2950
+ }
2951
+ .pane-title {
2952
+ font-family: 'Lora', Georgia, serif; font-size: 1.32rem; font-weight: 600;
2953
+ letter-spacing: -0.01em; line-height: 1.25; margin-bottom: 5px;
2954
+ }
2955
+ .pane-desc { color: var(--muted); font-size: 0.88rem; line-height: 1.55; max-width: 52ch; }
2956
+ .pane-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
2957
+ .pane-body { display: flex; flex-direction: column; gap: 14px; }
2958
+
2984
2959
  .card-desc { color: var(--muted); font-size: 0.9rem; line-height: 1.55; margin-bottom: 12px; }
2985
2960
 
2986
2961
  .link-result {
@@ -3027,39 +3002,14 @@ const adminViewStyles = `
3027
3002
  .inline-result.ok { background: var(--ok-bg); color: var(--ok-text); border: 1px solid var(--ok-border); }
3028
3003
  .inline-result.err { background: var(--err-bg); color: var(--err-text); border: 1px solid var(--err-border); }
3029
3004
 
3030
- /* ── Sections (Conversation page stack) ─────────────────────────────── */
3031
-
3032
- /* Each section is a <details>; the card's own padding sits on the
3033
- <summary>/<div class="sect-body"> instead of the <details> element so a
3034
- collapsed section is just its header row. */
3035
- details.sect { padding: 0; overflow: hidden; }
3036
- details.sect > summary.sect-head {
3037
- padding: 24px 28px; margin-bottom: 0; cursor: pointer; list-style: none;
3038
- }
3039
- details.sect > summary.sect-head::-webkit-details-marker { display: none; }
3040
- details.sect > .sect-body { padding: 0 28px 24px; }
3041
-
3042
- .sect-head {
3043
- display: flex; align-items: flex-start; justify-content: space-between;
3044
- gap: 12px; flex-wrap: wrap;
3045
- }
3046
- .sect-head .card-title { margin-bottom: 0; }
3047
- .sect-title { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
3048
- .sect-caret {
3049
- flex-shrink: 0; margin-top: 3px; color: var(--subtle);
3050
- transition: transform 140ms; display: inline-block;
3051
- }
3052
- details.sect[open] > summary.sect-head .sect-caret { transform: rotate(90deg); }
3053
- .sect-disabled { opacity: 0.7; }
3054
-
3055
3005
  .refresh-btn {
3056
3006
  flex-shrink: 0; padding: 6px 12px;
3057
3007
  border: 1px solid var(--border); border-radius: 10px;
3058
3008
  background: rgba(0,0,0,0.025); color: var(--muted);
3059
- font: 500 0.84rem/1.2 'DM Sans', sans-serif; cursor: pointer;
3009
+ font: 500 0.8rem/1.2 'DM Sans', sans-serif; cursor: pointer;
3010
+ transition: background 120ms, color 120ms;
3060
3011
  }
3061
3012
  .refresh-btn:hover { background: rgba(0,0,0,0.06); color: var(--text); }
3062
- .sect-head-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
3063
3013
 
3064
3014
  /* ── Workspace ──────────────────────────────────────────────────────── */
3065
3015
 
@@ -3426,8 +3376,15 @@ const adminViewStyles = `
3426
3376
  }
3427
3377
  .status-pill.running { background: var(--ok-bg); color: var(--ok-text); border: 1px solid var(--ok-border); }
3428
3378
 
3379
+ @media (max-width: 860px) {
3380
+ .settings-shell { grid-template-columns: 1fr; }
3381
+ .settings-rail { position: static; }
3382
+ .rail-nav { flex-direction: row; flex-wrap: wrap; }
3383
+ .rail-link span { display: none; }
3384
+ .rail-link { padding: 9px; }
3385
+ }
3386
+
3429
3387
  @media (max-width: 640px) {
3430
- .tab-btn { padding: 9px 12px; font-size: 0.82rem; min-width: 60px; }
3431
3388
  .config-grid { grid-template-columns: 1fr; }
3432
3389
  .config-row { grid-template-columns: 1fr; gap: 4px; }
3433
3390
  .workspace-split { grid-template-columns: 1fr; }