dsh-plugin-admin 0.4.2 → 0.5.0

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/lib/client.js CHANGED
@@ -2,14 +2,15 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-admin', factory: (require) => {
2
2
  var module = { exports: {} }; var exports = module.exports;
3
3
 
4
4
  /**
5
- * dsh-plugin-admin browser half: unified "管理中心" (插件管理 & 会话管理)
6
- * page inside the settings panel.
5
+ * dsh-plugin-admin browser half: administration surfaces parked where the
6
+ * settings shell already renders their domain.
7
7
  *
8
- * The page rides the shell's `settings.section` slot (the shell owns
9
- * the dialog chrome, navigation, and close); its component is assembled with
10
- * the platform-shared React (module-table seed word, same instance the shell
11
- * renders with), providing a tabbed modern UI to manage both profile plugin
12
- * layers and session lifecycles over the /api RPC gateway.
8
+ * Three slot contributions (all assembled with the platform-shared React,
9
+ * module-table seed word, same instance the shell renders with):
10
+ * - 扩展插件: a tab inside the shell-owned 插件 section (`settings.plugins.tab`),
11
+ * after 插件配置 and 插件列表;
12
+ * - MCP服务器 / 历史会话: standalone `settings.section` pages in the settings nav.
13
+ * Panels talk to the host over the /api RPC gateway.
13
14
  */
14
15
 
15
16
  var React = require('react')
