@geminixiang/mikan 1.0.0-beta.72 → 1.0.0-beta.74
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.
- package/CHANGELOG.md +16 -0
- package/dist/adapters/web/admin/portal.js +274 -301
- package/dist/adapters/web/admin/portal.js.map +1 -1
- package/dist/adapters/web/portal-shell.js +5 -2
- package/dist/adapters/web/portal-shell.js.map +1 -1
- package/dist/harness/execution-env.js +34 -1
- package/dist/harness/execution-env.js.map +1 -1
- package/dist/harness/prompt.js +1 -1
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/tools/index.d.ts.map +1 -1
- package/dist/harness/tools/index.js +6 -1
- package/dist/harness/tools/index.js.map +1 -1
- package/dist/harness/tools/secret-redaction.d.ts +34 -0
- package/dist/harness/tools/secret-redaction.d.ts.map +1 -0
- package/dist/harness/tools/secret-redaction.js +77 -0
- package/dist/harness/tools/secret-redaction.js.map +1 -0
- package/dist/runtime/conversation-runtime.d.ts.map +1 -1
- package/dist/runtime/conversation-runtime.js +19 -10
- package/dist/runtime/conversation-runtime.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -3
|
@@ -1414,197 +1414,97 @@ async function serveConversationEventDelete(res, body, services, token) {
|
|
|
1414
1414
|
}
|
|
1415
1415
|
// ── Utilities ──────────────────────────────────────────────────────────────────
|
|
1416
1416
|
const esc = escapeHtml;
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
<
|
|
1454
|
-
<
|
|
1455
|
-
<
|
|
1456
|
-
|
|
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
|
+
// The label span is hidden below 860px, so mirror it into aria-label/title to
|
|
1432
|
+
// keep an accessible name and a hover hint once the text disappears.
|
|
1433
|
+
return `<button class="rail-link${active ? " active" : ""}" type="button" data-pane="${id}" aria-current="${active ? "page" : "false"}" aria-label="${esc(label)}" title="${esc(label)}">${svg}<span>${esc(label)}</span></button>`;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* One settings pane: an eyebrow-free header (icon lives in the rail, not
|
|
1437
|
+
* repeated here) with a title, a one-line description, and header actions,
|
|
1438
|
+
* followed by body content. Every pane shares this shape so scanning the
|
|
1439
|
+
* page feels like one continuous settings surface instead of assembled
|
|
1440
|
+
* cards — the visual language `frontend-design` calls for here is a
|
|
1441
|
+
* quiet, editorial settings app, not a dashboard of boxes.
|
|
1442
|
+
*/
|
|
1443
|
+
function settingsPane(id, title, description, actions, body) {
|
|
1444
|
+
return `<section class="pane" id="pane-${id}" data-pane="${id}">
|
|
1445
|
+
<header class="pane-head">
|
|
1446
|
+
<div><h2 class="pane-title">${esc(title)}</h2><p class="pane-desc">${esc(description)}</p></div>
|
|
1447
|
+
<div class="pane-actions">${actions}</div>
|
|
1448
|
+
</header>
|
|
1449
|
+
<div class="pane-body">${body}</div>
|
|
1450
|
+
</section>`;
|
|
1451
|
+
}
|
|
1452
|
+
const adminViewBody = `<div class="settings-shell">
|
|
1453
|
+
<aside class="settings-rail" aria-label="Admin sections">
|
|
1454
|
+
<div class="rail-scope" role="tablist" aria-label="Scope">
|
|
1455
|
+
<button class="rail-scope-btn active" role="tab" aria-selected="true" aria-controls="panel-conversation" data-tab="conversation">This conversation</button>
|
|
1456
|
+
<button class="rail-scope-btn" role="tab" aria-selected="false" aria-controls="panel-global" data-tab="global">Workspace</button>
|
|
1510
1457
|
</div>
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1458
|
+
<nav class="rail-nav" id="rail-nav-conversation" data-scope="conversation">
|
|
1459
|
+
${railLink("settings", "Settings", "settings", true)}
|
|
1460
|
+
${railLink("workspace", "Workspace", "workspace")}
|
|
1461
|
+
${railLink("skills", "Skills", "skills")}
|
|
1462
|
+
${railLink("mcp", "MCP servers", "mcp")}
|
|
1463
|
+
${railLink("vault", "Vault", "vault")}
|
|
1464
|
+
${railLink("events", "Events", "events")}
|
|
1465
|
+
${railLink("session", "Session view", "session")}
|
|
1466
|
+
</nav>
|
|
1467
|
+
<nav class="rail-nav" id="rail-nav-global" data-scope="global" hidden>
|
|
1468
|
+
${railLink("g-overview", "All conversations", "overview", true)}
|
|
1469
|
+
${railLink("g-usage", "Token usage", "usage")}
|
|
1470
|
+
${railLink("g-settings", "Defaults", "settings")}
|
|
1471
|
+
${railLink("g-mcp", "MCP servers", "mcp")}
|
|
1472
|
+
${railLink("g-skills", "Skills", "skills")}
|
|
1473
|
+
${railLink("g-events", "Events", "events")}
|
|
1474
|
+
</nav>
|
|
1475
|
+
</aside>
|
|
1476
|
+
|
|
1477
|
+
<div class="settings-panels">
|
|
1478
|
+
<div class="tab-panel active" id="panel-conversation">
|
|
1479
|
+
${settingsPane("settings", "Settings", "Model, thinking level, auto-reply, and workspace visibility for this conversation.", '<button class="refresh-btn" type="button" aria-label="Refresh" title="Refresh" onclick="loadSettings()">↻</button>', '<div id="settings-content"><div class="loading-msg">Loading…</div></div>')}
|
|
1480
|
+
|
|
1481
|
+
${settingsPane("workspace", "Workspace", "Read-only browser for this conversation's files on disk.", '<button class="refresh-btn" type="button" aria-label="Refresh" title="Refresh" 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>')}
|
|
1482
|
+
|
|
1483
|
+
${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" type="button" aria-label="Refresh" title="Refresh" 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>')}
|
|
1484
|
+
|
|
1485
|
+
${settingsPane("mcp", "MCP servers", "External tools this conversation can call during a run.", '<button class="refresh-btn" type="button" aria-label="Refresh" title="Refresh" 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>')}
|
|
1486
|
+
|
|
1487
|
+
${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>')}
|
|
1488
|
+
|
|
1489
|
+
${settingsPane("events", "Events", "Scheduled and one-shot events tied to this conversation.", '<button class="refresh-btn" type="button" aria-label="Refresh" title="Refresh" onclick="loadConversationEvents()">↻</button>', '<div id="events-content"><div class="loading-msg">Loading…</div></div>')}
|
|
1490
|
+
|
|
1491
|
+
${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
1492
|
</div>
|
|
1525
|
-
</details>
|
|
1526
|
-
</div>
|
|
1527
1493
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
<div>
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
<button class="refresh-btn" onclick="
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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>
|
|
1494
|
+
<div class="tab-panel" id="panel-global">
|
|
1495
|
+
${settingsPane("g-overview", "All conversations", "Every registered conversation across every platform.", '<button class="refresh-btn" type="button" aria-label="Refresh" title="Refresh" onclick="loadAllConversations()">↻</button>', '<div id="all-conv-content"><div class="loading-msg">Loading…</div></div>')}
|
|
1496
|
+
|
|
1497
|
+
${settingsPane("g-usage", "Token usage", "Spend and volume across every conversation.", '<button class="refresh-btn" type="button" aria-label="Refresh" title="Refresh" 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>')}
|
|
1498
|
+
|
|
1499
|
+
${settingsPane("g-settings", "Workspace defaults", "The fallback model, sandbox limits, and Slack behavior every conversation inherits.", '<button class="refresh-btn" type="button" aria-label="Refresh" title="Refresh" onclick="loadGlobalSettings()">↻</button>', '<div id="global-settings-content"><div class="loading-msg">Loading…</div></div>')}
|
|
1500
|
+
|
|
1501
|
+
${settingsPane("g-mcp", "MCP servers", "External tools available to every conversation in this workspace.", '<button class="refresh-btn" type="button" aria-label="Refresh" title="Refresh" 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>')}
|
|
1502
|
+
|
|
1503
|
+
${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" type="button" aria-label="Refresh" title="Refresh" 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>')}
|
|
1504
|
+
|
|
1505
|
+
${settingsPane("g-events", "Global events", "Every scheduled event across the whole workspace.", '<button class="refresh-btn" type="button" aria-label="Refresh" title="Refresh" onclick="loadEvents()">↻</button>', '<div id="global-events-content"><div class="loading-msg">Loading…</div></div>')}
|
|
1595
1506
|
</div>
|
|
1596
|
-
</
|
|
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>
|
|
1507
|
+
</div>
|
|
1608
1508
|
</div>
|
|
1609
1509
|
|
|
1610
1510
|
<dialog id="mcp-install-dialog" class="mcp-dialog" aria-labelledby="mcp-dialog-title">
|
|
@@ -1791,98 +1691,117 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
|
|
|
1791
1691
|
return sections.join('');
|
|
1792
1692
|
}
|
|
1793
1693
|
|
|
1794
|
-
// ──
|
|
1694
|
+
// ── Scope tabs (rail top switch) ────────────────────────────────────────────
|
|
1795
1695
|
|
|
1796
|
-
const
|
|
1696
|
+
const scopeBtns = document.querySelectorAll('.rail-scope-btn');
|
|
1797
1697
|
const tabPanels = document.querySelectorAll('.tab-panel');
|
|
1698
|
+
const railNavConversation = document.getElementById('rail-nav-conversation');
|
|
1699
|
+
const railNavGlobal = document.getElementById('rail-nav-global');
|
|
1798
1700
|
|
|
1799
1701
|
function switchTab(tabId) {
|
|
1800
|
-
|
|
1702
|
+
scopeBtns.forEach((btn) => {
|
|
1801
1703
|
const active = btn.dataset.tab === tabId;
|
|
1802
1704
|
btn.classList.toggle('active', active);
|
|
1803
1705
|
btn.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
1804
1706
|
});
|
|
1805
1707
|
tabPanels.forEach((panel) => panel.classList.toggle('active', panel.id === 'panel-' + tabId));
|
|
1708
|
+
railNavConversation.hidden = tabId !== 'conversation';
|
|
1709
|
+
railNavGlobal.hidden = tabId !== 'global';
|
|
1806
1710
|
if (tabId === 'global') initGlobal();
|
|
1711
|
+
const firstPane = (tabId === 'global' ? railNavGlobal : railNavConversation).querySelector('.rail-link');
|
|
1712
|
+
if (firstPane) selectPane(tabId, firstPane.dataset.pane);
|
|
1807
1713
|
}
|
|
1808
|
-
|
|
1714
|
+
scopeBtns.forEach((btn) => btn.addEventListener('click', () => switchTab(btn.dataset.tab)));
|
|
1809
1715
|
|
|
1810
|
-
// ──
|
|
1716
|
+
// ── Rail navigation (single active pane per scope) ──────────────────────────
|
|
1811
1717
|
|
|
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
1718
|
// Looked up by name (not collected into an object up front) because the
|
|
1834
1719
|
// loaders below are const declarations further down this same script;
|
|
1835
1720
|
// 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
|
-
//
|
|
1838
|
-
//
|
|
1839
|
-
function
|
|
1721
|
+
// dead zone. By the time this function is actually called (after a rail
|
|
1722
|
+
// click, always after the whole script has executed), they're all
|
|
1723
|
+
// initialized.
|
|
1724
|
+
function paneLoader(key) {
|
|
1840
1725
|
switch (key) {
|
|
1841
1726
|
case 'settings': return loadSettings;
|
|
1842
1727
|
case 'workspace': return loadWorkspace;
|
|
1843
1728
|
case 'skills': return loadSkills;
|
|
1844
1729
|
case 'mcp': return loadMcpServers;
|
|
1845
1730
|
case 'events': return loadConversationEvents;
|
|
1731
|
+
case 'g-overview': return loadAllConversations;
|
|
1732
|
+
case 'g-usage': return loadTokenUsage;
|
|
1733
|
+
case 'g-settings': return loadGlobalSettings;
|
|
1734
|
+
case 'g-mcp': return loadMcpServers;
|
|
1735
|
+
case 'g-skills': return loadGlobalSkills;
|
|
1736
|
+
case 'g-events': return loadEvents;
|
|
1846
1737
|
default: return undefined;
|
|
1847
1738
|
}
|
|
1848
1739
|
}
|
|
1849
|
-
const
|
|
1740
|
+
const paneLoaded = new Set();
|
|
1850
1741
|
|
|
1851
|
-
function
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
function ensureSectionLoaded(key) {
|
|
1856
|
-
if (sectionLoaded.has(key)) return;
|
|
1857
|
-
const loader = sectionLoader(key);
|
|
1742
|
+
function ensurePaneLoaded(key) {
|
|
1743
|
+
if (paneLoaded.has(key)) return;
|
|
1744
|
+
const loader = paneLoader(key);
|
|
1858
1745
|
if (!loader) return;
|
|
1859
|
-
|
|
1746
|
+
paneLoaded.add(key);
|
|
1860
1747
|
loader();
|
|
1861
1748
|
}
|
|
1862
1749
|
|
|
1863
|
-
function
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1750
|
+
function selectPane(scope, key) {
|
|
1751
|
+
const nav = scope === 'global' ? railNavGlobal : railNavConversation;
|
|
1752
|
+
const panel = document.getElementById(scope === 'global' ? 'panel-global' : 'panel-conversation');
|
|
1753
|
+
nav.querySelectorAll('.rail-link').forEach((btn) => {
|
|
1754
|
+
const active = btn.dataset.pane === key;
|
|
1755
|
+
btn.classList.toggle('active', active);
|
|
1756
|
+
btn.setAttribute('aria-current', active ? 'page' : 'false');
|
|
1757
|
+
});
|
|
1758
|
+
panel.querySelectorAll('.pane').forEach((pane) => {
|
|
1759
|
+
pane.classList.toggle('active', pane.dataset.pane === key);
|
|
1760
|
+
});
|
|
1761
|
+
ensurePaneLoaded(key);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function initRailNav() {
|
|
1765
|
+
document.querySelectorAll('.rail-link').forEach((btn) => {
|
|
1766
|
+
btn.addEventListener('click', () => {
|
|
1767
|
+
const nav = btn.closest('.rail-nav');
|
|
1768
|
+
const scope = nav.dataset.scope;
|
|
1769
|
+
selectPane(scope, btn.dataset.pane);
|
|
1872
1770
|
});
|
|
1873
1771
|
});
|
|
1772
|
+
// The first rail link in each scope starts active in the markup, so its
|
|
1773
|
+
// pane needs the matching 'active' class and its data loaded up front.
|
|
1774
|
+
selectPane('conversation', 'settings');
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// ── Conversation switcher ───────────────────────────────────────────────────
|
|
1778
|
+
|
|
1779
|
+
async function initConvSwitcher() {
|
|
1780
|
+
const sel = document.getElementById('conv-switcher');
|
|
1781
|
+
try {
|
|
1782
|
+
const data = await apiGet('/admin/api/conversations');
|
|
1783
|
+
sel.innerHTML = data.conversations.map((c) => {
|
|
1784
|
+
const key = c.platform + ':' + c.conversationId;
|
|
1785
|
+
const label = (c.label || c.conversationId) + (c.running ? ' (running)' : '');
|
|
1786
|
+
const selected = key === defaultConversationKey ? ' selected' : '';
|
|
1787
|
+
return '<option value="' + escAttr(key) + '"' + selected + '>' + escHtml(label) + '</option>';
|
|
1788
|
+
}).join('');
|
|
1789
|
+
sel.addEventListener('change', () => setActiveConversation(sel.value));
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
// ignore; conversation selector stays empty
|
|
1792
|
+
}
|
|
1874
1793
|
}
|
|
1875
1794
|
|
|
1876
1795
|
function setActiveConversation(key) {
|
|
1877
1796
|
activeConversationKey = key;
|
|
1878
1797
|
const sel = document.getElementById('conv-switcher');
|
|
1879
1798
|
if (sel && sel.value !== key) sel.value = key;
|
|
1880
|
-
// Data is scoped to the previous conversation; drop
|
|
1881
|
-
//
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1799
|
+
// Data is scoped to the previous conversation; drop the conversation
|
|
1800
|
+
// panes (not the global ones — those aren't conversation-scoped) so
|
|
1801
|
+
// the active pane refetches for the new scope.
|
|
1802
|
+
const activeLink = railNavConversation.querySelector('.rail-link.active');
|
|
1803
|
+
['settings', 'workspace', 'skills', 'mcp', 'events'].forEach((key) => paneLoaded.delete(key));
|
|
1804
|
+
if (activeLink) ensurePaneLoaded(activeLink.dataset.pane);
|
|
1886
1805
|
openLogin(true);
|
|
1887
1806
|
openSessionView(true);
|
|
1888
1807
|
}
|
|
@@ -2661,17 +2580,10 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
|
|
|
2661
2580
|
|
|
2662
2581
|
// ── Global section ──────────────────────────────────────────────────────────
|
|
2663
2582
|
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
loadAllConversations();
|
|
2669
|
-
loadTokenUsage();
|
|
2670
|
-
loadGlobalSettings();
|
|
2671
|
-
loadGlobalSkills();
|
|
2672
|
-
loadEvents();
|
|
2673
|
-
loadMcpServers();
|
|
2674
|
-
}
|
|
2583
|
+
// Switching to the Workspace scope only needs to (re)select its first
|
|
2584
|
+
// pane; every pane still loads lazily through paneLoader/ensurePaneLoaded
|
|
2585
|
+
// the first time it becomes active, same as the conversation scope.
|
|
2586
|
+
function initGlobal() {}
|
|
2675
2587
|
|
|
2676
2588
|
async function loadAllConversations() {
|
|
2677
2589
|
const container = document.getElementById('all-conv-content');
|
|
@@ -2923,10 +2835,10 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
|
|
|
2923
2835
|
|
|
2924
2836
|
// ── Init ─────────────────────────────────────────────────────────────────────
|
|
2925
2837
|
|
|
2926
|
-
//
|
|
2838
|
+
// Panes load on demand (see ensurePaneLoaded); loadSettingsPanel already
|
|
2927
2839
|
// calls loadModels() itself the first time it's needed.
|
|
2928
2840
|
initConvSwitcher();
|
|
2929
|
-
|
|
2841
|
+
initRailNav();
|
|
2930
2842
|
`;
|
|
2931
2843
|
function renderAdminPage(token) {
|
|
2932
2844
|
const userLabel = token.platformUserName ?? token.platformUserId;
|
|
@@ -2959,28 +2871,93 @@ function renderAdminErrorPage(message) {
|
|
|
2959
2871
|
}
|
|
2960
2872
|
// ── Styles ─────────────────────────────────────────────────────────────────────
|
|
2961
2873
|
const adminViewStyles = `
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
.
|
|
2970
|
-
|
|
2971
|
-
|
|
2874
|
+
/* The shared shell caps at 960px for the narrower session/login portals;
|
|
2875
|
+
the sidebar + single-pane layout here reads cramped at that width, so
|
|
2876
|
+
admin alone widens it. */
|
|
2877
|
+
.shell { max-width: 1180px; }
|
|
2878
|
+
|
|
2879
|
+
/* ── Settings shell: persistent rail + single active pane ───────────── */
|
|
2880
|
+
|
|
2881
|
+
.settings-shell {
|
|
2882
|
+
display: grid;
|
|
2883
|
+
grid-template-columns: 216px 1fr;
|
|
2884
|
+
align-items: start;
|
|
2885
|
+
gap: 22px;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
.settings-rail {
|
|
2889
|
+
position: sticky;
|
|
2890
|
+
top: 28px;
|
|
2891
|
+
display: flex;
|
|
2892
|
+
flex-direction: column;
|
|
2893
|
+
gap: 16px;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
.rail-scope {
|
|
2897
|
+
display: flex;
|
|
2898
|
+
padding: 3px;
|
|
2899
|
+
border: 1px solid var(--border);
|
|
2900
|
+
border-radius: 11px;
|
|
2901
|
+
background: rgba(0,0,0,0.03);
|
|
2902
|
+
gap: 2px;
|
|
2903
|
+
}
|
|
2904
|
+
.rail-scope-btn {
|
|
2905
|
+
flex: 1;
|
|
2906
|
+
padding: 7px 8px;
|
|
2907
|
+
border: none;
|
|
2908
|
+
border-radius: 8px;
|
|
2909
|
+
background: transparent;
|
|
2972
2910
|
color: var(--muted);
|
|
2973
|
-
font:
|
|
2974
|
-
|
|
2911
|
+
font: 600 0.72rem/1.2 'DM Sans', sans-serif;
|
|
2912
|
+
letter-spacing: 0.01em;
|
|
2913
|
+
cursor: pointer;
|
|
2975
2914
|
transition: background 140ms, color 140ms;
|
|
2976
2915
|
}
|
|
2977
|
-
.
|
|
2978
|
-
.
|
|
2979
|
-
.
|
|
2980
|
-
|
|
2981
|
-
.
|
|
2916
|
+
.rail-scope-btn:hover { color: var(--text); }
|
|
2917
|
+
.rail-scope-btn.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
2918
|
+
.rail-scope-btn:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
|
|
2919
|
+
|
|
2920
|
+
.rail-nav { display: flex; flex-direction: column; gap: 1px; }
|
|
2921
|
+
.rail-nav[hidden] { display: none; }
|
|
2922
|
+
.rail-link {
|
|
2923
|
+
display: flex; align-items: center; gap: 10px;
|
|
2924
|
+
padding: 8px 10px; border: none; border-radius: 9px;
|
|
2925
|
+
background: transparent; color: var(--muted); text-align: left;
|
|
2926
|
+
font: 500 0.86rem/1.2 'DM Sans', sans-serif; cursor: pointer;
|
|
2927
|
+
transition: background 120ms, color 120ms;
|
|
2928
|
+
}
|
|
2929
|
+
.rail-link svg { flex-shrink: 0; opacity: 0.75; }
|
|
2930
|
+
.rail-link:hover { background: rgba(0,0,0,0.045); color: var(--text); }
|
|
2931
|
+
.rail-link:hover svg { opacity: 1; }
|
|
2932
|
+
.rail-link.active {
|
|
2933
|
+
background: var(--text); color: #fafafa; font-weight: 600;
|
|
2934
|
+
}
|
|
2935
|
+
.rail-link.active svg { opacity: 1; }
|
|
2936
|
+
.rail-link:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }
|
|
2937
|
+
|
|
2938
|
+
.settings-panels { min-width: 0; }
|
|
2939
|
+
.tab-panel { display: none; flex-direction: column; gap: 0; }
|
|
2982
2940
|
.tab-panel.active { display: flex; }
|
|
2983
2941
|
|
|
2942
|
+
/* ── Panes: one continuous surface, not stacked cards ────────────────── */
|
|
2943
|
+
|
|
2944
|
+
.pane { display: none; padding: 30px 0; border-bottom: 1px solid var(--border); }
|
|
2945
|
+
.pane:last-child { border-bottom: none; }
|
|
2946
|
+
.pane.active { display: block; animation: pane-in 180ms ease; }
|
|
2947
|
+
@keyframes pane-in { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
|
|
2948
|
+
|
|
2949
|
+
.pane-head {
|
|
2950
|
+
display: flex; align-items: flex-start; justify-content: space-between;
|
|
2951
|
+
gap: 16px; margin-bottom: 22px; flex-wrap: wrap;
|
|
2952
|
+
}
|
|
2953
|
+
.pane-title {
|
|
2954
|
+
font-family: 'Lora', Georgia, serif; font-size: 1.32rem; font-weight: 600;
|
|
2955
|
+
letter-spacing: -0.01em; line-height: 1.25; margin-bottom: 5px;
|
|
2956
|
+
}
|
|
2957
|
+
.pane-desc { color: var(--muted); font-size: 0.88rem; line-height: 1.55; max-width: 52ch; }
|
|
2958
|
+
.pane-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
2959
|
+
.pane-body { display: flex; flex-direction: column; gap: 14px; }
|
|
2960
|
+
|
|
2984
2961
|
.card-desc { color: var(--muted); font-size: 0.9rem; line-height: 1.55; margin-bottom: 12px; }
|
|
2985
2962
|
|
|
2986
2963
|
.link-result {
|
|
@@ -3008,6 +2985,9 @@ const adminViewStyles = `
|
|
|
3008
2985
|
display: grid; grid-template-columns: 1fr 1fr; gap: 18px;
|
|
3009
2986
|
}
|
|
3010
2987
|
.config-block { display: flex; flex-direction: column; gap: 10px; }
|
|
2988
|
+
/* Config blocks are flex columns, so a plain child button stretches to the
|
|
2989
|
+
full column width. Keep the save action a normal-sized button instead. */
|
|
2990
|
+
.config-block .primary-action-btn { align-self: flex-start; min-width: 132px; justify-content: center; }
|
|
3011
2991
|
.config-row { display: grid; grid-template-columns: 110px 1fr; gap: 10px; align-items: center; }
|
|
3012
2992
|
.config-row.config-row-stack { grid-template-columns: 1fr; }
|
|
3013
2993
|
.config-row label { font-size: 0.82rem; color: var(--muted); }
|
|
@@ -3027,39 +3007,14 @@ const adminViewStyles = `
|
|
|
3027
3007
|
.inline-result.ok { background: var(--ok-bg); color: var(--ok-text); border: 1px solid var(--ok-border); }
|
|
3028
3008
|
.inline-result.err { background: var(--err-bg); color: var(--err-text); border: 1px solid var(--err-border); }
|
|
3029
3009
|
|
|
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
3010
|
.refresh-btn {
|
|
3056
3011
|
flex-shrink: 0; padding: 6px 12px;
|
|
3057
3012
|
border: 1px solid var(--border); border-radius: 10px;
|
|
3058
3013
|
background: rgba(0,0,0,0.025); color: var(--muted);
|
|
3059
|
-
font: 500 0.
|
|
3014
|
+
font: 500 0.8rem/1.2 'DM Sans', sans-serif; cursor: pointer;
|
|
3015
|
+
transition: background 120ms, color 120ms;
|
|
3060
3016
|
}
|
|
3061
3017
|
.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
3018
|
|
|
3064
3019
|
/* ── Workspace ──────────────────────────────────────────────────────── */
|
|
3065
3020
|
|
|
@@ -3110,14 +3065,14 @@ const adminViewStyles = `
|
|
|
3110
3065
|
.skill-row {
|
|
3111
3066
|
padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px;
|
|
3112
3067
|
background: rgba(0,0,0,0.02);
|
|
3113
|
-
display: flex;
|
|
3068
|
+
display: flex; flex-direction: column; align-items: stretch; gap: 6px;
|
|
3114
3069
|
}
|
|
3115
3070
|
.skill-row-btn {
|
|
3116
3071
|
flex: 1 1 auto; min-width: 0; text-align: left; cursor: pointer; font-family: inherit;
|
|
3117
3072
|
background: transparent; border: none; padding: 0;
|
|
3118
3073
|
}
|
|
3119
3074
|
.skill-row-btn:hover .skill-name { color: var(--accent); }
|
|
3120
|
-
.skill-row-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
3075
|
+
.skill-row-actions { display: flex; gap: 6px; flex-shrink: 0; justify-content: flex-end; }
|
|
3121
3076
|
.skill-name {
|
|
3122
3077
|
font-weight: 650; font-size: 0.9rem; color: var(--text);
|
|
3123
3078
|
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
@@ -3426,8 +3381,26 @@ const adminViewStyles = `
|
|
|
3426
3381
|
}
|
|
3427
3382
|
.status-pill.running { background: var(--ok-bg); color: var(--ok-text); border: 1px solid var(--ok-border); }
|
|
3428
3383
|
|
|
3384
|
+
@media (max-width: 860px) {
|
|
3385
|
+
.settings-shell { grid-template-columns: 1fr; }
|
|
3386
|
+
.settings-rail { position: static; }
|
|
3387
|
+
/* Keep labels on narrow screens: a scrollable chip row is far easier to
|
|
3388
|
+
scan than a row of bare icons with no text. */
|
|
3389
|
+
.rail-nav {
|
|
3390
|
+
flex-direction: row; flex-wrap: nowrap; gap: 6px;
|
|
3391
|
+
overflow-x: auto; padding-bottom: 6px;
|
|
3392
|
+
/* Without min-width:0 a nowrap flex row refuses to shrink and blows the
|
|
3393
|
+
page width out instead of scrolling. */
|
|
3394
|
+
min-width: 0; max-width: 100%;
|
|
3395
|
+
scrollbar-width: thin; -webkit-overflow-scrolling: touch;
|
|
3396
|
+
}
|
|
3397
|
+
.settings-rail { min-width: 0; }
|
|
3398
|
+
.rail-nav[hidden] { display: none; }
|
|
3399
|
+
.rail-link { flex: 0 0 auto; padding: 7px 11px; }
|
|
3400
|
+
.rail-link span { white-space: nowrap; }
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3429
3403
|
@media (max-width: 640px) {
|
|
3430
|
-
.tab-btn { padding: 9px 12px; font-size: 0.82rem; min-width: 60px; }
|
|
3431
3404
|
.config-grid { grid-template-columns: 1fr; }
|
|
3432
3405
|
.config-row { grid-template-columns: 1fr; gap: 4px; }
|
|
3433
3406
|
.workspace-split { grid-template-columns: 1fr; }
|