@@ -23,12 +24,6 @@ var CSS_TEXT = [
23
24
  '@keyframes dsh-admin-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.85); } }',
24
25
  '[data-dsh-admin-section] { display: flex; flex-direction: column; width: 100%; gap: 14px; padding: 2px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, #222); max-height: calc(100vh - 140px); overflow: hidden; }',
25
26
  '[data-dsh-admin-section] * { box-sizing: border-box; }',
26
- '[data-dsh-admin-section] .tabs { display: flex; padding: 3px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.2)); border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3)); gap: 3px; }',
27
- '[data-dsh-admin-section] .tab { flex: 1; border: 0; background: transparent; border-radius: 7px; padding: 7px 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 7px; font-size: 12px; font-weight: 500; color: var(--dsw-alias-label-secondary, #666); transition: all 0.15s ease; font-family: inherit; }',
28
- '[data-dsh-admin-section] .tab:hover:not(.active) { color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); }',
29
- '[data-dsh-admin-section] .tab.active { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); color: var(--dsw-alias-label-primary, #333); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); }',
30
- '[data-dsh-admin-section] .tab-count { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-secondary, #555); }',
31
- '[data-dsh-admin-section] .tab.active .tab-count { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; }',
32
27
  '[data-dsh-admin-section] .toolbar { display: flex; gap: 8px; align-items: center; padding: 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 12px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); }',
33
28
  '[data-dsh-admin-section] .toolbar .input { height: 32px; padding: 0 12px; border-radius: 8px; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); }',
34
29
  '[data-dsh-admin-section] .search-wrap { flex: 1.6; min-width: 0; position: relative; display: flex; align-items: center; height: 32px; }',
@@ -63,6 +58,10 @@ var CSS_TEXT = [
63
58
  '[data-dsh-admin-section] .btn.sm { height: 28px; padding: 0 10px; font-size: 12px; border-radius: 6px; }',
64
59
  '[data-dsh-admin-section] .spinner { width: 12px; height: 12px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: dsh-admin-spin 0.7s linear infinite; display: inline-block; flex: none; }',
65
60
  '[data-dsh-admin-section] .list { display: flex; flex-direction: column; gap: 10px; flex: 1 1 auto; min-height: 0; max-height: 560px; overflow-y: auto; padding: 2px 4px 2px 2px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)) transparent; }',
61
+ '[data-dsh-admin-section] .list.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-content: start; }',
62
+ '[data-dsh-admin-section] .list.grid2 .empty { grid-column: 1 / -1; }',
63
+ '[data-dsh-admin-section] .list.grid2 .card { cursor: pointer; }',
64
+ '[data-dsh-admin-section] .list.grid2 .card:hover { transform: none; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); box-shadow: none; }',
66
65
  '[data-dsh-admin-section] .list::-webkit-scrollbar { width: 6px; }',
67
66
  '[data-dsh-admin-section] .list::-webkit-scrollbar-thumb { border-radius: 99px; background: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); }',
68
67
  '[data-dsh-admin-section] .session-group { display: flex; flex-direction: column; gap: 6px; }',
@@ -121,7 +120,7 @@ var CSS_TEXT = [
121
120
  '[data-dsh-admin-section] .mcp-test-fail { color: var(--dsw-alias-state-error-primary, #ef4444); }',
122
121
  '[data-dsh-admin-section] .mcp-test-warn { color: #d97706; opacity: 0.95; }',
123
122
  '[data-dsh-admin-section] .mcp-test-cached { font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); }',
124
- '@media (max-width: 520px) { [data-dsh-admin-section] { gap: 10px; } [data-dsh-admin-section] .toolbar { align-items: stretch; flex-wrap: wrap; padding: 8px; } [data-dsh-admin-section] .toolbar .input-wrap { flex-basis: 100%; } [data-dsh-admin-section] .card-header { align-items: flex-start; } [data-dsh-admin-section] .card-actions { flex-wrap: wrap; justify-content: flex-end; } [data-dsh-admin-section] .footer { gap: 6px; align-items: flex-start; flex-direction: column; } [data-dsh-admin-section] .footer .path { max-width: 100%; } }',
123
+ '@media (max-width: 520px) { [data-dsh-admin-section] { gap: 10px; } [data-dsh-admin-section] .list.grid2 { grid-template-columns: 1fr; } [data-dsh-admin-section] .toolbar { align-items: stretch; flex-wrap: wrap; padding: 8px; } [data-dsh-admin-section] .toolbar .input-wrap { flex-basis: 100%; } [data-dsh-admin-section] .card-header { align-items: flex-start; } [data-dsh-admin-section] .card-actions { flex-wrap: wrap; justify-content: flex-end; } [data-dsh-admin-section] .footer { gap: 6px; align-items: flex-start; flex-direction: column; } [data-dsh-admin-section] .footer .path { max-width: 100%; } }',
125
124
  '@keyframes dsh-toast-in { 0% { opacity: 0; transform: translateY(-12px) scale(0.96); } 100% { opacity: 1; transform: translateY(0) scale(1); } }',
126
125
  '@keyframes dsh-toast-out { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-8px) scale(0.96); } }',
127
126
  '.dsh-admin-toast { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); z-index: 99999; padding: 10px 20px; border-radius: 10px; font-size: 13px; font-weight: 500; line-height: 1.4; box-shadow: 0 8px 30px rgba(0,0,0,0.15), 0 2px 8px rgba(0,0,0,0.08); display: flex; align-items: center; gap: 8px; pointer-events: none; animation: dsh-toast-in 0.25s ease-out forwards; max-width: 90vw; word-break: break-word; }',
@@ -385,7 +384,7 @@ function setupMenuInjection(call, refreshSessions) {
385
384
  }
386
385
  if (exact.length === 0) { showToast('error', '❌ 未找到匹配的会话'); return }
387
386
  if (exact.length > 1) {
388
- showToast('error', '❌ 存在 ' + String(exact.length) + ' 个同名会话,无法确定要删除的目标;请在 设置 → 管理中心 → 会话管理 中按会话 ID 删除')
387
+ showToast('error', '❌ 存在 ' + String(exact.length) + ' 个同名会话,无法确定要删除的目标;请在 设置 → 历史会话 中按会话 ID 删除')
389
388
  return
390
389
  }
391
390
  cb(exact[0])
@@ -514,7 +513,8 @@ function setupMenuInjection(call, refreshSessions) {
514
513
  }
515
514
 
516
515
  /**
517
- * Plugins management settings section (standalone, no internal tabs).
516
+ * 扩展插件 tab contribution: plugin management rendered inside the shell-owned
517
+ * 插件 settings section (after 插件配置 and 插件列表).
518
518
  */
519
519
  function PluginsSection(props) {
520
520
  // Lazy initializer: the seed object (and its localStorage read) must be
@@ -751,7 +751,7 @@ function PluginsSection(props) {
751
751
  alive.current = true
752
752
  reloadPlugins()
753
753
  // Auto-check for remote updates on mount (the panel loads lazily when
754
- // the 插件管理 tab is opened, so this runs once per open). The auto path
754
+ // the 扩展插件 tab is opened, so this runs once per open). The auto path
755
755
  // is cache-friendly: the host serves its 5-minute cache, so re-opening
756
756
  // the tab shortly after does not re-hit the registry. The manual
757
757
  // ⬆ 检查更新 button is the force path — it bypasses the cache and
@@ -775,7 +775,7 @@ function PluginsSection(props) {
775
775
  }
776
776
 
777
777
  /**
778
- * Sessions management settings section (standalone, no internal tabs).
778
+ * 历史会话 settings section (standalone settings-nav page).
779
779
  */
780
780
  function SessionsSection(props) {
781
781
  var pairSessions = useState({
@@ -853,7 +853,7 @@ function SessionsSection(props) {
853
853
  }
854
854
 
855
855
  /**
856
- * MCP configuration settings section (standalone, no internal tabs).
856
+ * MCP服务器 settings section (standalone settings-nav page).
857
857
  */
858
858
 
859
859
  /* ------------------------------------------------------------------------ */
@@ -972,7 +972,7 @@ function formatTestTime(ms) {
972
972
  /* once a newer version is detected it is kept in localStorage until a */
973
973
  /* check confirms the installed version is current again (i.e. the update */
974
974
  /* actually completed). This is what keeps the badge visible after closing */
975
- /* and reopening the 管理中心, across dsh restarts, and even when a later */
975
+ /* and reopening the dialog, across dsh restarts, and even when a later */
976
976
  /* re-check hits a transient network failure — and what clears it only once */
977
977
  /* the plugin is upgraded (更新完删除提醒). */
978
978
  /* ------------------------------------------------------------------------ */
@@ -1356,35 +1356,6 @@ function McpSection(props) {
1356
1356
  renderMcpSection(mView, patchDraft, reloadMcp, openMcpEditor, closeMcpEditor, saveMcpDraft, removeMcpEntry, testMcpEntry))
1357
1357
  }
1358
1358
 
1359
- /** Unified settings page; only the selected panel mounts and loads. */
1360
- function AdminCenterSection(props) {
1361
- var pairTab = useState('plugins')
1362
- var activeTab = pairTab[0]
1363
- var setActiveTab = pairTab[1]
1364
- var tabs = [
1365
- { id: 'plugins', label: '插件管理', component: PluginsSection },
1366
- { id: 'sessions', label: '会话管理', component: SessionsSection },
1367
- { id: 'mcp', label: 'MCP 配置', component: McpSection },
1368
- ]
1369
- var selected = tabs.find(function (tab) { return tab.id === activeTab }) || tabs[0]
1370
-
1371
- return createElement('div', { 'data-dsh-admin-section': '' },
1372
- createElement('div', { className: 'tabs', role: 'tablist', 'aria-label': '管理中心功能' },
1373
- tabs.map(function (tab) {
1374
- return createElement('button', {
1375
- type: 'button',
1376
- key: tab.id,
1377
- className: 'tab' + (tab.id === selected.id ? ' active' : ''),
1378
- role: 'tab',
1379
- 'aria-selected': tab.id === selected.id,
1380
- onClick: function () { setActiveTab(tab.id) },
1381
- }, tab.label)
1382
- })
1383
- ),
1384
- createElement(selected.component, { key: selected.id, call: props.call, refreshSessions: props.refreshSessions })
1385
- )
1386
- }
1387
-
1388
1359
  /* ========================================================================== */
1389
1360
  /* Render Plugins View */
1390
1361
  /* ========================================================================== */
@@ -1392,8 +1363,9 @@ function AdminCenterSection(props) {
1392
1363
  function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade) {
1393
1364
  var elements = []
1394
1365
 
1395
- // Toolbar: search (primary, wide) + install input + actions in one row.
1396
- elements.push(createElement('div', { className: 'toolbar', key: 'toolbar' },
1366
+ // Toolbar, two rows: search alone on the first (it gets the full width),
1367
+ // install input + actions on the second.
1368
+ elements.push(createElement('div', { className: 'toolbar', key: 'toolbar-search' },
1397
1369
  createElement('div', { className: 'search-wrap', key: 'search-wrap' },
1398
1370
  createElement('span', { className: 'search-icon', key: 'icon' }, '🔍'),
1399
1371
  createElement('input', {
@@ -1411,6 +1383,8 @@ function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade)
1411
1383
  onClick: function () { patch({ needle: '' }) },
1412
1384
  }, '✕') : null
1413
1385
  ),
1386
+ ))
1387
+ elements.push(createElement('div', { className: 'toolbar', key: 'toolbar-install' },
1414
1388
  createElement('div', { className: 'input-wrap install-wrap', key: 'install-wrap' },
1415
1389
  createElement('input', {
1416
1390
  className: 'input',
@@ -1530,7 +1504,7 @@ function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade)
1530
1504
  ))
1531
1505
  }
1532
1506
 
1533
- elements.push(createElement('div', { className: 'list', key: 'list' }, rows))
1507
+ elements.push(createElement('div', { className: 'list grid2', key: 'list' }, rows))
1534
1508
 
1535
1509
  // Footer
1536
1510
  var hintText = view.note !== '' ? view.note : '更改在重启 dsh 后生效(关闭 dsh 进程后重新运行即可)'
@@ -1859,7 +1833,7 @@ function renderPluginCard(plugin, view, remove, patch, upgrade) {
1859
1833
  var isConfirming = view.confirming === plugin.name
1860
1834
 
1861
1835
  var titleChildren = [
1862
- createElement('span', { className: 'card-title-text', key: 'name' }, plugin.name),
1836
+ createElement('span', { className: 'card-title-text', key: 'name', title: plugin.name }, plugin.name),
1863
1837
  ]
1864
1838
  if (plugin.version) {
1865
1839
  titleChildren.push(createElement('span', { className: 'tag version', key: 'version' }, 'v' + plugin.version))
@@ -1994,7 +1968,17 @@ function filterSessions(sessions, filter, needle) {
1994
1968
  }
1995
1969
 
1996
1970
  /**
1997
- * Filter plugin layers by the type pill and a fuzzy name/version/path query.
1971
+ * Display rank for the plugin list: 内置 first, then 包安装 (registry
1972
+ * install), then 本地安装 (local path). Host order is kept within a rank.
1973
+ */
1974
+ function pluginSortRank(plugin) {
1975
+ if (!plugin.removable) return 0
1976
+ return plugin.localPath ? 2 : 1
1977
+ }
1978
+
1979
+ /**
1980
+ * Filter plugin layers by the type pill and a fuzzy name/version/path query,
1981
+ * then sort 内置 → 包安装 → 本地安装 (stable: equal ranks keep host order).
1998
1982
  * The needle matches case-insensitively against the package name, the
1999
1983
  * installed version, and the local source path, so "tool" finds
2000
1984
  * dsh-custom-tool and "0.2" finds version rows.
@@ -2011,7 +1995,7 @@ function filterPlugins(plugins, filter, needle) {
2011
1995
  }
2012
1996
  result.push(p)
2013
1997
  }
2014
- return result
1998
+ return result.sort(function (a, b) { return pluginSortRank(a) - pluginSortRank(b) })
2015
1999
  }/**
2016
2000
  * Group sessions by their accounting workspace in registry order; sessions
2017
2001
  * without a workspace trail under an "未分组" bucket. Empty workspaces are
@@ -2294,12 +2278,1529 @@ function renderSessionCard(session, view, act, patch) {
2294
2278
  return createElement('div', { key: session.id, className: 'card' }, cardChildren)
2295
2279
  }
2296
2280
 
2281
+ /* ========================================================================== */
2282
+ /* Subagent Administration (merged from dsh-plugin-subagents) */
2283
+ /* ========================================================================== */
2284
+
2285
+ /**
2286
+ * dsh-plugin-subagents browser half: the 子智能体管理 settings section.
2287
+ *
2288
+ * Renders a management panel over the shell's `settings.section` slot (same
2289
+ * platform-shared React and --dsw-* design tokens as the admin plugin) and
2290
+ * drives the host's `subagentAdmin` remote over the /api RPC gateway. One
2291
+ * managed row in the profile cordis.patch.yml equals one named subagent: a
2292
+ * `@deepseek-ai/dsh-tool-subagent` instance whose config carries the agent
2293
+ * name (toolName), the persona prompt, the tool constraint lists, and the
2294
+ * model specification (agentOptions).
2295
+ */
2296
+
2297
+ var TOOLNAME_RE = /^[a-z][a-z0-9_]{1,48}$/
2298
+ var TOOL_REF_RE = /^[a-z][a-z0-9_]*$/
2299
+ var ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/
2300
+ var RESERVED_TOOL_NAMES = ['subagent', 'subagent_fork', 'run_code']
2301
+
2302
+ var SA_CSS_TEXT = [
2303
+ '@keyframes dsh-sa-spin { to { transform: rotate(360deg); } }',
2304
+ '[data-dsh-sa-section] { display: flex; flex-direction: column; width: 100%; gap: 14px; padding: 2px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, #222); overflow: visible; }',
2305
+ '[data-dsh-sa-section] * { box-sizing: border-box; }',
2306
+ '[data-dsh-sa-section] .tabs { display: flex; padding: 3px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.2)); border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3)); gap: 3px; }',
2307
+ '[data-dsh-sa-section] .tab { flex: 1; border: 0; background: transparent; border-radius: 7px; padding: 7px 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 7px; font-size: 12px; font-weight: 500; color: var(--dsw-alias-label-secondary, #666); transition: all 0.15s ease; font-family: inherit; }',
2308
+ '[data-dsh-sa-section] .tab:hover:not(.active) { color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); }',
2309
+ '[data-dsh-sa-section] .tab.active { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); color: var(--dsw-alias-label-primary, #333); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); }',
2310
+ '[data-dsh-sa-section] .tab-count { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-secondary, #555); }',
2311
+ '[data-dsh-sa-section] .tab.active .tab-count { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; }',
2312
+ '[data-dsh-sa-section] .toolbar { display: flex; gap: 8px; align-items: center; padding: 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 12px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); flex-wrap: wrap; }',
2313
+ '[data-dsh-sa-section] .filterbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 12px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); }',
2314
+ '[data-dsh-sa-section] .filter-label { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #666); flex: none; margin-right: 2px; }',
2315
+ '[data-dsh-sa-section] .search-wrap { flex: 1; min-width: 150px; position: relative; display: flex; align-items: center; height: 32px; }',
2316
+ '[data-dsh-sa-section] .search-wrap .input { flex: 1; min-width: 0; padding-left: 30px; font-size: 13px; height: 32px; }',
2317
+ '[data-dsh-sa-section] .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 12px; opacity: 0.7; pointer-events: none; z-index: 1; }',
2318
+ '[data-dsh-sa-section] .input { width: 100%; padding: 7px 12px; border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-bg-base, transparent); color: inherit; font: inherit; outline: none; transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; }',
2319
+ '[data-dsh-sa-section] .input:hover { border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.6)); }',
2320
+ '[data-dsh-sa-section] .input:focus { border-color: var(--dsw-static-blue-500, #3b82f6); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16); }',
2321
+ '[data-dsh-sa-section] textarea.input { resize: vertical; min-height: 80px; font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 12px; line-height: 1.55; }',
2322
+ '[data-dsh-sa-section] select.input { height: 32px; padding: 0 8px; cursor: pointer; }',
2323
+ '[data-dsh-sa-section] .btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; height: 32px; padding: 0 14px; border-radius: 9px; font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit; transition: all 0.15s ease; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-interactive-bg-hover, rgba(240,240,245,0.6)); color: var(--dsw-alias-label-secondary, #555); white-space: nowrap; flex: none; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }',
2324
+ '[data-dsh-sa-section] .btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(220,220,230,0.7)); border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.65)); color: var(--dsw-alias-label-primary, #222); }',
2325
+ '[data-dsh-sa-section] .btn:active:not(:disabled) { transform: translateY(1px); }',
2326
+ '[data-dsh-sa-section] .btn:focus-visible { outline: 2px solid var(--dsw-static-blue-500, #3b82f6); outline-offset: 2px; }',
2327
+ '[data-dsh-sa-section] .btn:disabled { opacity: 0.45; cursor: not-allowed; }',
2328
+ '[data-dsh-sa-section] .btn.primary { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; border-color: transparent; font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }',
2329
+ '[data-dsh-sa-section] .btn.primary:hover:not(:disabled) { background: var(--dsw-static-blue-400, #60a5fa); color: #fff; }',
2330
+ '[data-dsh-sa-section] .btn.danger { color: var(--dsw-alias-state-error-primary, #dc2626); border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); }',
2331
+ '[data-dsh-sa-section] .btn.danger:hover:not(:disabled) { background: rgba(239,68,68,0.1); border-color: rgba(220,38,38,0.35); color: #b91c1c; }',
2332
+ '[data-dsh-sa-section] .btn.danger-solid { background: var(--dsw-alias-state-error-primary, #dc2626); color: #fff; border-color: transparent; }',
2333
+ '[data-dsh-sa-section] .btn.sm { height: 28px; padding: 0 10px; font-size: 12px; border-radius: 6px; }',
2334
+ '[data-dsh-sa-section] .btn.sm.active { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-static-blue-500, #3b82f6); font-weight: 600; border-color: currentColor; }',
2335
+ '[data-dsh-sa-section] .spinner { width: 12px; height: 12px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: dsh-sa-spin 0.7s linear infinite; display: inline-block; flex: none; }',
2336
+ '[data-dsh-sa-section] .list { display: flex; flex-direction: column; gap: 10px; flex: 1 1 auto; min-height: 0; max-height: 560px; overflow-y: auto; padding: 2px 4px 2px 2px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)) transparent; }',
2337
+ '[data-dsh-sa-section] .card { display: flex; flex-direction: column; gap: 7px; padding: 12px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); transition: all 0.15s ease; }',
2338
+ '[data-dsh-sa-section] .card:hover { transform: translateY(-1px); border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.6)); box-shadow: 0 8px 20px rgba(0,0,0,0.06); }',
2339
+ '[data-dsh-sa-section] .card-header { display: flex; align-items: center; gap: 8px; min-width: 0; }',
2340
+ '[data-dsh-sa-section] .card-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); display: flex; align-items: center; gap: 7px; min-width: 0; flex: 1; font-family: var(--dsw-font-mono, ui-monospace, monospace); }',
2341
+ '[data-dsh-sa-section] .card-actions { display: flex; gap: 5px; align-items: center; flex: none; }',
2342
+ '[data-dsh-sa-section] .persona-box { display: flex; align-items: flex-start; gap: 6px; padding: 6px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); border-left: 2px solid var(--dsw-static-blue-500, #3b82f6); font-size: 12px; line-height: 1.45; color: var(--dsw-alias-label-secondary, #555); }',
2343
+ '[data-dsh-sa-section] .persona-text { flex: 1; min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; word-break: break-word; white-space: pre-wrap; }',
2344
+ '[data-dsh-sa-section] .card-sub { font-size: 11px; color: var(--dsw-alias-label-secondary, #666); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }',
2345
+ '[data-dsh-sa-section] .tag { font-size: 10px; font-weight: 500; padding: 1px 6px; border-radius: 4px; flex: none; display: inline-flex; align-items: center; gap: 4px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); color: var(--dsw-alias-label-secondary, #555); }',
2346
+ '[data-dsh-sa-section] .tag.provider { background: rgba(139, 92, 246, 0.12); color: #7c3aed; }',
2347
+ '[data-dsh-sa-section] .tag.model { background: rgba(59, 130, 246, 0.12); color: var(--dsw-static-blue-500, #3b82f6); }',
2348
+ '[data-dsh-sa-section] .tag.live { background: rgba(16, 185, 129, 0.12); color: var(--dsw-alias-state-success-primary, #10b981); }',
2349
+ '[data-dsh-sa-section] .tag.warn { background: rgba(245, 158, 11, 0.14); color: #d97706; font-weight: 600; }',
2350
+ '[data-dsh-sa-section] .tag.dead { background: rgba(239, 68, 68, 0.12); color: var(--dsw-alias-state-error-primary, #dc2626); }',
2351
+ '[data-dsh-sa-section] .chips { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }',
2352
+ '[data-dsh-sa-section] .chip { font-size: 10px; font-family: var(--dsw-font-mono, ui-monospace, monospace); padding: 1px 6px; border-radius: 4px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-secondary, #555); }',
2353
+ '[data-dsh-sa-section] .chip.deny { background: rgba(239, 68, 68, 0.08); border-color: rgba(220,38,38,0.25); color: #b91c1c; }',
2354
+ '[data-dsh-sa-section] .chip.allow { background: rgba(16, 185, 129, 0.08); border-color: rgba(16,185,129,0.3); color: #047857; }',
2355
+ '[data-dsh-sa-section] .form { flex: 1 1 auto; min-height: 0; max-height: calc(100vh - 220px); overflow-y: auto; display: flex; flex-direction: column; gap: 10px; padding: 14px; border-radius: 12px; border: 1px solid var(--dsw-static-blue-500, #3b82f6); background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 4px 14px rgba(59,130,246,0.08); }',
2356
+ '[data-dsh-sa-section] .form-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); display: flex; align-items: center; gap: 6px; }',
2357
+ '[data-dsh-sa-section] .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }',
2358
+ '[data-dsh-sa-section] .form-grid .full { grid-column: 1 / -1; }',
2359
+ '[data-dsh-sa-section] .field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }',
2360
+ '[data-dsh-sa-section] .field-label { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary, #666); display: flex; align-items: center; gap: 5px; }',
2361
+ '[data-dsh-sa-section] .field-hint { font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); line-height: 1.4; }',
2362
+ '[data-dsh-sa-section] .fieldset { border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 10px; padding: 10px; display: flex; flex-direction: column; gap: 8px; }',
2363
+ '[data-dsh-sa-section] .fieldset-legend { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary, #666); padding: 0 4px; }',
2364
+ '[data-dsh-sa-section] .tag-input { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 8px; border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-bg-base, transparent); min-height: 34px; align-items: center; cursor: text; }',
2365
+ '[data-dsh-sa-section] .tag-input:focus-within { border-color: var(--dsw-static-blue-500, #3b82f6); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16); }',
2366
+ '[data-dsh-sa-section] .tag-input input { border: 0; outline: none; background: transparent; color: inherit; font: inherit; font-size: 12px; flex: 1; min-width: 90px; }',
2367
+ '[data-dsh-sa-section] .tag-chip { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-family: var(--dsw-font-mono, ui-monospace, monospace); padding: 2px 4px 2px 8px; border-radius: 5px; }',
2368
+ '[data-dsh-sa-section] .tag-chip.allow { background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16,185,129,0.3); color: #047857; }',
2369
+ '[data-dsh-sa-section] .tag-chip.deny { background: rgba(239, 68, 68, 0.08); border: 1px solid rgba(220,38,38,0.25); color: #b91c1c; }',
2370
+ '[data-dsh-sa-section] .tag-chip button { border: 0; background: transparent; cursor: pointer; color: inherit; opacity: 0.65; font-size: 11px; padding: 0 3px; line-height: 1; }',
2371
+ '[data-dsh-sa-section] .tag-chip button:hover { opacity: 1; }',
2372
+ '[data-dsh-sa-section] .checkbox-row { display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--dsw-alias-label-secondary, #555); cursor: pointer; }',
2373
+ '[data-dsh-sa-section] .checkbox-row input { accent-color: var(--dsw-static-blue-500, #3b82f6); }',
2374
+ '[data-dsh-sa-section] .env-row { display: flex; gap: 6px; align-items: center; }',
2375
+ '[data-dsh-sa-section] .env-row .env-key { flex: 2; min-width: 0; }',
2376
+ '[data-dsh-sa-section] .env-row .env-value { flex: 3; min-width: 0; }',
2377
+ '[data-dsh-sa-section] .cli-toggle { border: 0; background: transparent; cursor: pointer; color: var(--dsw-alias-label-secondary, #555); font-size: 12px; line-height: 1; padding: 3px 6px; border-radius: 6px; }',
2378
+ '[data-dsh-sa-section] .cli-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); color: var(--dsw-alias-label-primary, #222); }',
2379
+ '[data-dsh-sa-section] .cli-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary, #222); display: inline-flex; align-items: center; flex: none; white-space: nowrap; font-family: var(--dsw-font-mono, ui-monospace, monospace); cursor: pointer; }',
2380
+ '[data-dsh-sa-section] .cli-tags { display: flex; flex-wrap: wrap; gap: 4px; flex: 1; min-width: 0; align-items: center; }',
2381
+ '[data-dsh-sa-section] .cli-detail { display: flex; flex-direction: column; gap: 8px; }',
2382
+ '[data-dsh-sa-section] .cli-scan-list { display: flex; flex-direction: column; gap: 6px; }',
2383
+ '[data-dsh-sa-section] .cli-scan-row { display: flex; gap: 8px; align-items: center; }',
2384
+ '[data-dsh-sa-section] .cli-scan-row .input { flex: 1; min-width: 0; }',
2385
+ '[data-dsh-sa-section] .cli-scan-hint { font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-secondary, #666); }',
2386
+ '[data-dsh-sa-section] .cli-scan-card { display: flex; gap: 8px; align-items: center; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.12)); }',
2387
+ '[data-dsh-sa-section] .cli-scan-card .btn { margin-left: auto; flex: none; }',
2388
+ '[data-dsh-sa-section] .error-strip { display: flex; align-items: flex-start; gap: 8px; padding: 9px 12px; border-radius: 9px; border: 1px solid rgba(220,38,38,0.35); background: rgba(239,68,68,0.08); color: var(--dsw-alias-state-error-primary, #dc2626); font-size: 12px; line-height: 1.45; word-break: break-word; }',
2389
+ '[data-dsh-sa-section] .warn-strip { display: flex; flex-direction: column; gap: 4px; padding: 9px 12px; border-radius: 9px; border: 1px solid rgba(217,119,6,0.35); background: rgba(245,158,11,0.08); color: #b45309; font-size: 12px; line-height: 1.5; word-break: break-word; }',
2390
+ '[data-dsh-sa-section] .warn-strip div { display: flex; gap: 6px; align-items: flex-start; }',
2391
+ '[data-dsh-sa-section] .empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 34px 12px; border: 1px dashed var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); border-radius: 12px; color: var(--dsw-alias-label-tertiary, #999); font-size: 12px; text-align: center; }',
2392
+ '[data-dsh-sa-section] .empty .big { font-size: 26px; }',
2393
+ '[data-dsh-sa-section] .toast { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); z-index: 10000; max-width: 520px; padding: 10px 16px; border-radius: 10px; font-size: 12px; line-height: 1.5; box-shadow: 0 8px 24px rgba(0,0,0,0.18); background: rgba(239,68,68,0.96); color: #fff; word-break: break-word; white-space: pre-line; }',
2394
+ '[data-dsh-sa-section] .history-row { display: flex; flex-direction: column; gap: 3px; padding: 9px 12px; border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); }',
2395
+ '[data-dsh-sa-section] .history-head { display: flex; align-items: center; gap: 8px; font-size: 12px; flex-wrap: wrap; }',
2396
+ '[data-dsh-sa-section] .history-time { font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); margin-left: auto; }',
2397
+ '[data-dsh-sa-section] .history-detail { font-size: 11px; color: var(--dsw-alias-label-tertiary, #888); font-family: var(--dsw-font-mono, ui-monospace, monospace); word-break: break-all; max-height: 60px; overflow: hidden; }',
2398
+ '[data-dsh-sa-section] .footer-note { font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); line-height: 1.5; padding: 0 2px; }',
2399
+ '[data-dsh-sa-section] .sa-picker { position: relative; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }',
2400
+ '[data-dsh-sa-section] .sa-picker-input { border: 0; outline: none; background: transparent; color: inherit; font: inherit; font-size: 12px; flex: 1; min-width: 120px; padding: 2px 4px; }',
2401
+ '[data-dsh-sa-section] .sa-picker-list { width: 100%; margin-top: 4px; max-height: 220px; overflow-y: auto; background: var(--dsh-alias-bg-elevated, #fff); border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); border-radius: 8px; box-shadow: 0 6px 18px rgba(0,0,0,0.14); padding: 3px; scrollbar-width: thin; }',
2402
+ '[data-dsh-sa-section] .sa-picker-opt { padding: 6px 9px; font-size: 12px; cursor: pointer; border-radius: 5px; color: var(--dsw-alias-label-primary, #222); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
2403
+ '[data-dsh-sa-section] .sa-picker-opt.active, [data-dsh-sa-section] .sa-picker-opt:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(59,130,246,0.14)); color: var(--dsw-static-blue-500, #3b82f6); }',
2404
+ '[data-dsh-sa-section] .sa-picker-empty { padding: 6px 9px; font-size: 11px; color: var(--dsw-alias-label-tertiary, #999); }',
2405
+ ]
2406
+
2407
+ var SA_STYLE_SEEN = false
2408
+ function injectSaStyles() {
2409
+ if (SA_STYLE_SEEN) return
2410
+ var style = document.createElement('style')
2411
+ style.setAttribute('data-dsh-sa-styles', '')
2412
+ style.textContent = SA_CSS_TEXT.join('\n')
2413
+ document.head.appendChild(style)
2414
+ SA_STYLE_SEEN = true
2415
+ }
2416
+
2417
+ /** The gateway wraps every remote result in an { ok, value | error } envelope. */
2418
+ function unwrap(result) {
2419
+ if (result && typeof result === 'object' && 'ok' in result) {
2420
+ if (result.ok) return result.value
2421
+ var detail = result.error && result.error.message ? result.error.message : String(result.error)
2422
+ throw new Error(detail)
2423
+ }
2424
+ return result
2425
+ }
2426
+
2427
+ /* ========================================================================== */
2428
+ /* Small components */
2429
+ /* ========================================================================== */
2430
+
2431
+ function Spinner() {
2432
+ return createElement('span', { className: 'spinner' })
2433
+ }
2434
+
2435
+ /** Two-click inline confirm; disarms after 3.2s. */
2436
+ function ConfirmButton(props) {
2437
+ var armed = useState(false)
2438
+ var isArmed = armed[0]
2439
+ var setArmed = armed[1]
2440
+ var timer = useRef(null)
2441
+ useEffect(function () {
2442
+ return function () { if (timer.current) clearTimeout(timer.current) }
2443
+ }, [])
2444
+ var label = isArmed ? (props.confirmLabel || '确认?') : props.label
2445
+ return createElement('button', {
2446
+ type: 'button',
2447
+ className: 'btn sm ' + (isArmed ? 'danger-solid' : 'danger'),
2448
+ disabled: props.disabled === true,
2449
+ onClick: function () {
2450
+ if (!isArmed) {
2451
+ setArmed(true)
2452
+ timer.current = setTimeout(function () { setArmed(false) }, 3200)
2453
+ return
2454
+ }
2455
+ if (timer.current) clearTimeout(timer.current)
2456
+ setArmed(false)
2457
+ props.onConfirm()
2458
+ },
2459
+ }, label)
2460
+ }
2461
+
2462
+ /* ========================================================================== */
2463
+ /* Form draft handling */
2464
+ /* ========================================================================== */
2465
+
2466
+ function emptyDraft() {
2467
+ return {
2468
+ id: '',
2469
+ toolName: '',
2470
+ provider: 'spawn',
2471
+ persona: '',
2472
+ allow: [],
2473
+ deny: [],
2474
+ agentProvider: '',
2475
+ agentModel: '',
2476
+ maxTokens: '',
2477
+ maxDepthManaged: false,
2478
+ maxDepth: '3',
2479
+ backgroundMode: 'one-shot',
2480
+ enableRunInBackground: true,
2481
+ }
2482
+ }
2483
+
2484
+ function draftFromEntry(entry) {
2485
+ var config = entry.config || {}
2486
+ return {
2487
+ id: entry.id,
2488
+ toolName: config.toolName || '',
2489
+ provider: config.provider || 'spawn',
2490
+ persona: config.persona || '',
2491
+ allow: (config.toolFilter && config.toolFilter.allow) || [],
2492
+ deny: (config.toolFilter && config.toolFilter.deny) || [],
2493
+ agentProvider: (config.agentOptions && config.agentOptions.provider) || '',
2494
+ agentModel: (config.agentOptions && config.agentOptions.model) || '',
2495
+ maxTokens: config.agentOptions && config.agentOptions.maxTokens !== undefined ? String(config.agentOptions.maxTokens) : '',
2496
+ maxDepthManaged: config.maxDepth === 'provider-managed',
2497
+ maxDepth: typeof config.maxDepth === 'number' ? String(config.maxDepth) : '3',
2498
+ backgroundMode: config.backgroundMode || 'one-shot',
2499
+ enableRunInBackground: config.enableRunInBackground !== false,
2500
+ }
2501
+ }
2502
+
2503
+ /** Client-side validation mirroring the host rules; returns an error string or null. */
2504
+ function validateDraft(draft, isCreate, otherToolNames, providerMeta, candidateNames) {
2505
+ if (isCreate && !ID_RE.test(draft.id)) return '实例 ID 只能包含字母、数字、下划线和中划线(字母或数字开头,最长 64 位)'
2506
+ if (!TOOLNAME_RE.test(draft.toolName)) return '子智能体名称必须是 2-48 位小写字母/数字/下划线且字母开头'
2507
+ if (RESERVED_TOOL_NAMES.indexOf(draft.toolName) !== -1) return '子智能体名称 "' + draft.toolName + '" 是保留名(内置预设已占用)'
2508
+ if (otherToolNames.indexOf(draft.toolName) !== -1) return '子智能体名称 "' + draft.toolName + '" 已被其他实例使用'
2509
+ var meta = providerMeta[draft.provider]
2510
+ if (!meta) return '未知的执行后端 "' + draft.provider + '"'
2511
+ if (draft.persona !== '' && meta.capabilities.persona === false) return '后端 "' + draft.provider + '" 不支持 persona(提示词)'
2512
+ if ((draft.allow.length > 0 || draft.deny.length > 0) && meta.capabilities.toolFilter === false) return '后端 "' + draft.provider + '" 不支持 toolFilter(工具约束)'
2513
+ if (!draft.maxDepthManaged) {
2514
+ if (draft.maxDepth !== '' && !/^\d+$/.test(draft.maxDepth)) return '最大委托深度必须是非负整数'
2515
+ if (draft.maxDepth !== '' && meta.capabilities.depthLimit === false) {
2516
+ return '后端 "' + draft.provider + '" 无法执行数值 maxDepth;请勾选「交由后端管理」或换后端'
2517
+ }
2518
+ }
2519
+ if (draft.maxTokens !== '' && !/^\d+$/.test(draft.maxTokens)) return 'maxTokens 必须是正整数'
2520
+ var filterNames = (draft.allow || []).concat(draft.deny || [])
2521
+ for (var fi = 0; fi < filterNames.length; fi++) {
2522
+ var tname = filterNames[fi]
2523
+ if (RESERVED_TOOL_NAMES.indexOf(tname) !== -1) return '工具 "' + tname + '" 是保留名(' + RESERVED_TOOL_NAMES.join('/') + '),不能用于工具约束'
2524
+ if (candidateNames && candidateNames.length > 0 && candidateNames.indexOf(tname) === -1) return '工具 "' + tname + '" 不在可选清单中(来自运行中工具或内置名录),保存会被拒绝'
2525
+ }
2526
+ return null
2527
+ }
2528
+
2529
+ function draftToPayload(draft) {
2530
+ var config = { provider: draft.provider, toolName: draft.toolName }
2531
+ if (draft.persona.trim() !== '') config.persona = draft.persona
2532
+ if (draft.allow.length > 0 || draft.deny.length > 0) {
2533
+ config.toolFilter = {}
2534
+ if (draft.allow.length > 0) config.toolFilter.allow = draft.allow.slice()
2535
+ if (draft.deny.length > 0) config.toolFilter.deny = draft.deny.slice()
2536
+ }
2537
+ var agentOptions = {}
2538
+ if (draft.agentProvider.trim() !== '') agentOptions.provider = draft.agentProvider.trim()
2539
+ if (draft.agentModel.trim() !== '') agentOptions.model = draft.agentModel.trim()
2540
+ if (draft.maxTokens.trim() !== '') agentOptions.maxTokens = Number(draft.maxTokens)
2541
+ if (Object.keys(agentOptions).length > 0) config.agentOptions = agentOptions
2542
+ if (draft.maxDepthManaged) config.maxDepth = 'provider-managed'
2543
+ else if (draft.maxDepth !== '') config.maxDepth = Number(draft.maxDepth)
2544
+ config.backgroundMode = draft.backgroundMode
2545
+ config.enableRunInBackground = draft.enableRunInBackground === true
2546
+ return { id: draft.id.trim(), config: config }
2547
+ }
2548
+
2549
+ /* ========================================================================== */
2550
+ /* Searchable picker (single/multi) */
2551
+ /* ========================================================================== */
2552
+
2553
+ /**
2554
+ * Filterable picker used for the model/provider selects (single) and the
2555
+ * tool-constraint chips (multi). Renders a text input plus a scrollable,
2556
+ * filtered dropdown; click or Enter selects. Single mode also commits the
2557
+ * typed text (so arbitrary values still work); multi mode appends chips and
2558
+ * hides already-picked options from the list.
2559
+ */
2560
+ function Picker(props) {
2561
+ var multi = props.multi === true
2562
+ var values = props.values || []
2563
+ var options = props.options || []
2564
+ var allowCustom = props.allowCustom !== false
2565
+ var inputRef = useRef(null)
2566
+ var textState = useState('')
2567
+ var text = textState[0]
2568
+ var setText = textState[1]
2569
+ var openState = useState(false)
2570
+ var open = openState[0]
2571
+ var setOpen = openState[1]
2572
+ var highlightState = useState(0)
2573
+ var highlight = highlightState[0]
2574
+ var setHighlight = highlightState[1]
2575
+
2576
+ var selected = {}
2577
+ for (var si = 0; si < values.length; si++) selected[values[si]] = true
2578
+
2579
+ var filtered = options.filter(function (o) {
2580
+ if (multi && selected[o.value]) return false
2581
+ var q = text.trim().toLowerCase()
2582
+ if (q === '') return true
2583
+ var label = (o.label || o.value).toLowerCase()
2584
+ return label.indexOf(q) !== -1 || o.value.toLowerCase().indexOf(q) !== -1
2585
+ })
2586
+
2587
+ function emit(next) { props.onChange(next) }
2588
+
2589
+ function pick(opt) {
2590
+ if (multi) {
2591
+ if (selected[opt.value] || !TOOL_REF_RE.test(opt.value)) return
2592
+ emit(values.concat([opt.value]))
2593
+ setText(''); setHighlight(0); setOpen(true)
2594
+ } else {
2595
+ emit([opt.value]); setText(''); setOpen(false)
2596
+ }
2597
+ }
2598
+ function addTyped(v) {
2599
+ var val = (v || '').trim()
2600
+ if (val === '') return
2601
+ if (multi && (selected[val] || !TOOL_REF_RE.test(val))) return
2602
+ emit(multi ? values.concat([val]) : [val])
2603
+ setText(''); setHighlight(0)
2604
+ }
2605
+ function onInputChange(e) {
2606
+ var v = e.target.value
2607
+ setText(v); setOpen(true); setHighlight(0)
2608
+ if (!multi) emit([v])
2609
+ }
2610
+ function onKeyDown(e) {
2611
+ var live = (inputRef.current ? inputRef.current.value : text).trim()
2612
+ if (e.key === 'ArrowDown') { e.preventDefault(); setHighlight(function (h) { return Math.min(h + 1, filtered.length - 1) }); return }
2613
+ if (e.key === 'ArrowUp') { e.preventDefault(); setHighlight(function (h) { return Math.max(h - 1, 0) }); return }
2614
+ if (e.key === 'Enter') {
2615
+ e.preventDefault()
2616
+ if (filtered.length > 0 && highlight >= 0 && highlight < filtered.length) {
2617
+ var hl = filtered[highlight]
2618
+ if (live === '' || live === hl.value || live === (hl.label || hl.value)) { pick(hl); return }
2619
+ }
2620
+ if (allowCustom && live !== '') addTyped(live)
2621
+ return
2622
+ }
2623
+ if (e.key === 'Escape') setOpen(false)
2624
+ }
2625
+
2626
+ var tokens = multi ? values.map(function (v) {
2627
+ return createElement('span', { className: 'tag-chip ' + (props.kind || 'allow'), key: 'tok-' + v },
2628
+ v,
2629
+ createElement('button', {
2630
+ type: 'button', 'aria-label': '移除 ' + v,
2631
+ onClick: function (ev) { ev.stopPropagation(); emit(values.filter(function (x) { return x !== v })) },
2632
+ }, '✕'))
2633
+ }) : null
2634
+
2635
+ var list = open
2636
+ ? createElement('div', { className: 'sa-picker-list' },
2637
+ filtered.length === 0
2638
+ ? createElement('div', { className: 'sa-picker-empty' }, allowCustom && text.trim() !== '' ? '回车添加:' + text.trim() : '无匹配')
2639
+ : filtered.map(function (o, idx) {
2640
+ return createElement('div', {
2641
+ key: o.value,
2642
+ className: 'sa-picker-opt' + (idx === highlight ? ' active' : ''),
2643
+ onMouseDown: function (ev) { ev.preventDefault(); pick(o) },
2644
+ onMouseEnter: function () { setHighlight(idx) },
2645
+ }, o.label || o.value)
2646
+ }))
2647
+ : null
2648
+
2649
+ return createElement('div', { className: 'sa-picker' + (multi ? ' tag-input' : '') },
2650
+ tokens,
2651
+ createElement('input', {
2652
+ ref: inputRef,
2653
+ className: 'sa-picker-input',
2654
+ value: multi ? text : (values[0] || ''),
2655
+ placeholder: props.placeholder || '',
2656
+ 'aria-label': props.ariaLabel || props.placeholder || '',
2657
+ onChange: onInputChange,
2658
+ onKeyDown: onKeyDown,
2659
+ onFocus: function () { setOpen(true) },
2660
+ onBlur: function () { setOpen(false) },
2661
+ }),
2662
+ list,
2663
+ )
2664
+ }
2665
+
2666
+ /* ========================================================================== */
2667
+ /* Subagents panel */
2668
+ /* ========================================================================== */
2669
+
2670
+ function SubagentsPanel(props) {
2671
+ var call = props.call
2672
+
2673
+ var viewState = useState({ loading: true, error: null, data: null })
2674
+ var view = viewState[0]
2675
+ var setView = viewState[1]
2676
+
2677
+ var formState = useState(null)
2678
+ var form = formState[0]
2679
+ var setForm = formState[1]
2680
+
2681
+ var needleState = useState('')
2682
+ var needle = needleState[0]
2683
+ var setNeedle = needleState[1]
2684
+
2685
+ var providerFilterState = useState('全部')
2686
+ var providerFilter = providerFilterState[0]
2687
+ var setProviderFilter = providerFilterState[1]
2688
+
2689
+ var noticeState = useState(null)
2690
+ var notice = noticeState[0]
2691
+ var setNotice = noticeState[1]
2692
+
2693
+ var toastState = useState(null)
2694
+ var toast = toastState[0]
2695
+ var setToast = toastState[1]
2696
+
2697
+ var reload = function () {
2698
+ setView(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
2699
+ call('subagentAdmin/list', {}).then(function (raw) {
2700
+ var result = unwrap(raw)
2701
+ setView({ loading: false, error: null, data: result })
2702
+ }).catch(function (error) {
2703
+ setView({ loading: false, error: String((error && error.message) || error), data: null })
2704
+ })
2705
+ }
2706
+ useEffect(reload, [])
2707
+
2708
+ useEffect(function () {
2709
+ if (!toast) return undefined
2710
+ var timer = setTimeout(function () { setToast(null) }, 4200)
2711
+ return function () { clearTimeout(timer) }
2712
+ }, [toast])
2713
+
2714
+ var data = view.data
2715
+ var entries = (data && data.entries) || []
2716
+ var meta = (data && data.meta) || { tools: [], providers: [], llmProviders: [], llmModels: {} }
2717
+ var candidateNames = meta.tools.map(function (tool) { return tool.name })
2718
+ var providerMeta = {}
2719
+ meta.providers.forEach(function (provider) { providerMeta[provider.name] = provider })
2720
+ var llmProviders = meta.llmProviders || []
2721
+ var llmModels = meta.llmModels || {}
2722
+
2723
+ var filtered = entries.filter(function (entry) {
2724
+ var config = entry.config || {}
2725
+ if (providerFilter !== '全部' && config.provider !== providerFilter) return false
2726
+ if (needle === '') return true
2727
+ var haystack = [entry.id, config.toolName, config.provider, config.persona, config.agentOptions && config.agentOptions.model]
2728
+ .filter(Boolean).join(' ').toLowerCase()
2729
+ return haystack.indexOf(needle.toLowerCase()) !== -1
2730
+ })
2731
+
2732
+ var openCreate = function () {
2733
+ setNotice(null)
2734
+ setForm({ draft: emptyDraft(), editing: false, saving: false, error: null })
2735
+ }
2736
+ var openEdit = function (entry) {
2737
+ setNotice(null)
2738
+ setForm({ draft: draftFromEntry(entry), editing: true, saving: false, error: null })
2739
+ }
2740
+ var closeForm = function () { setForm(null) }
2741
+
2742
+ var saveForm = function () {
2743
+ if (!form || form.saving) return
2744
+ var otherToolNames = entries
2745
+ .filter(function (entry) { return !form.editing || entry.id !== form.draft.id })
2746
+ .map(function (entry) { return (entry.config || {}).toolName })
2747
+ .filter(Boolean)
2748
+ var clientError = validateDraft(form.draft, !form.editing, otherToolNames, providerMeta, candidateNames)
2749
+ if (clientError) {
2750
+ setForm(Object.assign({}, form, { error: clientError }))
2751
+ return
2752
+ }
2753
+ var saving = Object.assign({}, form, { saving: true, error: null })
2754
+ setForm(saving)
2755
+ var payload = draftToPayload(saving.draft)
2756
+ call('subagentAdmin/upsert', { entry: payload }).then(function (raw) {
2757
+ var result = unwrap(raw)
2758
+ setView(function (prev) { return Object.assign({}, prev, { data: result, loading: false }) })
2759
+ setForm(null)
2760
+ setNotice(result.warnings && result.warnings.length > 0 ? result.warnings : null)
2761
+ }).catch(function (error) {
2762
+ var message = String((error && error.message) || error)
2763
+ setForm(Object.assign({}, saving, { saving: false, error: message }))
2764
+ setToast('保存失败:' + message)
2765
+ })
2766
+ }
2767
+
2768
+ var removeEntry = function (entry) {
2769
+ call('subagentAdmin/remove', { id: entry.id }).then(function (raw) {
2770
+ var result = unwrap(raw)
2771
+ setView(function (prev) { return Object.assign({}, prev, { data: result, loading: false }) })
2772
+ setNotice(null)
2773
+ }).catch(function (error) {
2774
+ setToast('删除失败:' + String((error && error.message) || error))
2775
+ })
2776
+ }
2777
+
2778
+ var patchDraft = function (patch) {
2779
+ setForm(function (prev) {
2780
+ return Object.assign({}, prev, { draft: Object.assign({}, prev.draft, patch) })
2781
+ })
2782
+ }
2783
+
2784
+ var children = []
2785
+
2786
+ // Main toolbar: search + primary actions. Provider filters live on their own
2787
+ // row below so a growing backend list never squeezes the search box.
2788
+ children.push(createElement('div', { className: 'toolbar', key: 'toolbar' },
2789
+ createElement('div', { className: 'search-wrap', key: 'search' },
2790
+ createElement('span', { className: 'search-icon' }, '🔍'),
2791
+ createElement('input', {
2792
+ className: 'input',
2793
+ placeholder: '搜索子智能体(名称/ID/后端/提示词/模型)...',
2794
+ value: needle,
2795
+ onChange: function (event) { setNeedle(event.target.value) },
2796
+ })
2797
+ ),
2798
+ createElement('button', { type: 'button', className: 'btn sm primary', key: 'create', onClick: openCreate }, '+ 新建子智能体'),
2799
+ createElement('button', { type: 'button', className: 'btn sm', key: 'refresh', onClick: reload, disabled: view.loading },
2800
+ view.loading ? createElement(Spinner, { key: 'spin' }) : '↻', '刷新')
2801
+ ))
2802
+
2803
+ if (meta.providers.length > 1) {
2804
+ children.push(createElement('div', { className: 'filterbar', key: 'filterbar' },
2805
+ createElement('span', { className: 'filter-label' }, '执行后端'),
2806
+ createElement('button', {
2807
+ type: 'button', key: 'pill-all',
2808
+ className: 'btn sm' + (providerFilter === '全部' ? ' active' : ''),
2809
+ onClick: function () { setProviderFilter('全部') },
2810
+ }, '全部'),
2811
+ meta.providers.map(function (provider) {
2812
+ return createElement('button', {
2813
+ type: 'button', key: 'pill-' + provider.name,
2814
+ className: 'btn sm' + (providerFilter === provider.name ? ' active' : ''),
2815
+ onClick: function () { setProviderFilter(providerFilter === provider.name ? '全部' : provider.name) },
2816
+ }, provider.name)
2817
+ })
2818
+ ))
2819
+ }
2820
+
2821
+ if (form) {
2822
+ children.push(SubagentForm({
2823
+ form: form,
2824
+ meta: meta,
2825
+ candidateNames: candidateNames,
2826
+ providerMeta: providerMeta,
2827
+ llmProviders: llmProviders,
2828
+ llmModels: llmModels,
2829
+ entries: entries,
2830
+ onPatch: patchDraft,
2831
+ onClose: closeForm,
2832
+ onSave: saveForm,
2833
+ }))
2834
+ }
2835
+
2836
+ if (notice) {
2837
+ children.push(createElement('div', { className: 'warn-strip', key: 'notice' },
2838
+ notice.map(function (warning, index) {
2839
+ return createElement('div', { key: index }, '⚠️ ', warning)
2840
+ })
2841
+ ))
2842
+ }
2843
+
2844
+ if (!form) {
2845
+ if (view.loading && entries.length === 0) {
2846
+ children.push(createElement('div', { className: 'empty', key: 'loading' },
2847
+ createElement('span', { className: 'big' }, '⏳'),
2848
+ '正在加载子智能体配置…'))
2849
+ } else if (view.error) {
2850
+ children.push(createElement('div', { className: 'error-strip', key: 'error' }, '⚠️ 加载失败:', view.error))
2851
+ } else if (filtered.length === 0) {
2852
+ children.push(createElement('div', { className: 'empty', key: 'empty' },
2853
+ createElement('span', { className: 'big' }, '🤖'),
2854
+ entries.length === 0
2855
+ ? '还没有受管子智能体。点击「+ 新建子智能体」创建第一个:名称、提示词、工具约束、模型指定全部可配。'
2856
+ : '没有匹配当前搜索/筛选的子智能体。'))
2857
+ } else {
2858
+ children.push(createElement('div', { className: 'list', key: 'cards' }, filtered.map(function (entry) {
2859
+ return SubagentCard({
2860
+ entry: entry,
2861
+ onEdit: openEdit,
2862
+ onRemove: removeEntry,
2863
+ })
2864
+ })))
2865
+ }
2866
+ }
2867
+
2868
+ children.push(createElement('div', { className: 'footer-note', key: 'note' },
2869
+ '每张卡片对应 profile cordis.patch.yml 中一个 @deepseek-ai/dsh-tool-subagent 行;保存即写入该文件,由 Cordis HMR 热加载生效(无需重启),重启 dsh 后同样自动加载。首次修改前原文件自动备份为 cordis.patch.yml.bak-subagent-admin。'))
2870
+
2871
+ return createElement('div', null,
2872
+ toast ? createElement('div', { className: 'toast' }, toast) : null,
2873
+ children
2874
+ )
2875
+ }
2876
+
2877
+ function SubagentForm(props) {
2878
+ var form = props.form
2879
+ var draft = form.draft
2880
+ var meta = props.meta
2881
+ var candidateNames = props.candidateNames
2882
+ var providerMeta = props.providerMeta
2883
+ var llmProviders = props.llmProviders || []
2884
+ var llmModels = props.llmModels || {}
2885
+ var entries = props.entries
2886
+ var onPatch = props.onPatch
2887
+ var onClose = props.onClose
2888
+ var onSave = props.onSave
2889
+
2890
+ var providerInfo = providerMeta[draft.provider]
2891
+ var capabilityHint = providerInfo
2892
+ ? '能力:' + [
2893
+ providerInfo.capabilities.persona ? '✓ 提示词' : '✗ 提示词',
2894
+ providerInfo.capabilities.toolFilter ? '✓ 工具约束' : '✗ 工具约束',
2895
+ providerInfo.capabilities.depthLimit ? '✓ 深度上限' : '✗ 深度上限',
2896
+ providerInfo.continuable ? '✓ 可持续会话' : '仅一次性',
2897
+ ].join(' / ')
2898
+ : ''
2899
+ // Model picker options: the selected provider's catalog, else every model.
2900
+ var modelOptions = draft.agentProvider && llmModels[draft.agentProvider]
2901
+ ? llmModels[draft.agentProvider]
2902
+ : [].concat.apply([], Object.keys(llmModels).map(function (k) { return llmModels[k] }))
2903
+
2904
+ return createElement('div', { className: 'form', key: 'form' },
2905
+ createElement('div', { className: 'form-title' },
2906
+ form.editing ? '✏️ 编辑子智能体' : '✨ 新建子智能体',
2907
+ createElement('span', { style: { marginLeft: 'auto', display: 'flex', gap: '6px' } },
2908
+ createElement('button', { type: 'button', className: 'btn sm', onClick: onClose, disabled: form.saving }, '取消'),
2909
+ createElement('button', { type: 'button', className: 'btn sm primary', onClick: onSave, disabled: form.saving },
2910
+ form.saving ? createElement(Spinner, { key: 'spin' }) : null,
2911
+ '保存')
2912
+ )
2913
+ ),
2914
+ form.error ? createElement('div', { className: 'error-strip' }, '⚠️ ', form.error) : null,
2915
+ createElement('div', { className: 'form-grid' },
2916
+ createElement('div', { className: 'field' },
2917
+ createElement('span', { className: 'field-label' }, '实例 ID'),
2918
+ createElement('input', {
2919
+ className: 'input', value: draft.id, disabled: form.editing || form.saving,
2920
+ placeholder: '如 researcher、code-reviewer',
2921
+ onChange: function (event) { onPatch({ id: event.target.value }) },
2922
+ }),
2923
+ createElement('span', { className: 'field-hint' }, '补丁行标识,创建后不可改;持久化在 profile 的 cordis.patch.yml')
2924
+ ),
2925
+ createElement('div', { className: 'field' },
2926
+ createElement('span', { className: 'field-label' }, '子智能体名称(模型可见工具名)'),
2927
+ createElement('input', {
2928
+ className: 'input', value: draft.toolName, disabled: form.saving,
2929
+ placeholder: '如 web_researcher(模型用它发起委托)',
2930
+ onChange: function (event) { onPatch({ toolName: event.target.value }) },
2931
+ }),
2932
+ createElement('span', { className: 'field-hint' }, '不能用保留名 subagent / subagent_fork / run_code,且各实例间唯一')
2933
+ ),
2934
+ createElement('div', { className: 'field' },
2935
+ createElement('span', { className: 'field-label' }, '执行后端(provider)'),
2936
+ createElement('select', {
2937
+ className: 'input', value: draft.provider, disabled: form.saving,
2938
+ onChange: function (event) { onPatch({ provider: event.target.value }) },
2939
+ },
2940
+ meta.providers.map(function (provider) {
2941
+ return createElement('option', { key: provider.name, value: provider.name }, provider.name)
2942
+ })
2943
+ ),
2944
+ createElement('span', { className: 'field-hint' }, capabilityHint)
2945
+ ),
2946
+ createElement('div', { className: 'field' },
2947
+ createElement('span', { className: 'field-label' }, '后台模式'),
2948
+ createElement('select', {
2949
+ className: 'input', value: draft.backgroundMode, disabled: form.saving,
2950
+ onChange: function (event) { onPatch({ backgroundMode: event.target.value }) },
2951
+ },
2952
+ createElement('option', { value: 'one-shot' }, 'one-shot(一次性任务)'),
2953
+ createElement('option', { value: 'continuable' }, 'continuable(可持续会话)')
2954
+ ),
2955
+ createElement('label', { className: 'checkbox-row' },
2956
+ createElement('input', {
2957
+ type: 'checkbox', checked: draft.enableRunInBackground === true, disabled: form.saving,
2958
+ onChange: function (event) { onPatch({ enableRunInBackground: event.target.checked }) },
2959
+ }),
2960
+ '暴露 run_in_background 参数'
2961
+ )
2962
+ ),
2963
+ createElement('div', { className: 'field full' },
2964
+ createElement('span', { className: 'field-label' }, '提示词(persona,留空继承部署默认)'),
2965
+ createElement('textarea', {
2966
+ className: 'input', value: draft.persona, rows: 4, disabled: form.saving,
2967
+ placeholder: '该子智能体的人设/职责说明…支持 {{model}} 与 {{cwd}} 模板变量',
2968
+ onChange: function (event) { onPatch({ persona: event.target.value }) },
2969
+ }),
2970
+ createElement('span', { className: 'field-hint' }, '保存后影子覆盖(shadow)部署级 persona,仅对该子智能体生效')
2971
+ ),
2972
+ createElement('div', { className: 'fieldset full' },
2973
+ createElement('span', { className: 'fieldset-legend' }, '工具约束(toolFilter,留空则不限制;可选项来自 harness 已注册工具与内置工具名录,也可手填未知名)'),
2974
+ createElement('div', { className: 'field' },
2975
+ createElement('span', { className: 'field-label' }, '仅允许(allow 白名单)'),
2976
+ createElement(Picker, {
2977
+ multi: true, kind: 'allow', allowCustom: true,
2978
+ values: draft.allow, options: candidateNames.map(function (n) { return { value: n, label: n } }),
2979
+ placeholder: '输入或选择工具名,如 read / glob / grep', ariaLabel: '仅允许工具',
2980
+ onChange: function (next) { onPatch({ allow: next }) },
2981
+ }),
2982
+ createElement('span', { className: 'field-hint' }, '设置后子智能体只保留名单内工具,其余从提示词移除且拒绝执行')
2983
+ ),
2984
+ createElement('div', { className: 'field' },
2985
+ createElement('span', { className: 'field-label' }, '禁止(deny 黑名单)'),
2986
+ createElement(Picker, {
2987
+ multi: true, kind: 'deny', allowCustom: true,
2988
+ values: draft.deny, options: candidateNames.map(function (n) { return { value: n, label: n } }),
2989
+ placeholder: '输入或选择工具名,如 bash / pwsh', ariaLabel: '禁止工具',
2990
+ onChange: function (next) { onPatch({ deny: next }) },
2991
+ }),
2992
+ createElement('span', { className: 'field-hint' }, '名单内工具对子智能体不可见;未知工具名保存时会被拒绝')
2993
+ )
2994
+ ),
2995
+ createElement('div', { className: 'fieldset full' },
2996
+ createElement('span', { className: 'fieldset-legend' }, '模型指定(agentOptions,留空字段继承父代理当前路由)'),
2997
+ createElement('div', { className: 'form-grid' },
2998
+ createElement('div', { className: 'field' },
2999
+ createElement('span', { className: 'field-label' }, 'LLM provider'),
3000
+ createElement(Picker, {
3001
+ multi: false, allowCustom: true,
3002
+ values: draft.agentProvider ? [draft.agentProvider] : [],
3003
+ options: llmProviders.map(function (p) { return { value: p.id, label: p.name } }),
3004
+ placeholder: '留空继承,如 optirouter / deepseek-official', ariaLabel: 'LLM provider',
3005
+ onChange: function (next) { onPatch({ agentProvider: next[0] || '' }) },
3006
+ })
3007
+ ),
3008
+ createElement('div', { className: 'field' },
3009
+ createElement('span', { className: 'field-label' }, '模型标识(model)'),
3010
+ createElement(Picker, {
3011
+ multi: false, allowCustom: true,
3012
+ values: draft.agentModel ? [draft.agentModel] : [],
3013
+ options: modelOptions.map(function (m) { return { value: m.id, label: m.name } }),
3014
+ placeholder: '留空继承,如 auto', ariaLabel: '模型标识',
3015
+ onChange: function (next) { onPatch({ agentModel: next[0] || '' }) },
3016
+ }),
3017
+ createElement('span', { className: 'field-hint' }, '从已配置模型中选择,或手填模型 id(需在该 provider 路由上注册)')
3018
+ ),
3019
+ createElement('div', { className: 'field' },
3020
+ createElement('span', { className: 'field-label' }, 'maxTokens(单次回复上限)'),
3021
+ createElement('input', {
3022
+ className: 'input', value: draft.maxTokens, disabled: form.saving,
3023
+ placeholder: '留空使用默认', inputMode: 'numeric',
3024
+ onChange: function (event) { onPatch({ maxTokens: event.target.value.replace(/[^0-9]/g, '') }) },
3025
+ })
3026
+ ),
3027
+ createElement('div', { className: 'field' },
3028
+ createElement('span', { className: 'field-label' }, '最大委托深度(maxDepth,0 = 禁止再委托)'),
3029
+ createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
3030
+ createElement('input', {
3031
+ className: 'input', value: draft.maxDepthManaged ? '' : draft.maxDepth,
3032
+ disabled: form.saving || draft.maxDepthManaged,
3033
+ placeholder: '3(默认)', inputMode: 'numeric',
3034
+ onChange: function (event) { onPatch({ maxDepth: event.target.value.replace(/[^0-9]/g, '') }) },
3035
+ }),
3036
+ createElement('label', { className: 'checkbox-row', style: { flex: 'none' } },
3037
+ createElement('input', {
3038
+ type: 'checkbox', checked: draft.maxDepthManaged, disabled: form.saving,
3039
+ onChange: function (event) { onPatch({ maxDepthManaged: event.target.checked }) },
3040
+ }),
3041
+ '交由后端管理'
3042
+ )
3043
+ )
3044
+ )
3045
+ )
3046
+ )
3047
+ )
3048
+ )
3049
+ }
3050
+
3051
+ function SubagentCard(props) {
3052
+ var entry = props.entry
3053
+ var config = entry.config || {}
3054
+ var live = entry.live || {}
3055
+ var liveTag = live.providerPresent === false
3056
+ ? createElement('span', { className: 'tag dead' }, '❌ 后端未注册')
3057
+ : live.toolRegistered
3058
+ ? createElement('span', { className: 'tag live' }, '✅ 已挂载')
3059
+ : createElement('span', { className: 'tag warn' }, '⚠️ 工具未挂载')
3060
+ var hasModelRoute = config.agentOptions && (config.agentOptions.model || config.agentOptions.provider)
3061
+ var modelLabel = hasModelRoute
3062
+ ? ((config.agentOptions.model || '') + (config.agentOptions.provider ? (config.agentOptions.model ? ' @ ' : '') + config.agentOptions.provider : ''))
3063
+ : '继承父代理'
3064
+
3065
+ var children = [
3066
+ createElement('div', { className: 'card-header', key: 'head' },
3067
+ createElement('span', { className: 'card-title' }, '🤖 ', config.toolName || entry.id),
3068
+ createElement('span', { className: 'card-actions' },
3069
+ createElement('button', { type: 'button', className: 'btn sm', onClick: function () { props.onEdit(entry) } }, '编辑'),
3070
+ createElement(ConfirmButton, {
3071
+ label: '删除', confirmLabel: '确认删除?',
3072
+ onConfirm: function () { props.onRemove(entry) },
3073
+ })
3074
+ )
3075
+ ),
3076
+ createElement('div', { className: 'card-sub', key: 'tags' },
3077
+ createElement('span', { className: 'tag provider' }, '⚙ ' + (config.provider || '?')),
3078
+ createElement('span', { className: 'tag model' }, '🧠 ' + modelLabel),
3079
+ createElement('span', { className: 'tag' }, config.backgroundMode === 'continuable' ? '可持续' : '一次性'),
3080
+ typeof config.maxDepth === 'number' ? createElement('span', { className: 'tag' }, '深度≤' + config.maxDepth) : null,
3081
+ config.maxDepth === 'provider-managed' ? createElement('span', { className: 'tag' }, '深度=后端管理') : null,
3082
+ liveTag
3083
+ ),
3084
+ ]
3085
+
3086
+ if (config.persona) {
3087
+ children.push(createElement('div', { className: 'persona-box', key: 'persona' },
3088
+ createElement('span', null, '📝'),
3089
+ createElement('span', { className: 'persona-text' }, config.persona)
3090
+ ))
3091
+ }
3092
+
3093
+ var chips = []
3094
+ if (config.toolFilter && config.toolFilter.allow && config.toolFilter.allow.length > 0) {
3095
+ chips.push(createElement('span', { key: 'allow-label', style: { fontSize: '10px', color: '#047857', fontWeight: 600 } }, '仅允许'))
3096
+ config.toolFilter.allow.forEach(function (name) {
3097
+ chips.push(createElement('span', { key: 'a-' + name, className: 'chip allow' }, name))
3098
+ })
3099
+ }
3100
+ if (config.toolFilter && config.toolFilter.deny && config.toolFilter.deny.length > 0) {
3101
+ chips.push(createElement('span', { key: 'deny-label', style: { fontSize: '10px', color: '#b91c1c', fontWeight: 600 } }, '禁止'))
3102
+ config.toolFilter.deny.forEach(function (name) {
3103
+ chips.push(createElement('span', { key: 'd-' + name, className: 'chip deny' }, name))
3104
+ })
3105
+ }
3106
+ if (chips.length > 0) {
3107
+ children.push(createElement('div', { className: 'chips', key: 'chips' }, chips))
3108
+ }
3109
+
3110
+ children.push(createElement('div', { className: 'card-sub', key: 'meta' },
3111
+ createElement('span', { className: 'tag', title: entry.id }, 'ID: ' + entry.id),
3112
+ config.toolFilter === undefined ? createElement('span', { className: 'tag' }, '工具不限制') : null
3113
+ ))
3114
+
3115
+ return createElement('div', { className: 'card', key: entry.id }, children)
3116
+ }
3117
+
3118
+ /* ========================================================================== */
3119
+ /* History panel */
3120
+ /* ========================================================================== */
3121
+
3122
+ function HistoryPanel(props) {
3123
+ var call = props.call
3124
+ var state = useState({ loading: true, error: null, data: null })
3125
+ var view = state[0]
3126
+ var setView = state[1]
3127
+
3128
+ var reload = function () {
3129
+ setView(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
3130
+ call('subagentAdmin/history', { limit: 200 }).then(function (raw) {
3131
+ setView({ loading: false, error: null, data: unwrap(raw) })
3132
+ }).catch(function (error) {
3133
+ setView({ loading: false, error: String((error && error.message) || error), data: null })
3134
+ })
3135
+ }
3136
+ useEffect(reload, [])
3137
+
3138
+ var actionLabels = { create: '新建', update: '更新', delete: '删除', corrupt: '损坏记录', mount: '挂载', unmount: '卸载', 'cli-update': 'CLI 配置' }
3139
+ var children = [
3140
+ createElement('div', { className: 'toolbar', key: 'bar' },
3141
+ createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #666)', flex: 1 } },
3142
+ '配置台账:每次新建/更新/删除都追加一条记录(JSONL),文件路径见下方。'),
3143
+ createElement('button', { type: 'button', className: 'btn sm', onClick: reload, disabled: view.loading },
3144
+ view.loading ? createElement(Spinner, { key: 'spin' }) : '↻', '刷新')
3145
+ ),
3146
+ ]
3147
+
3148
+ if (view.error) {
3149
+ children.push(createElement('div', { className: 'error-strip', key: 'err' }, '⚠️ 加载失败:', view.error))
3150
+ } else if (view.data && view.data.records && view.data.records.length > 0) {
3151
+ children.push(createElement('div', { className: 'footer-note', key: 'path' }, '台账文件:' + view.data.path))
3152
+ children.push(createElement('div', { className: 'list', key: 'rows' }, view.data.records.map(function (record, index) {
3153
+ return createElement('div', { className: 'history-row', key: index },
3154
+ createElement('div', { className: 'history-head' },
3155
+ createElement('span', {
3156
+ className: 'tag ' + (record.action === 'delete' || record.action === 'unmount' ? 'dead'
3157
+ : record.action === 'create' || record.action === 'mount' ? 'live' : 'warn'),
3158
+ }, actionLabels[record.action] || record.action),
3159
+ createElement('span', { style: { fontWeight: 600 } }, record.id || '—'),
3160
+ record.toolName ? createElement('span', { className: 'tag' }, record.toolName) : null,
3161
+ createElement('span', { className: 'history-time' }, record.at || '')
3162
+ ),
3163
+ record.entry ? createElement('div', { className: 'history-detail' }, JSON.stringify(record.entry.config || record.entry)) : null
3164
+ )
3165
+ })))
3166
+ } else if (!view.loading) {
3167
+ children.push(createElement('div', { className: 'empty', key: 'empty' },
3168
+ createElement('span', { className: 'big' }, '📜'),
3169
+ '还没有变更记录。每次新建、编辑或删除子智能体后都会在这里留痕。'))
3170
+ }
3171
+
3172
+ return createElement('div', null, children)
3173
+ }
3174
+
3175
+ /* ========================================================================== */
3176
+ /* CLI backends */
3177
+ /* ========================================================================== */
3178
+
3179
+ /** Draft shape for one CLI backend's editable config. */
3180
+ function cliDraftFromConfig(config) {
3181
+ config = config || {}
3182
+ return {
3183
+ providerName: config.providerName !== undefined ? String(config.providerName) : '',
3184
+ permissionMode: config.permissionMode !== undefined ? String(config.permissionMode) : '',
3185
+ disposeGraceMs: config.disposeGraceMs !== undefined ? String(config.disposeGraceMs) : '3000',
3186
+ envPairs: Object.keys(config.env || {}).map(function (key) {
3187
+ return { key: key, value: String(config.env[key]) }
3188
+ }),
3189
+ }
3190
+ }
3191
+
3192
+ /** Convert a draft back into the wire config object. */
3193
+ function cliConfigFromDraft(draft) {
3194
+ var config = {
3195
+ providerName: draft.providerName.trim(),
3196
+ permissionMode: draft.permissionMode,
3197
+ disposeGraceMs: Number(draft.disposeGraceMs.trim()),
3198
+ env: {},
3199
+ }
3200
+ draft.envPairs.forEach(function (pair) {
3201
+ var key = pair.key.trim()
3202
+ if (key !== '') config.env[key] = pair.value
3203
+ })
3204
+ return config
3205
+ }
3206
+
3207
+ /** Shared env key/value pair editor (used by every CLI backend card). */
3208
+ function EnvPairsEditor(props) {
3209
+ var pairs = props.pairs || []
3210
+ return createElement('div', { className: 'fieldset' },
3211
+ createElement('span', { className: 'fieldset-legend' }, 'env(传给 CLI 子进程的额外环境变量)'),
3212
+ pairs.map(function (pair, index) {
3213
+ return createElement('div', { className: 'env-row', key: index },
3214
+ createElement('input', {
3215
+ className: 'input env-key', value: pair.key,
3216
+ placeholder: '变量名(如 OPENAI_API_KEY)',
3217
+ onChange: function (event) { props.onPatchPair(index, { key: event.target.value }) },
3218
+ }),
3219
+ createElement('input', {
3220
+ className: 'input env-value', value: pair.value,
3221
+ placeholder: '变量值',
3222
+ onChange: function (event) { props.onPatchPair(index, { value: event.target.value }) },
3223
+ }),
3224
+ createElement('button', { type: 'button', className: 'btn sm', onClick: function () { props.onRemove(index) } }, '✕'),
3225
+ )
3226
+ }),
3227
+ createElement('button', { type: 'button', className: 'btn sm', onClick: props.onAdd }, '+ 添加变量'),
3228
+ )
3229
+ }
3230
+
3231
+ /** One CLI backend card: collapsible status header + config form + mount/save/unmount. */
3232
+ function CliBackendCard(props) {
3233
+ var backend = props.backend
3234
+ var draft = props.draft
3235
+ var busy = props.busy === true
3236
+ var expanded = props.expanded === true
3237
+ var onPatch = props.onPatch
3238
+ var onPatchEnvPair = props.onPatchEnvPair
3239
+ var onAddEnvPair = props.onAddEnvPair
3240
+ var onRemoveEnvPair = props.onRemoveEnvPair
3241
+ var onToggle = props.onToggle
3242
+ var onSave = props.onSave
3243
+ var onUnmount = props.onUnmount
3244
+
3245
+ var packageOk = !!(backend.providerPackage && backend.providerPackage.ok)
3246
+ var runner = backend.runner || { ok: false, version: null }
3247
+ var cli = backend.cli || { ok: false, version: null }
3248
+ var modeValue = backend.permissionModes.indexOf(draft.permissionMode) !== -1
3249
+ ? draft.permissionMode
3250
+ : backend.permissionModes[0]
3251
+
3252
+ var onToggle = props.onToggle
3253
+ var onSave = props.onSave
3254
+ var onInstall = props.onInstall
3255
+ var onUnmount = props.onUnmount
3256
+
3257
+ var packageOk = !!(backend.providerPackage && backend.providerPackage.ok)
3258
+ var runner = backend.runner || { ok: false, version: null }
3259
+ var cli = backend.cli || { ok: false, version: null }
3260
+ var missingPackages = backend.missing || []
3261
+ var depsMissing = missingPackages.length > 0
3262
+ var missingLabel = missingPackages.join(' + ')
3263
+ var modeValue = backend.permissionModes.indexOf(draft.permissionMode) !== -1
3264
+ ? draft.permissionMode
3265
+ : backend.permissionModes[0]
3266
+
3267
+ return createElement('div', { className: 'card', key: backend.id },
3268
+ createElement('div', { className: 'card-header' },
3269
+ createElement('button', {
3270
+ type: 'button', className: 'cli-toggle',
3271
+ 'aria-expanded': expanded,
3272
+ 'aria-label': expanded ? '收起明细' : '展开明细',
3273
+ title: expanded ? '收起明细' : '展开明细',
3274
+ onClick: onToggle,
3275
+ }, expanded ? '▾' : '▸'),
3276
+ createElement('span', { className: 'cli-title', onClick: onToggle }, '🔌 ' + backend.label),
3277
+ createElement('span', { className: 'cli-tags' },
3278
+ createElement('span', { className: 'tag ' + (backend.mounted ? 'live' : '') }, backend.mounted ? '已挂载' : '未挂载'),
3279
+ createElement('span', { className: 'tag ' + (packageOk ? 'live' : 'dead') },
3280
+ packageOk
3281
+ ? 'provider 包' + (backend.providerPackage.version ? ' v' + backend.providerPackage.version : ' ✓')
3282
+ : 'provider 包 ✗'),
3283
+ createElement('span', { className: 'tag ' + (runner.ok ? 'live' : 'warn') },
3284
+ runner.ok ? 'CLI 依赖 ✓' : 'CLI 依赖 ✗'),
3285
+ createElement('span', { className: 'tag ' + (cli.ok ? 'model' : 'warn') },
3286
+ cli.ok ? 'PATH ' + cli.version : 'PATH ✗ ' + backend.cliCommand),
3287
+ ),
3288
+ createElement('span', { style: { display: 'flex', gap: '5px', flex: 'none' } },
3289
+ depsMissing
3290
+ ? createElement('button', {
3291
+ type: 'button', className: 'btn sm',
3292
+ disabled: busy,
3293
+ title: 'npm install -g 全局安装缺失包:' + missingLabel,
3294
+ onClick: onInstall,
3295
+ }, busy ? createElement(Spinner, { key: 'spin' }) : null, busy ? '安装中…' : '安装依赖包')
3296
+ : null,
3297
+ backend.mounted
3298
+ ? createElement('button', { type: 'button', className: 'btn sm primary', disabled: busy, onClick: onSave },
3299
+ busy ? createElement(Spinner, { key: 'spin' }) : null, '保存配置')
3300
+ : packageOk
3301
+ ? createElement('button', { type: 'button', className: 'btn sm primary', disabled: busy, onClick: onSave },
3302
+ busy ? createElement(Spinner, { key: 'spin' }) : null, '挂载')
3303
+ : null,
3304
+ backend.mounted
3305
+ ? createElement(ConfirmButton, { label: '卸载', confirmLabel: '确认卸载?', disabled: busy, onConfirm: onUnmount })
3306
+ : null,
3307
+ )
3308
+ ),
3309
+ expanded ? createElement('div', { className: 'cli-detail', key: 'detail' },
3310
+ createElement('div', { className: 'form-grid' },
3311
+ createElement('div', { className: 'field' },
3312
+ createElement('span', { className: 'field-label' }, 'providerName(执行后端注册名)'),
3313
+ createElement('input', {
3314
+ className: 'input', value: draft.providerName,
3315
+ placeholder: '如 codex / claude-code(子智能体表单的执行后端选项)',
3316
+ onChange: function (event) { onPatch({ providerName: event.target.value }) },
3317
+ }),
3318
+ ),
3319
+ createElement('div', { className: 'field' },
3320
+ createElement('span', { className: 'field-label' }, 'permissionMode(CLI 权限模式)'),
3321
+ createElement('select', {
3322
+ className: 'input', value: modeValue,
3323
+ onChange: function (event) { onPatch({ permissionMode: event.target.value }) },
3324
+ }, backend.permissionModes.map(function (mode) {
3325
+ return createElement('option', { key: mode, value: mode }, mode)
3326
+ })),
3327
+ createElement('span', { className: 'field-hint' }, '枚举来自该 provider 包的 Config schema')
3328
+ ),
3329
+ createElement('div', { className: 'field' },
3330
+ createElement('span', { className: 'field-label' }, 'disposeGraceMs(进程树终止宽限,毫秒)'),
3331
+ createElement('input', {
3332
+ className: 'input', value: draft.disposeGraceMs,
3333
+ placeholder: '3000',
3334
+ onChange: function (event) { onPatch({ disposeGraceMs: event.target.value }) },
3335
+ }),
3336
+ ),
3337
+ ),
3338
+ createElement(EnvPairsEditor, {
3339
+ pairs: draft.envPairs,
3340
+ onPatchPair: onPatchEnvPair,
3341
+ onAdd: onAddEnvPair,
3342
+ onRemove: onRemoveEnvPair,
3343
+ }),
3344
+ ) : null,
3345
+ )
3346
+ }
3347
+
3348
+ /** Draft shape for one generic external-CLI backend's editable config. */
3349
+ function cliDraftFromGeneric(backend) {
3350
+ backend = backend || {}
3351
+ return {
3352
+ command: backend.command !== undefined ? String(backend.command) : '',
3353
+ argsText: Array.isArray(backend.args) ? backend.args.join(' ') : '{prompt}',
3354
+ providerName: backend.providerName !== undefined ? String(backend.providerName) : '',
3355
+ disposeGraceMs: backend.disposeGraceMs !== undefined ? String(backend.disposeGraceMs) : '3000',
3356
+ envPairs: Object.keys(backend.env || {}).map(function (key) {
3357
+ return { key: key, value: String(backend.env[key]) }
3358
+ }),
3359
+ }
3360
+ }
3361
+
3362
+ /** Convert a generic draft into the wire config object (args split on whitespace). */
3363
+ function cliConfigFromGenericDraft(draft) {
3364
+ var config = {
3365
+ command: draft.command.trim(),
3366
+ args: draft.argsText.trim().split(/\s+/).filter(Boolean),
3367
+ providerName: draft.providerName.trim(),
3368
+ disposeGraceMs: Number(draft.disposeGraceMs.trim()),
3369
+ env: {},
3370
+ }
3371
+ draft.envPairs.forEach(function (pair) {
3372
+ var key = pair.key.trim()
3373
+ if (key !== '') config.env[key] = pair.value
3374
+ })
3375
+ return config
3376
+ }
3377
+
3378
+ /** One generic external-CLI backend card (served by this plugin's command provider). */
3379
+ function GenericCliCard(props) {
3380
+ var backend = props.backend
3381
+ var draft = props.draft
3382
+ var busy = props.busy === true
3383
+ var expanded = props.expanded === true
3384
+ var onToggle = props.onToggle
3385
+ var onPatch = props.onPatch
3386
+ var onPatchEnvPair = props.onPatchEnvPair
3387
+ var onAddEnvPair = props.onAddEnvPair
3388
+ var onRemoveEnvPair = props.onRemoveEnvPair
3389
+ var onSave = props.onSave
3390
+ var onUnmount = props.onUnmount
3391
+
3392
+ var cli = backend.cli || { ok: false, version: null }
3393
+
3394
+ return createElement('div', { className: 'card', key: backend.id },
3395
+ createElement('div', { className: 'card-header' },
3396
+ createElement('button', {
3397
+ type: 'button', className: 'cli-toggle',
3398
+ 'aria-expanded': expanded,
3399
+ 'aria-label': expanded ? '收起明细' : '展开明细',
3400
+ title: expanded ? '收起明细' : '展开明细',
3401
+ onClick: onToggle,
3402
+ }, expanded ? '▾' : '▸'),
3403
+ createElement('span', { className: 'cli-title', onClick: onToggle }, '⌨️ ' + backend.command),
3404
+ createElement('span', { className: 'cli-tags' },
3405
+ createElement('span', { className: 'tag live' }, '已挂载'),
3406
+ createElement('span', { className: 'tag ' + (backend.providerPresent ? 'live' : 'warn') },
3407
+ backend.providerPresent ? '后端在线 ✓' : '后端未注册'),
3408
+ createElement('span', { className: 'tag ' + (cli.ok ? 'model' : 'warn') },
3409
+ cli.ok ? 'PATH ' + cli.version : 'PATH ✗ ' + backend.command),
3410
+ ),
3411
+ createElement('span', { style: { display: 'flex', gap: '5px', flex: 'none' } },
3412
+ createElement('button', { type: 'button', className: 'btn sm primary', disabled: busy, onClick: onSave },
3413
+ busy ? createElement(Spinner, { key: 'spin' }) : null, '保存配置'),
3414
+ createElement(ConfirmButton, { label: '卸载', confirmLabel: '确认卸载?', disabled: busy, onConfirm: onUnmount }),
3415
+ )
3416
+ ),
3417
+ expanded ? createElement('div', { className: 'cli-detail', key: 'detail' },
3418
+ createElement('div', { className: 'form-grid' },
3419
+ createElement('div', { className: 'field' },
3420
+ createElement('span', { className: 'field-label' }, 'command(PATH 命令名或绝对路径)'),
3421
+ createElement('input', {
3422
+ className: 'input', value: draft.command,
3423
+ placeholder: '如 gemini / qwen / C:\\tools\\aider.exe',
3424
+ onChange: function (event) { onPatch({ command: event.target.value }) },
3425
+ }),
3426
+ ),
3427
+ createElement('div', { className: 'field' },
3428
+ createElement('span', { className: 'field-label' }, 'providerName(执行后端注册名)'),
3429
+ createElement('input', {
3430
+ className: 'input', value: draft.providerName,
3431
+ placeholder: '如 cli-gemini(子智能体表单的执行后端选项)',
3432
+ onChange: function (event) { onPatch({ providerName: event.target.value }) },
3433
+ }),
3434
+ ),
3435
+ createElement('div', { className: 'field full' },
3436
+ createElement('span', { className: 'field-label' }, 'args(空格分隔,{prompt} 占位提示词)'),
3437
+ createElement('input', {
3438
+ className: 'input', value: draft.argsText,
3439
+ placeholder: '-p {prompt}',
3440
+ onChange: function (event) { onPatch({ argsText: event.target.value }) },
3441
+ }),
3442
+ createElement('span', { className: 'field-hint' }, 'one-shot 纯文本:stdout 即委托结果,非零退出记为失败;prompt 经 {prompt} 传入'),
3443
+ ),
3444
+ createElement('div', { className: 'field' },
3445
+ createElement('span', { className: 'field-label' }, 'disposeGraceMs(进程树终止宽限,毫秒)'),
3446
+ createElement('input', {
3447
+ className: 'input', value: draft.disposeGraceMs,
3448
+ placeholder: '3000',
3449
+ onChange: function (event) { onPatch({ disposeGraceMs: event.target.value }) },
3450
+ }),
3451
+ ),
3452
+ ),
3453
+ createElement(EnvPairsEditor, {
3454
+ pairs: draft.envPairs,
3455
+ onPatchPair: onPatchEnvPair,
3456
+ onAdd: onAddEnvPair,
3457
+ onRemove: onRemoveEnvPair,
3458
+ }),
3459
+ ) : null,
3460
+ )
3461
+ }
3462
+
3463
+ function CliPanel(props) {
3464
+ var call = props.call
3465
+
3466
+ var viewState = useState({ loading: true, error: null, data: null })
3467
+ var view = viewState[0]
3468
+ var setView = viewState[1]
3469
+
3470
+ var draftsState = useState({})
3471
+ var drafts = draftsState[0]
3472
+ var setDrafts = draftsState[1]
3473
+
3474
+ var busyState = useState({})
3475
+ var busy = busyState[0]
3476
+ var setBusy = busyState[1]
3477
+
3478
+ var customCommandState = useState('')
3479
+ var customCommand = customCommandState[0]
3480
+ var setCustomCommand = customCommandState[1]
3481
+
3482
+ var expandedState = useState({})
3483
+ var expanded = expandedState[0]
3484
+ var setExpanded = expandedState[1]
3485
+
3486
+ var toggleExpanded = function (backendId) {
3487
+ setExpanded(function (prev) {
3488
+ var next = Object.assign({}, prev)
3489
+ next[backendId] = prev[backendId] !== true
3490
+ return next
3491
+ })
3492
+ }
3493
+
3494
+ var toastState = useState(null)
3495
+ var toast = toastState[0]
3496
+ var setToast = toastState[1]
3497
+
3498
+ useEffect(function () {
3499
+ if (!toast) return undefined
3500
+ var timer = setTimeout(function () { setToast(null) }, 4200)
3501
+ return function () { clearTimeout(timer) }
3502
+ }, [toast])
3503
+
3504
+ /** Adopt a detection payload: view data + reset drafts to the served configs. */
3505
+ var absorb = function (result) {
3506
+ setView({ loading: false, error: null, data: result })
3507
+ var next = {}
3508
+ ;(result.backends || []).forEach(function (backend) {
3509
+ next[backend.id] = backend.kind === 'generic'
3510
+ ? cliDraftFromGeneric(backend)
3511
+ : cliDraftFromConfig(backend.config)
3512
+ })
3513
+ setDrafts(next)
3514
+ setBusy({})
3515
+ }
3516
+
3517
+ var reload = function () {
3518
+ setView(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
3519
+ call('subagentAdmin/cliList', {}).then(function (raw) {
3520
+ absorb(unwrap(raw))
3521
+ }).catch(function (error) {
3522
+ setView({ loading: false, error: String((error && error.message) || error), data: null })
3523
+ })
3524
+ }
3525
+ useEffect(reload, [])
3526
+
3527
+ var patchDraft = function (backendId, patch) {
3528
+ setDrafts(function (prev) {
3529
+ var current = prev[backendId] || cliDraftFromConfig(null)
3530
+ var next = Object.assign({}, prev)
3531
+ next[backendId] = Object.assign({}, current, patch)
3532
+ return next
3533
+ })
3534
+ }
3535
+
3536
+ // Env-pair edits derive from the latest queued state (functional update), so
3537
+ // rapid consecutive edits can never overwrite each other via stale drafts.
3538
+ var patchEnvPair = function (backendId, index, patch) {
3539
+ setDrafts(function (prev) {
3540
+ var current = prev[backendId] || cliDraftFromConfig(null)
3541
+ var pairs = (current.envPairs || []).map(function (pair, i) {
3542
+ return i === index ? Object.assign({}, pair, patch) : pair
3543
+ })
3544
+ var next = Object.assign({}, prev)
3545
+ next[backendId] = Object.assign({}, current, { envPairs: pairs })
3546
+ return next
3547
+ })
3548
+ }
3549
+ var addEnvPair = function (backendId) {
3550
+ setDrafts(function (prev) {
3551
+ var current = prev[backendId] || cliDraftFromConfig(null)
3552
+ var next = Object.assign({}, prev)
3553
+ next[backendId] = Object.assign({}, current, { envPairs: (current.envPairs || []).concat([{ key: '', value: '' }]) })
3554
+ return next
3555
+ })
3556
+ }
3557
+ var removeEnvPair = function (backendId, index) {
3558
+ setDrafts(function (prev) {
3559
+ var current = prev[backendId] || cliDraftFromConfig(null)
3560
+ var next = Object.assign({}, prev)
3561
+ next[backendId] = Object.assign({}, current, { envPairs: (current.envPairs || []).filter(function (_, i) { return i !== index }) })
3562
+ return next
3563
+ })
3564
+ }
3565
+
3566
+ var runUpsert = function (backend) {
3567
+ if (busy[backend.id]) return
3568
+ var draft = drafts[backend.id]
3569
+ if (!draft) return
3570
+ var payload
3571
+ if (backend.kind === 'generic') {
3572
+ if (!/^[^\s]+$/.test(draft.command.trim())) {
3573
+ setToast('command 不能包含空格(PATH 命令名或绝对路径)')
3574
+ return
3575
+ }
3576
+ var args = draft.argsText.trim().split(/\s+/).filter(Boolean)
3577
+ if (args.length === 0 || args.length > 20 || !args.every(function (arg) { return arg.length <= 256 })) {
3578
+ setToast('args 必须是 1-20 个非空片段(单条 ≤ 256 字符),用 {prompt} 占位提示词')
3579
+ return
3580
+ }
3581
+ if (args.indexOf('{prompt}') === -1) {
3582
+ setToast('args 必须包含 {prompt} 占位符(提示词将替换该占位符传入 CLI)')
3583
+ return
3584
+ }
3585
+ if (!/^[a-z][a-z0-9_-]{0,47}$/.test(draft.providerName.trim())) {
3586
+ setToast('providerName 必须是 1-48 位小写字母/数字/下划线/中划线且字母开头')
3587
+ return
3588
+ }
3589
+ if (!/^\d+$/.test(draft.disposeGraceMs.trim())) {
3590
+ setToast('disposeGraceMs 必须是非负整数(毫秒)')
3591
+ return
3592
+ }
3593
+ payload = { kind: 'generic', backendId: backend.id, config: cliConfigFromGenericDraft(draft) }
3594
+ } else {
3595
+ if (!/^[a-z][a-z0-9_-]{0,47}$/.test(draft.providerName.trim())) {
3596
+ setToast('providerName 必须是 1-48 位小写字母/数字/下划线/中划线且字母开头')
3597
+ return
3598
+ }
3599
+ if (!/^\d+$/.test(draft.disposeGraceMs.trim())) {
3600
+ setToast('disposeGraceMs 必须是非负整数(毫秒)')
3601
+ return
3602
+ }
3603
+ payload = { backendId: backend.id, config: cliConfigFromDraft(draft) }
3604
+ }
3605
+ var busyPatch = {}
3606
+ busyPatch[backend.id] = true
3607
+ setBusy(Object.assign({}, busy, busyPatch))
3608
+ call('subagentAdmin/cliUpsert', { payload: payload }).then(function (raw) {
3609
+ absorb(unwrap(raw))
3610
+ }).catch(function (error) {
3611
+ setBusy({})
3612
+ setToast('保存失败:' + String((error && error.message) || error))
3613
+ })
3614
+ }
3615
+
3616
+ var runUnmount = function (backend) {
3617
+ if (busy[backend.id]) return
3618
+ var busyPatch = {}
3619
+ busyPatch[backend.id] = true
3620
+ setBusy(Object.assign({}, busy, busyPatch))
3621
+ // Generic backends are recognized server-side by their "cli-" id prefix.
3622
+ call('subagentAdmin/cliRemove', { id: backend.id }).then(function (raw) {
3623
+ absorb(unwrap(raw))
3624
+ }).catch(function (error) {
3625
+ setBusy({})
3626
+ setToast('卸载失败:' + String((error && error.message) || error))
3627
+ })
3628
+ }
3629
+
3630
+ var runInstall = function (backend) {
3631
+ if (busy[backend.id]) return
3632
+ var busyPatch = {}
3633
+ busyPatch[backend.id] = true
3634
+ setBusy(Object.assign({}, busy, busyPatch))
3635
+ call('subagentAdmin/cliInstall', { backendId: backend.id }).then(function (raw) {
3636
+ var result = unwrap(raw)
3637
+ absorb(result)
3638
+ setToast((result && result.output ? result.output : '依赖包安装完成') + ',已重新检测')
3639
+ }).catch(function (error) {
3640
+ var message = String((error && error.message) || error)
3641
+ if (message.indexOf('404') !== -1) {
3642
+ message += '(宿主端未注册该接口:请重启 dsh 加载最新插件后重试)'
3643
+ }
3644
+ setBusy({})
3645
+ setToast('安装失败:' + message)
3646
+ })
3647
+ }
3648
+
3649
+ var runGenericMount = function (command) {
3650
+ if (!/^[^\s]+$/.test(command)) {
3651
+ setToast('command 不能包含空格(PATH 命令名或绝对路径)')
3652
+ return
3653
+ }
3654
+ var busyPatch = { __scan__: true }
3655
+ setBusy(Object.assign({}, busy, busyPatch))
3656
+ call('subagentAdmin/cliUpsert', { payload: { kind: 'generic', config: { command: command } } }).then(function (raw) {
3657
+ absorb(unwrap(raw))
3658
+ setCustomCommand('')
3659
+ }).catch(function (error) {
3660
+ setBusy({})
3661
+ setToast('挂载失败:' + String((error && error.message) || error))
3662
+ })
3663
+ }
3664
+
3665
+ var children = []
3666
+
3667
+ // 本机 CLI card sits on top: scan list + custom command + refresh; the
3668
+ // builtin codex/claude-code cards and mounted generic cards follow below.
3669
+ var backends = (view.data && view.data.backends) || []
3670
+ var others = (view.data && view.data.others) || []
3671
+ var busyAny = Object.keys(busy).some(function (key) { return busy[key] === true })
3672
+ var genericBackends = backends.filter(function (backend) { return backend.kind === 'generic' })
3673
+
3674
+ children.push(createElement('div', { className: 'card', key: 'local-cli' },
3675
+ createElement('div', { className: 'card-header' },
3676
+ createElement('span', { className: 'card-title' }, '🛰️ 本机 CLI'),
3677
+ createElement('span', { className: 'tag' }, '通用命令行后端 · one-shot 纯文本'),
3678
+ createElement('span', { style: { marginLeft: 'auto', display: 'flex' } },
3679
+ createElement('button', { type: 'button', className: 'btn sm', onClick: reload, disabled: view.loading },
3680
+ view.loading ? createElement(Spinner, { key: 'spin' }) : '⟳', '重新检测')
3681
+ )
3682
+ ),
3683
+ createElement('div', { className: 'cli-scan-hint' },
3684
+ '检测并挂载 harness 内置的外部 CLI 后端(codex / claude-code)与本机其他命令行工具;挂载后即可在「子智能体」表单的执行后端下拉中选用。'),
3685
+ createElement('div', { className: 'cli-scan-list' },
3686
+ createElement('div', { className: 'cli-scan-row', key: 'custom' },
3687
+ createElement('input', {
3688
+ className: 'input', value: customCommand,
3689
+ placeholder: '添加自定义 CLI:输入命令名或绝对路径,如 aider(挂载为 one-shot 后端)',
3690
+ onChange: function (event) { setCustomCommand(event.target.value) },
3691
+ }),
3692
+ createElement('button', {
3693
+ type: 'button', className: 'btn sm',
3694
+ disabled: busyAny || customCommand.trim() === '',
3695
+ onClick: function () { runGenericMount(customCommand.trim()) },
3696
+ }, '挂载'),
3697
+ ),
3698
+ (function () {
3699
+ var mountedCommands = new Set(genericBackends.map(function (backend) {
3700
+ return backend.command.toLowerCase()
3701
+ }))
3702
+ return others
3703
+ .filter(function (item) { return !!(item.cli && item.cli.ok) })
3704
+ .filter(function (item) { return !mountedCommands.has(item.name.toLowerCase()) })
3705
+ .map(function (item) {
3706
+ return createElement('div', { className: 'cli-scan-card', key: item.name },
3707
+ createElement('span', { className: 'cli-title' }, '⌨️ ' + item.name),
3708
+ createElement('span', { className: 'cli-tags' },
3709
+ createElement('span', { className: 'tag' }, '未挂载'),
3710
+ createElement('span', { className: 'tag live' }, 'PATH ' + item.cli.version),
3711
+ ),
3712
+ createElement('button', {
3713
+ type: 'button', className: 'btn sm', disabled: busyAny,
3714
+ onClick: function () { runGenericMount(item.name) },
3715
+ }, '挂载'),
3716
+ )
3717
+ })
3718
+ })(),
3719
+ others.length > 0 && others.every(function (item) { return !(item.cli && item.cli.ok) })
3720
+ ? createElement('span', { className: 'tag', key: 'none' }, '未检测到其他 agent CLI')
3721
+ : null,
3722
+ ),
3723
+ ))
3724
+
3725
+ if (view.error) {
3726
+ children.push(createElement('div', { className: 'error-strip', key: 'error' }, '⚠️ 加载失败:', view.error))
3727
+ } else {
3728
+ genericBackends.forEach(function (backend) {
3729
+ children.push(GenericCliCard({
3730
+ backend: backend,
3731
+ draft: drafts[backend.id] || cliDraftFromGeneric(backend),
3732
+ busy: busy[backend.id] === true,
3733
+ expanded: expanded[backend.id] === true,
3734
+ onToggle: function () { toggleExpanded(backend.id) },
3735
+ onPatch: function (patch) { patchDraft(backend.id, patch) },
3736
+ onPatchEnvPair: function (index, patch) { patchEnvPair(backend.id, index, patch) },
3737
+ onAddEnvPair: function () { addEnvPair(backend.id) },
3738
+ onRemoveEnvPair: function (index) { removeEnvPair(backend.id, index) },
3739
+ onSave: function () { runUpsert(backend) },
3740
+ onUnmount: function () { runUnmount(backend) },
3741
+ }))
3742
+ })
3743
+ backends.filter(function (backend) { return backend.kind !== 'generic' }).forEach(function (backend) {
3744
+ children.push(CliBackendCard({
3745
+ backend: backend,
3746
+ draft: drafts[backend.id] || cliDraftFromConfig(backend.config),
3747
+ busy: busy[backend.id] === true,
3748
+ expanded: expanded[backend.id] === true,
3749
+ onToggle: function () { toggleExpanded(backend.id) },
3750
+ onPatch: function (patch) { patchDraft(backend.id, patch) },
3751
+ onPatchEnvPair: function (index, patch) { patchEnvPair(backend.id, index, patch) },
3752
+ onAddEnvPair: function () { addEnvPair(backend.id) },
3753
+ onRemoveEnvPair: function (index) { removeEnvPair(backend.id, index) },
3754
+ onSave: function () { runUpsert(backend) },
3755
+ onInstall: function () { runInstall(backend) },
3756
+ onUnmount: function () { runUnmount(backend) },
3757
+ }))
3758
+ })
3759
+ }
3760
+
3761
+ return createElement('div', null,
3762
+ toast ? createElement('div', { className: 'toast' }, toast) : null,
3763
+ children
3764
+ )
3765
+ }
3766
+
3767
+ /* ========================================================================== */
3768
+ /* Section entrypoint */
3769
+ /* ========================================================================== */
3770
+
3771
+ function SubagentAdminSection(props) {
3772
+ var tabState = useState('subagents')
3773
+ var activeTab = tabState[0]
3774
+ var setActiveTab = tabState[1]
3775
+ var tabs = [
3776
+ { id: 'subagents', label: '子智能体', component: SubagentsPanel },
3777
+ { id: 'cli', label: 'CLI 后端', component: CliPanel },
3778
+ { id: 'history', label: '变更记录', component: HistoryPanel },
3779
+ ]
3780
+ var selected = tabs.find(function (tab) { return tab.id === activeTab }) || tabs[0]
3781
+
3782
+ return createElement('div', { 'data-dsh-sa-section': '' },
3783
+ createElement('div', { className: 'tabs', role: 'tablist', 'aria-label': '子智能体管理' },
3784
+ tabs.map(function (tab) {
3785
+ return createElement('button', {
3786
+ type: 'button', key: tab.id,
3787
+ className: 'tab' + (tab.id === selected.id ? ' active' : ''),
3788
+ role: 'tab', 'aria-selected': tab.id === selected.id,
3789
+ onClick: function () { setActiveTab(tab.id) },
3790
+ }, tab.label)
3791
+ })
3792
+ ),
3793
+ createElement(selected.component, { key: selected.id, call: props.call })
3794
+ )
3795
+ }
3796
+
2297
3797
  /* ========================================================================== */
2298
3798
  /* Plugin Entrypoint */
2299
3799
  /* ========================================================================== */
2300
3800
 
2301
3801
  function apply(ctx) {
2302
3802
  injectStyles()
3803
+ injectSaStyles()
2303
3804
 
2304
3805
  var call = function (method, args) {
2305
3806
  return ctx.connection.rpc.call('/api', method, { args: args })
@@ -2320,16 +3821,47 @@ function apply(ctx) {
2320
3821
  } catch (e) { refreshSessions = null }
2321
3822
  var disposeSidebar = ctx.effect(function () { return setupMenuInjection(call, refreshSessions) })
2322
3823
 
2323
- // One management-center entry keeps related local administration together;
2324
- // its tab container mounts only the selected panel.
3824
+ // Four surfaces, each parked where the shell already renders its domain.
3825
+ // 插件管理 joins the shell-owned 插件 section as its third tab (插件配置 0,
3826
+ // 插件列表 10, 扩展插件 20); MCP / 子智能体 / 历史会话 become standalone
3827
+ // settings pages (MCP服务器 takes the 25 slot right after Agent 预设,
3828
+ // 子智能体 follows at 26, 历史会话 sorts last). inject() waits on each
3829
+ // slot's declaration, so registration order never matters.
3830
+ ctx.slots.inject('settings.plugins.tab', function () {
3831
+ return ctx.slots.register({
3832
+ name: 'settings.plugins.tab',
3833
+ id: 'extensions',
3834
+ order: 20,
3835
+ label: '扩展插件',
3836
+ inject: function () { return { call: call } },
3837
+ }, PluginsSection)
3838
+ })
2325
3839
  ctx.slots.inject('settings.section', function () {
2326
3840
  return ctx.slots.register({
2327
3841
  name: 'settings.section',
2328
- id: 'plugin-admin',
3842
+ id: 'mcp-servers',
2329
3843
  order: 25,
2330
- label: '管理中心',
3844
+ label: 'MCP服务器',
3845
+ inject: function () { return { call: call } },
3846
+ }, McpSection)
3847
+ })
3848
+ ctx.slots.inject('settings.section', function () {
3849
+ return ctx.slots.register({
3850
+ name: 'settings.section',
3851
+ id: 'session-history',
3852
+ order: 100,
3853
+ label: '历史会话',
2331
3854
  inject: function () { return { call: call, refreshSessions: refreshSessions } },
2332
- }, AdminCenterSection)
3855
+ }, SessionsSection)
3856
+ })
3857
+ ctx.slots.inject('settings.section', function () {
3858
+ return ctx.slots.register({
3859
+ name: 'settings.section',
3860
+ id: 'subagent-admin',
3861
+ order: 26,
3862
+ label: '子智能体',
3863
+ inject: function () { return { call: call } },
3864
+ }, SubagentAdminSection)
2333
3865
  })
2334
3866
  }
2335
3867