@xcanwin/manyoyo 5.7.14 → 5.8.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.
@@ -50,14 +50,17 @@
50
50
  mobileActionsOpen: false,
51
51
  configModalOpen: false,
52
52
  createModalOpen: false,
53
+ agentTemplateModalOpen: false,
53
54
  configLoading: false,
54
55
  configSaving: false,
55
56
  createLoading: false,
56
57
  createSubmitting: false,
58
+ agentTemplateSaving: false,
57
59
  configSnapshot: null,
58
60
  sessionDetail: null,
59
61
  sessionDetailError: '',
60
62
  sessionDetailRequestId: 0,
63
+ agentTemplateError: '',
61
64
  createAgentPromptAuto: false,
62
65
  createContainerPathBase: '',
63
66
  createDefaults: null,
@@ -167,6 +170,8 @@
167
170
  const activeMeta = document.getElementById('activeMeta');
168
171
  const activityCommandBtn = document.getElementById('activityCommandBtn');
169
172
  const activityAgentBtn = document.getElementById('activityAgentBtn');
173
+ const agentTemplateBtn = document.getElementById('agentTemplateBtn');
174
+ const activityModelChip = document.getElementById('activityModelChip');
170
175
  const messagesNode = document.getElementById('messages');
171
176
  const terminalPanel = document.getElementById('terminalPanel');
172
177
  const detailPanel = document.getElementById('detailPanel');
@@ -182,6 +187,15 @@
182
187
  const sendState = document.getElementById('sendState');
183
188
  const sendBtn = document.getElementById('sendBtn');
184
189
  const stopBtn = document.getElementById('stopBtn');
190
+ const agentTemplateModal = document.getElementById('agentTemplateModal');
191
+ const agentTemplateTip = document.getElementById('agentTemplateTip');
192
+ const containerAgentPromptEditor = document.getElementById('containerAgentPromptEditor');
193
+ const agentTemplateOverrideGroup = document.getElementById('agentTemplateOverrideGroup');
194
+ const agentPromptOverrideEditor = document.getElementById('agentPromptOverrideEditor');
195
+ const agentTemplateError = document.getElementById('agentTemplateError');
196
+ const agentTemplateCancelBtn = document.getElementById('agentTemplateCancelBtn');
197
+ const agentTemplateResetBtn = document.getElementById('agentTemplateResetBtn');
198
+ const agentTemplateSaveBtn = document.getElementById('agentTemplateSaveBtn');
185
199
  const refreshBtn = document.getElementById('refreshBtn');
186
200
  const removeBtn = document.getElementById('removeBtn');
187
201
  const removeAllBtn = document.getElementById('removeAllBtn');
@@ -192,6 +206,8 @@
192
206
  const TERMINAL_MIN_ROWS = 12;
193
207
  const TERMINAL_DEFAULT_COLS = 120;
194
208
  const TERMINAL_DEFAULT_ROWS = 36;
209
+ const WEB_SESSION_KEY_SEPARATOR = '~';
210
+ const WEB_DEFAULT_AGENT_ID = 'default';
195
211
  const YOLO_COMMAND_MAP = {
196
212
  claude: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
197
213
  cc: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
@@ -1055,6 +1071,133 @@
1055
1071
  }) || null;
1056
1072
  }
1057
1073
 
1074
+ function parseSessionKey(sessionName) {
1075
+ const raw = String(sessionName || '').trim();
1076
+ if (!raw) {
1077
+ return {
1078
+ containerName: '',
1079
+ agentId: WEB_DEFAULT_AGENT_ID
1080
+ };
1081
+ }
1082
+ const separatorIndex = raw.indexOf(WEB_SESSION_KEY_SEPARATOR);
1083
+ if (separatorIndex === -1) {
1084
+ return {
1085
+ containerName: raw,
1086
+ agentId: WEB_DEFAULT_AGENT_ID
1087
+ };
1088
+ }
1089
+ return {
1090
+ containerName: raw.slice(0, separatorIndex),
1091
+ agentId: raw.slice(separatorIndex + 1) || WEB_DEFAULT_AGENT_ID
1092
+ };
1093
+ }
1094
+
1095
+ function isActiveAgentOverrideEditable() {
1096
+ const sessionRef = parseSessionKey(state.active);
1097
+ return Boolean(state.active && sessionRef.agentId && sessionRef.agentId !== WEB_DEFAULT_AGENT_ID);
1098
+ }
1099
+
1100
+ function showAgentTemplateError(message) {
1101
+ state.agentTemplateError = String(message || '').trim();
1102
+ if (!agentTemplateError) {
1103
+ return;
1104
+ }
1105
+ agentTemplateError.hidden = !state.agentTemplateError;
1106
+ agentTemplateError.textContent = state.agentTemplateError;
1107
+ }
1108
+
1109
+ function fillAgentTemplateForm(detail) {
1110
+ const currentDetail = detail && typeof detail === 'object' ? detail : {};
1111
+ if (containerAgentPromptEditor) {
1112
+ containerAgentPromptEditor.value = currentDetail.containerAgentPromptCommand || '';
1113
+ }
1114
+ if (agentPromptOverrideEditor) {
1115
+ agentPromptOverrideEditor.value = currentDetail.agentPromptCommandOverride || '';
1116
+ }
1117
+ if (agentTemplateOverrideGroup) {
1118
+ agentTemplateOverrideGroup.hidden = !isActiveAgentOverrideEditable();
1119
+ }
1120
+ if (agentTemplateTip) {
1121
+ const sessionName = currentDetail.agentName || state.active || '当前会话';
1122
+ const sourceMap = {
1123
+ agent: '当前 AGENT 覆盖',
1124
+ container: '容器默认模板',
1125
+ inferred: '从容器启动命令推导',
1126
+ none: '未配置'
1127
+ };
1128
+ const sourceLabel = sourceMap[currentDetail.agentPromptSource] || '未配置';
1129
+ const effectiveTemplate = currentDetail.agentPromptCommand || '—';
1130
+ agentTemplateTip.textContent = `${sessionName} · 当前生效来源:${sourceLabel} · 生效模板:${effectiveTemplate}`;
1131
+ }
1132
+ showAgentTemplateError('');
1133
+ }
1134
+
1135
+ async function ensureActiveSessionDetail() {
1136
+ if (!state.active) {
1137
+ return null;
1138
+ }
1139
+ if (state.sessionDetail && state.active === (state.sessionDetail.name || state.active)) {
1140
+ return state.sessionDetail;
1141
+ }
1142
+ await loadSessionDetailForSession(state.active);
1143
+ return state.sessionDetail;
1144
+ }
1145
+
1146
+ async function openAgentTemplateModal() {
1147
+ if (!state.active || state.agentTemplateSaving) {
1148
+ return;
1149
+ }
1150
+ const detail = await ensureActiveSessionDetail();
1151
+ if (!detail) {
1152
+ alert(state.sessionDetailError || '当前会话详情暂时不可用');
1153
+ return;
1154
+ }
1155
+ state.agentTemplateModalOpen = true;
1156
+ fillAgentTemplateForm(detail);
1157
+ syncUi();
1158
+ if (containerAgentPromptEditor) {
1159
+ containerAgentPromptEditor.focus();
1160
+ }
1161
+ }
1162
+
1163
+ function closeAgentTemplateModal() {
1164
+ state.agentTemplateModalOpen = false;
1165
+ showAgentTemplateError('');
1166
+ }
1167
+
1168
+ function resetAgentTemplateModal() {
1169
+ fillAgentTemplateForm(state.sessionDetail || {});
1170
+ }
1171
+
1172
+ async function saveAgentTemplateModal() {
1173
+ if (!state.active || state.agentTemplateSaving) {
1174
+ return;
1175
+ }
1176
+ state.agentTemplateSaving = true;
1177
+ showAgentTemplateError('');
1178
+ syncUi();
1179
+ try {
1180
+ const payload = {
1181
+ containerAgentPromptCommand: containerAgentPromptEditor ? String(containerAgentPromptEditor.value || '').trim() : ''
1182
+ };
1183
+ if (isActiveAgentOverrideEditable() && agentPromptOverrideEditor) {
1184
+ payload.agentPromptCommandOverride = String(agentPromptOverrideEditor.value || '').trim();
1185
+ }
1186
+ const data = await api('/api/sessions/' + encodeURIComponent(state.active) + '/agent-template', {
1187
+ method: 'PUT',
1188
+ body: JSON.stringify(payload)
1189
+ });
1190
+ state.sessionDetail = data && data.detail ? data.detail : state.sessionDetail;
1191
+ await refreshSessionsSilent({ preferredName: state.active });
1192
+ closeAgentTemplateModal();
1193
+ } catch (e) {
1194
+ showAgentTemplateError(e && e.message ? e.message : '保存失败');
1195
+ } finally {
1196
+ state.agentTemplateSaving = false;
1197
+ syncUi();
1198
+ }
1199
+ }
1200
+
1058
1201
  function isAgentRunActiveForSession(sessionName) {
1059
1202
  return Boolean(
1060
1203
  state.agentRun
@@ -1078,6 +1221,24 @@
1078
1221
  return Boolean(active && active.agentEnabled);
1079
1222
  }
1080
1223
 
1224
+ function resolveToolbarCliLabel() {
1225
+ const activeSession = getActiveSession();
1226
+ const detail = state.sessionDetail && state.active ? state.sessionDetail : null;
1227
+ const agentProgram = String(
1228
+ (detail && detail.agentProgram)
1229
+ || (activeSession && activeSession.agentProgram)
1230
+ || ''
1231
+ ).trim();
1232
+ return agentProgram || '未配置';
1233
+ }
1234
+
1235
+ function resolveToolbarModelLabel() {
1236
+ if (!state.active) {
1237
+ return '—';
1238
+ }
1239
+ return '自动';
1240
+ }
1241
+
1081
1242
  function buildActiveMeta(session) {
1082
1243
  if (!session) {
1083
1244
  return '会话不可用';
@@ -1566,6 +1727,13 @@
1566
1727
  let resumeStatusDetail = detail.resumeSupported
1567
1728
  ? '支持 resume,但当前会话还没有最近一次执行记录。'
1568
1729
  : '当前 Agent 程序或模板不支持 resume。';
1730
+ const templateSourceMap = {
1731
+ agent: '当前 AGENT 覆盖',
1732
+ container: '容器默认模板',
1733
+ inferred: '从启动命令推导',
1734
+ none: '未配置'
1735
+ };
1736
+ const templateSourceLabel = templateSourceMap[detail.agentPromptSource] || '未配置';
1569
1737
  if (detail.lastResumeOk === true) {
1570
1738
  resumeStatusValue = '最近成功';
1571
1739
  resumeStatusTone = 'ok';
@@ -1596,6 +1764,7 @@
1596
1764
  commandEntries.push({ label: '启动命令', value: applied.defaultCommand || '—' });
1597
1765
  }
1598
1766
  commandEntries.push({ label: 'Agent 模板', value: detail.agentPromptCommand || '—' });
1767
+ commandEntries.push({ label: '模板来源', value: templateSourceLabel });
1599
1768
  commandEntries.push({ label: 'yolo', value: applied.yolo || '—' });
1600
1769
 
1601
1770
  if (detailSummary) {
@@ -1611,6 +1780,7 @@
1611
1780
  renderKeyValueCard(detailSummary, 'Agent 运行', [
1612
1781
  { label: '已启用', value: detail.agentEnabled ? '是' : '否', tone: detail.agentEnabled ? 'ok' : 'warn' },
1613
1782
  { label: '程序', value: detail.agentProgram || '—' },
1783
+ { label: '模板来源', value: templateSourceLabel },
1614
1784
  { label: '支持 resume', value: detail.resumeSupported ? '是' : '否', tone: detail.resumeSupported ? 'ok' : 'warn' },
1615
1785
  { label: '最近 resume', value: lastResumeText },
1616
1786
  { label: '最近结果', value: detail.lastResumeOk == null ? '暂无' : (detail.lastResumeOk ? '成功' : '失败'), tone: detail.lastResumeOk == null ? 'info' : (detail.lastResumeOk ? 'ok' : 'danger') }
@@ -1728,6 +1898,18 @@
1728
1898
  activityAgentBtn.classList.toggle('is-active', agentMode);
1729
1899
  activityAgentBtn.setAttribute('aria-pressed', agentMode ? 'true' : 'false');
1730
1900
  }
1901
+ if (agentTemplateBtn) {
1902
+ agentTemplateBtn.textContent = `CLI · ${resolveToolbarCliLabel()}`;
1903
+ agentTemplateBtn.title = state.active
1904
+ ? '查看或修改当前会话的 CLI 模板'
1905
+ : '请先选择会话';
1906
+ }
1907
+ if (activityModelChip) {
1908
+ activityModelChip.textContent = `模型 · ${resolveToolbarModelLabel()}`;
1909
+ activityModelChip.title = state.active
1910
+ ? '当前版本暂不单独配置模型,默认跟随 CLI 或容器内配置'
1911
+ : '请先选择会话';
1912
+ }
1731
1913
  if (viewActivityBtn) viewActivityBtn.classList.toggle('is-active', activityTab);
1732
1914
  if (viewTerminalBtn) viewTerminalBtn.classList.toggle('is-active', terminalTab);
1733
1915
  if (viewDetailBtn) viewDetailBtn.classList.toggle('is-active', detailTab);
@@ -1764,6 +1946,9 @@
1764
1946
  if (stopBtn) {
1765
1947
  stopBtn.disabled = !activityTab || !agentMode || !activeAgentRunning || state.agentRun.stopping;
1766
1948
  }
1949
+ if (agentTemplateBtn) {
1950
+ agentTemplateBtn.disabled = !state.active || state.agentTemplateSaving;
1951
+ }
1767
1952
  commandInput.disabled = !activityTab || !state.active || (agentMode && !agentEnabled);
1768
1953
  if (commandInput) {
1769
1954
  commandInput.placeholder = agentMode
@@ -1823,9 +2008,21 @@
1823
2008
  if (directoryPickerModal) {
1824
2009
  directoryPickerModal.hidden = !state.directoryPicker.open;
1825
2010
  }
2011
+ if (agentTemplateModal) {
2012
+ agentTemplateModal.hidden = !state.agentTemplateModalOpen;
2013
+ }
2014
+ if (agentTemplateSaveBtn) {
2015
+ agentTemplateSaveBtn.disabled = state.agentTemplateSaving || !state.active;
2016
+ }
2017
+ if (agentTemplateResetBtn) {
2018
+ agentTemplateResetBtn.disabled = state.agentTemplateSaving;
2019
+ }
2020
+ if (agentTemplateCancelBtn) {
2021
+ agentTemplateCancelBtn.disabled = state.agentTemplateSaving;
2022
+ }
1826
2023
  document.body.classList.toggle(
1827
2024
  'modal-open',
1828
- state.configModalOpen || state.createModalOpen || state.directoryPicker.open
2025
+ state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen
1829
2026
  );
1830
2027
  if (!state.active) {
1831
2028
  sendState.textContent = '未选择会话';
@@ -2222,7 +2419,7 @@
2222
2419
  }
2223
2420
  }
2224
2421
  }
2225
- renderSessions();
2422
+ updateSidebarActiveSelection();
2226
2423
  syncUi();
2227
2424
  Promise.all([
2228
2425
  loadMessagesForSession(sessionName),
@@ -2310,173 +2507,244 @@
2310
2507
  return (parts || []).filter(Boolean).join(' · ');
2311
2508
  }
2312
2509
 
2313
- function createTreeToggle(options) {
2314
- const opts = options && typeof options === 'object' ? options : {};
2510
+ function createDisclosureButton(expanded, label) {
2315
2511
  const button = document.createElement('button');
2316
2512
  button.type = 'button';
2317
- button.className = `tree-toggle ${opts.className || ''}`.trim();
2318
- button.setAttribute('aria-expanded', opts.expanded ? 'true' : 'false');
2319
-
2320
- const main = document.createElement('div');
2321
- main.className = 'tree-toggle-main';
2322
-
2323
- const kicker = document.createElement('div');
2324
- kicker.className = opts.kickerClassName || 'workbench-group-kicker';
2325
- kicker.textContent = opts.kicker || '';
2326
- main.appendChild(kicker);
2327
-
2328
- const title = document.createElement('div');
2329
- title.className = opts.titleClassName || 'workbench-group-title';
2330
- title.textContent = opts.title || '';
2331
- title.title = opts.title || '';
2332
- main.appendChild(title);
2333
-
2334
- const metaText = createTreeMetaText(opts.metaParts);
2335
- if (metaText) {
2336
- const meta = document.createElement('div');
2337
- meta.className = 'tree-toggle-meta';
2338
- meta.textContent = metaText;
2339
- main.appendChild(meta);
2340
- }
2341
-
2342
- const caret = document.createElement('span');
2343
- caret.className = 'tree-toggle-caret';
2344
- caret.setAttribute('aria-hidden', 'true');
2345
- caret.textContent = '›';
2346
-
2347
- button.appendChild(main);
2348
- button.appendChild(caret);
2349
-
2350
- if (typeof opts.onClick === 'function') {
2351
- button.addEventListener('click', opts.onClick);
2352
- }
2353
-
2513
+ button.className = 'disclosure-toggle';
2514
+ button.dataset.disclosureLabel = label || '';
2515
+ button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
2516
+ button.setAttribute('aria-label', `${expanded ? '折叠' : '展开'}${label ? ` ${label}` : ''}`);
2517
+ button.innerHTML = '<svg viewBox="0 0 12 12" aria-hidden="true" focusable="false"><path d="M4 2.5L8 6L4 9.5"></path></svg>';
2354
2518
  return button;
2355
2519
  }
2356
2520
 
2357
- function createSidebarIcon(name) {
2358
- const icon = document.createElement('span');
2359
- icon.className = `sidebar-icon ${name ? `sidebar-icon-${name}` : ''}`.trim();
2360
- icon.setAttribute('aria-hidden', 'true');
2521
+ function createTreePrefixSegment() {
2522
+ const segment = document.createElement('span');
2523
+ segment.className = 'tree-prefix-segment';
2524
+ segment.setAttribute('aria-hidden', 'true');
2525
+ return segment;
2526
+ }
2361
2527
 
2362
- if (name === 'cube') {
2363
- icon.innerHTML = [
2364
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">',
2365
- '<path d="M12 3.5 4.75 7.5 12 11.5l7.25-4-7.25-4Z"></path>',
2366
- '<path d="M4.75 7.5v9L12 20.5l7.25-4v-9"></path>',
2367
- '<path d="M12 11.5v9"></path>',
2368
- '</svg>'
2369
- ].join('');
2528
+ function createTreePrefix(ancestorHasNext, isLastSibling, options) {
2529
+ const opts = options && typeof options === 'object' ? options : {};
2530
+ const prefix = document.createElement('div');
2531
+ prefix.className = 'tree-prefix';
2532
+
2533
+ (Array.isArray(ancestorHasNext) ? ancestorHasNext : []).forEach(function () {
2534
+ prefix.appendChild(createTreePrefixSegment());
2535
+ });
2536
+
2537
+ const branch = document.createElement('span');
2538
+ branch.className = `tree-prefix-branch ${isLastSibling ? 'is-last' : 'is-mid'}`;
2539
+ branch.setAttribute('aria-hidden', 'true');
2540
+ prefix.appendChild(branch);
2541
+
2542
+ let disclosure = null;
2543
+ let disclosureWrap = null;
2544
+ if (opts.expandable) {
2545
+ disclosure = createDisclosureButton(!!opts.expanded, opts.label || '');
2546
+ disclosureWrap = document.createElement('span');
2547
+ disclosureWrap.className = 'tree-prefix-toggle';
2548
+ disclosureWrap.appendChild(disclosure);
2549
+ prefix.appendChild(disclosureWrap);
2550
+ } else {
2551
+ const leaf = document.createElement('span');
2552
+ leaf.className = 'tree-prefix-leaf';
2553
+ leaf.setAttribute('aria-hidden', 'true');
2554
+ prefix.appendChild(leaf);
2370
2555
  }
2371
2556
 
2372
- return icon;
2557
+ return { root: prefix, disclosure, disclosureWrap };
2373
2558
  }
2374
2559
 
2375
- function createSidebarBadge(text, className) {
2376
- const badge = document.createElement('span');
2377
- badge.className = `sidebar-badge ${className || ''}`.trim();
2378
- badge.textContent = text || '';
2379
- return badge;
2560
+ function setDisclosureExpanded(control, expanded) {
2561
+ if (!control) {
2562
+ return;
2563
+ }
2564
+ const nextValue = expanded ? 'true' : 'false';
2565
+ if (control.button) {
2566
+ control.button.setAttribute('aria-expanded', nextValue);
2567
+ }
2568
+ if (control.disclosure) {
2569
+ control.disclosure.setAttribute('aria-expanded', nextValue);
2570
+ const label = control.disclosure.dataset.disclosureLabel || '';
2571
+ control.disclosure.setAttribute('aria-label', `${expanded ? '折叠' : '展开'}${label ? ` ${label}` : ''}`);
2572
+ }
2380
2573
  }
2381
2574
 
2382
- function createContainerToggle(containerGroup, expanded) {
2383
- const status = sessionStatusInfo(containerGroup && containerGroup.status);
2575
+ function createTreeItem(options) {
2576
+ const opts = options && typeof options === 'object' ? options : {};
2577
+ const row = document.createElement('div');
2578
+ row.className = `tree-node-row tree-node-row-${opts.kind || 'item'} ${opts.rowClassName || ''}`.trim();
2579
+ row.classList.toggle('has-active', !!opts.hasActive);
2580
+ row.setAttribute('role', 'none');
2581
+
2582
+ const prefixControl = createTreePrefix(opts.ancestorHasNext, !!opts.isLastSibling, {
2583
+ expandable: !!opts.expandable,
2584
+ expanded: !!opts.expanded,
2585
+ label: opts.disclosureLabel || opts.title || ''
2586
+ });
2587
+ row.appendChild(prefixControl.root);
2588
+
2384
2589
  const button = document.createElement('button');
2385
2590
  button.type = 'button';
2386
- button.className = 'tree-toggle container-toggle';
2387
- button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
2591
+ button.className = `tree-node-button tree-node-button-${opts.kind || 'item'} ${opts.className || ''}`.trim();
2592
+ button.setAttribute('role', 'treeitem');
2593
+ button.setAttribute('aria-level', String(opts.level || 1));
2594
+ if (opts.expandable) {
2595
+ button.setAttribute('aria-expanded', opts.expanded ? 'true' : 'false');
2596
+ }
2597
+ if (opts.kind === 'agent') {
2598
+ button.setAttribute('aria-selected', opts.active ? 'true' : 'false');
2599
+ }
2600
+ button.classList.toggle('active', !!opts.active);
2388
2601
 
2389
2602
  const main = document.createElement('div');
2390
- main.className = 'container-toggle-main';
2391
-
2392
- const titleRow = document.createElement('div');
2393
- titleRow.className = 'container-title-row';
2394
- titleRow.appendChild(createSidebarIcon('cube'));
2395
-
2396
- const titleStack = document.createElement('div');
2397
- titleStack.className = 'container-title-stack';
2398
-
2399
- const kicker = document.createElement('div');
2400
- kicker.className = 'container-card-kicker';
2401
- kicker.textContent = '容器';
2603
+ main.className = 'tree-node-main';
2402
2604
 
2403
2605
  const title = document.createElement('div');
2404
- title.className = 'container-card-title';
2405
- title.textContent = containerGroup && containerGroup.containerName ? containerGroup.containerName : '';
2406
- title.title = title.textContent;
2407
-
2408
- titleStack.appendChild(kicker);
2409
- titleStack.appendChild(title);
2410
- titleRow.appendChild(titleStack);
2411
- main.appendChild(titleRow);
2606
+ title.className = opts.titleClassName || 'tree-node-title';
2607
+ title.textContent = opts.title || '';
2608
+ title.title = opts.title || '';
2609
+ main.appendChild(title);
2412
2610
 
2413
- const meta = document.createElement('div');
2414
- meta.className = 'container-card-meta';
2415
- meta.appendChild(createSidebarBadge(status.label, `session-status ${status.tone} container-status-pill`));
2416
- meta.appendChild(createSidebarBadge(`${containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions.length : 0} 个 AGENT`, 'container-agent-badge'));
2417
- main.appendChild(meta);
2418
-
2419
- const infoText = createTreeMetaText([
2420
- containerGroup && containerGroup.image ? containerGroup.image : '',
2421
- formatDateTime(containerGroup && containerGroup.updatedAt) || '暂无更新'
2422
- ]);
2423
- if (infoText) {
2424
- const info = document.createElement('div');
2425
- info.className = 'container-card-info';
2426
- info.textContent = infoText;
2427
- info.title = infoText;
2428
- main.appendChild(info);
2429
- }
2430
-
2431
- const caret = document.createElement('span');
2432
- caret.className = 'tree-toggle-caret';
2433
- caret.setAttribute('aria-hidden', 'true');
2434
- caret.textContent = '›';
2611
+ if (opts.meta) {
2612
+ const meta = document.createElement('div');
2613
+ meta.className = `tree-node-meta ${opts.metaClassName || ''}`.trim();
2614
+ meta.textContent = opts.meta;
2615
+ main.appendChild(meta);
2616
+ }
2435
2617
 
2436
2618
  button.appendChild(main);
2437
- button.appendChild(caret);
2619
+ row.appendChild(button);
2620
+ if (opts.overlayNode) {
2621
+ row.appendChild(opts.overlayNode);
2622
+ }
2438
2623
 
2439
- return button;
2624
+ const control = {
2625
+ root: row,
2626
+ button,
2627
+ disclosure: prefixControl.disclosure
2628
+ };
2629
+ setDisclosureExpanded(control, !!opts.expanded);
2630
+ return control;
2440
2631
  }
2441
2632
 
2442
- function createAgentRow(session, index) {
2443
- const status = sessionStatusInfo(session.status);
2444
- const btn = document.createElement('button');
2445
- btn.type = 'button';
2446
- btn.className = 'agent-item';
2447
- btn.dataset.sessionName = session.name;
2448
- btn.style.setProperty('--item-index', String(index));
2449
- btn.classList.toggle('active', state.active === session.name);
2450
-
2451
- const sessionName = document.createElement('div');
2452
- sessionName.className = 'agent-name';
2453
- sessionName.textContent = session.agentName || session.name;
2454
-
2455
- const meta = document.createElement('div');
2456
- meta.className = 'agent-meta';
2633
+ function buildSessionTreeNodes(grouped) {
2634
+ return (Array.isArray(grouped) ? grouped : []).map(function (directoryGroup) {
2635
+ return {
2636
+ kind: 'directory',
2637
+ title: directoryGroup.path,
2638
+ disclosureLabel: directoryGroup.path,
2639
+ expanded: isDirectoryExpanded(directoryGroup),
2640
+ hasActive: directoryContainsActiveSession(directoryGroup),
2641
+ onToggle: function (expanded) {
2642
+ setSidebarDirectoryExpanded(directoryGroup.path, expanded);
2643
+ },
2644
+ children: (Array.isArray(directoryGroup.containers) ? directoryGroup.containers : []).map(function (containerGroup) {
2645
+ const status = sessionStatusInfo(containerGroup && containerGroup.status);
2646
+ const historyClassName = status.tone === 'history' ? 'tree-node-tone-history' : '';
2647
+ const containerName = String(containerGroup && containerGroup.containerName ? containerGroup.containerName : '').trim() || '未命名容器';
2648
+ const hoverMenu = document.createElement('div');
2649
+ hoverMenu.className = 'tree-node-hover-menu';
2650
+
2651
+ const addAgentBtn = document.createElement('button');
2652
+ addAgentBtn.type = 'button';
2653
+ addAgentBtn.className = 'secondary tree-node-menu-item';
2654
+ addAgentBtn.textContent = '新建AGENT';
2655
+ addAgentBtn.addEventListener('click', function (event) {
2656
+ event.stopPropagation();
2657
+ createAgentSession(containerName);
2658
+ });
2659
+ hoverMenu.appendChild(addAgentBtn);
2660
+
2661
+ return {
2662
+ kind: 'container',
2663
+ title: containerName,
2664
+ meta: status.label,
2665
+ metaClassName: `tree-node-status ${status.tone}`,
2666
+ className: historyClassName,
2667
+ disclosureLabel: containerName,
2668
+ expanded: isContainerExpanded(containerGroup),
2669
+ hasActive: containerContainsActiveSession(containerGroup),
2670
+ overlayNode: hoverMenu,
2671
+ onToggle: function (expanded) {
2672
+ setSidebarContainerExpanded(containerGroup.containerName, expanded);
2673
+ },
2674
+ children: (Array.isArray(containerGroup.sessions) ? containerGroup.sessions : []).map(function (session) {
2675
+ return {
2676
+ kind: 'agent',
2677
+ title: session.agentName || session.name,
2678
+ meta: formatDateTime(session.updatedAt) || '暂无更新',
2679
+ className: historyClassName,
2680
+ active: state.active === session.name,
2681
+ sessionName: session.name
2682
+ };
2683
+ })
2684
+ };
2685
+ })
2686
+ };
2687
+ });
2688
+ }
2457
2689
 
2458
- const statusBadge = document.createElement('span');
2459
- statusBadge.className = `session-status ${status.tone}`;
2460
- statusBadge.textContent = status.label;
2690
+ function renderSessionTreeNodes(nodes, parentNode, ancestorHasNext, itemCounter) {
2691
+ (Array.isArray(nodes) ? nodes : []).forEach(function (node, index) {
2692
+ const isLastSibling = index === nodes.length - 1;
2693
+ const item = createTreeItem({
2694
+ kind: node.kind,
2695
+ title: node.title,
2696
+ meta: node.meta,
2697
+ className: node.className,
2698
+ metaClassName: node.metaClassName,
2699
+ disclosureLabel: node.disclosureLabel,
2700
+ expandable: Array.isArray(node.children) && node.children.length > 0,
2701
+ expanded: !!node.expanded,
2702
+ active: !!node.active,
2703
+ hasActive: !!node.hasActive,
2704
+ level: ancestorHasNext.length + 1,
2705
+ ancestorHasNext: ancestorHasNext,
2706
+ isLastSibling: isLastSibling,
2707
+ overlayNode: node.overlayNode
2708
+ });
2461
2709
 
2462
- const messageCount = document.createElement('span');
2463
- messageCount.className = 'session-count';
2464
- messageCount.textContent = `${safeMessageCount(session.messageCount)} 条`;
2710
+ if (node.kind === 'agent') {
2711
+ item.button.dataset.sessionName = node.sessionName || '';
2712
+ item.button.style.setProperty('--item-index', String(itemCounter.value));
2713
+ itemCounter.value += 1;
2714
+ state.sessionNodeMap.set(node.sessionName, item.button);
2715
+ item.button.addEventListener('click', function () {
2716
+ handleSessionItemClick(node.sessionName || '');
2717
+ });
2718
+ parentNode.appendChild(item.root);
2719
+ return;
2720
+ }
2465
2721
 
2466
- meta.appendChild(statusBadge);
2467
- meta.appendChild(messageCount);
2722
+ const block = document.createElement('section');
2723
+ block.className = `tree-node-block tree-node-block-${node.kind}`;
2724
+ block.classList.toggle('has-active', !!node.hasActive);
2725
+ block.appendChild(item.root);
2468
2726
 
2469
- const time = document.createElement('div');
2470
- time.className = 'agent-time';
2471
- time.textContent = formatDateTime(session.updatedAt) || '暂无更新';
2727
+ const childrenNode = document.createElement('div');
2728
+ childrenNode.className = `tree-node-children tree-node-children-${node.kind}`;
2729
+ childrenNode.setAttribute('role', 'group');
2730
+ childrenNode.hidden = !node.expanded;
2731
+ renderSessionTreeNodes(node.children || [], childrenNode, ancestorHasNext.concat(!isLastSibling), itemCounter);
2732
+ block.appendChild(childrenNode);
2733
+ parentNode.appendChild(block);
2472
2734
 
2473
- btn.appendChild(sessionName);
2474
- btn.appendChild(meta);
2475
- btn.appendChild(time);
2476
- btn.addEventListener('click', function () {
2477
- handleSessionItemClick(btn.dataset.sessionName || '');
2735
+ const toggleNode = function () {
2736
+ const nextExpanded = childrenNode.hidden;
2737
+ if (typeof node.onToggle === 'function') {
2738
+ node.onToggle(nextExpanded);
2739
+ }
2740
+ setDisclosureExpanded(item, nextExpanded);
2741
+ childrenNode.hidden = !nextExpanded;
2742
+ };
2743
+ item.button.addEventListener('click', toggleNode);
2744
+ if (item.disclosure) {
2745
+ item.disclosure.addEventListener('click', toggleNode);
2746
+ }
2478
2747
  });
2479
- return btn;
2480
2748
  }
2481
2749
 
2482
2750
  function scrollActiveSessionIntoView() {
@@ -2492,6 +2760,37 @@
2492
2760
  }
2493
2761
  }
2494
2762
 
2763
+ function updateSidebarActiveSelection() {
2764
+ state.sessionNodeMap.forEach(function (buttonNode, sessionName) {
2765
+ const isActive = sessionName === state.active;
2766
+ if (!buttonNode) {
2767
+ return;
2768
+ }
2769
+ buttonNode.classList.toggle('active', isActive);
2770
+ buttonNode.setAttribute('aria-selected', isActive ? 'true' : 'false');
2771
+ });
2772
+
2773
+ if (!sessionList) {
2774
+ return;
2775
+ }
2776
+
2777
+ sessionList.querySelectorAll('.tree-node-block.has-active').forEach(function (blockNode) {
2778
+ blockNode.classList.remove('has-active');
2779
+ });
2780
+
2781
+ const activeNode = state.sessionNodeMap.get(state.active);
2782
+ let cursor = activeNode ? activeNode.parentElement : null;
2783
+ while (cursor && cursor !== sessionList) {
2784
+ if (cursor.classList && cursor.classList.contains('tree-node-children')) {
2785
+ const blockNode = cursor.parentElement;
2786
+ if (blockNode && blockNode.classList && blockNode.classList.contains('tree-node-block')) {
2787
+ blockNode.classList.add('has-active');
2788
+ }
2789
+ }
2790
+ cursor = cursor.parentElement;
2791
+ }
2792
+ }
2793
+
2495
2794
  function renderSessions() {
2496
2795
  const containerCount = new Set(state.sessions.map(function (session) {
2497
2796
  return session && session.containerName ? session.containerName : '';
@@ -2521,88 +2820,10 @@
2521
2820
  state.sessionRenderMode = 'tree';
2522
2821
 
2523
2822
  const grouped = groupSessionsByDirectory(state.sessions);
2524
- let itemIndex = 0;
2525
-
2526
- grouped.forEach(function (directoryGroup) {
2527
- const directoryExpanded = isDirectoryExpanded(directoryGroup);
2528
- const directoryHasActive = directoryContainsActiveSession(directoryGroup);
2529
- const group = document.createElement('section');
2530
- group.className = 'workbench-group';
2531
- group.classList.toggle('has-active', directoryHasActive);
2532
-
2533
- const groupHead = createTreeToggle({
2534
- className: 'workbench-group-head workbench-path-bar',
2535
- kickerClassName: 'workbench-group-kicker',
2536
- titleClassName: 'workbench-group-title',
2537
- kicker: '路径分组',
2538
- title: directoryGroup.path,
2539
- expanded: directoryExpanded,
2540
- metaParts: [
2541
- `${directoryGroup.containers.length} 容器`,
2542
- `${directoryGroup.containers.reduce(function (count, containerGroup) {
2543
- return count + containerGroup.sessions.length;
2544
- }, 0)} AGENT`,
2545
- formatDateTime(directoryGroup.updatedAt) || '暂无更新'
2546
- ]
2547
- });
2548
- group.appendChild(groupHead);
2549
-
2550
- const containerStack = document.createElement('div');
2551
- containerStack.className = 'container-stack workbench-group-body';
2552
- containerStack.hidden = !directoryExpanded;
2553
- groupHead.addEventListener('click', function () {
2554
- const nextExpanded = containerStack.hidden;
2555
- setSidebarDirectoryExpanded(directoryGroup.path, nextExpanded);
2556
- groupHead.setAttribute('aria-expanded', nextExpanded ? 'true' : 'false');
2557
- containerStack.hidden = !nextExpanded;
2558
- });
2559
-
2560
- directoryGroup.containers.forEach(function (containerGroup) {
2561
- const containerExpanded = isContainerExpanded(containerGroup);
2562
- const containerHasActive = containerContainsActiveSession(containerGroup);
2563
- const containerCard = document.createElement('section');
2564
- containerCard.className = 'container-card';
2565
- containerCard.classList.toggle('has-active', containerHasActive);
2566
-
2567
- const containerHead = document.createElement('div');
2568
- containerHead.className = 'container-card-head';
2569
-
2570
- const containerToggle = createContainerToggle(containerGroup, containerExpanded);
2571
-
2572
- const addAgentBtn = document.createElement('button');
2573
- addAgentBtn.type = 'button';
2574
- addAgentBtn.className = 'secondary add-agent-btn';
2575
- addAgentBtn.textContent = '新建AGENT';
2576
- addAgentBtn.addEventListener('click', function () {
2577
- createAgentSession(containerGroup.containerName);
2578
- });
2579
-
2580
- containerHead.appendChild(containerToggle);
2581
- containerCard.appendChild(containerHead);
2582
-
2583
- const agentList = document.createElement('div');
2584
- agentList.className = 'agent-list container-card-body';
2585
- agentList.hidden = !containerExpanded;
2586
- containerToggle.addEventListener('click', function () {
2587
- const nextExpanded = agentList.hidden;
2588
- setSidebarContainerExpanded(containerGroup.containerName, nextExpanded);
2589
- containerToggle.setAttribute('aria-expanded', nextExpanded ? 'true' : 'false');
2590
- agentList.hidden = !nextExpanded;
2591
- });
2592
- agentList.appendChild(addAgentBtn);
2593
- containerGroup.sessions.forEach(function (session) {
2594
- const row = createAgentRow(session, itemIndex);
2595
- state.sessionNodeMap.set(session.name, row);
2596
- agentList.appendChild(row);
2597
- itemIndex += 1;
2598
- });
2599
- containerCard.appendChild(agentList);
2600
- containerStack.appendChild(containerCard);
2601
- });
2602
-
2603
- group.appendChild(containerStack);
2604
- sessionList.appendChild(group);
2605
- });
2823
+ const treeNodes = buildSessionTreeNodes(grouped);
2824
+ const itemCounter = { value: 0 };
2825
+ renderSessionTreeNodes(treeNodes, sessionList, [], itemCounter);
2826
+ updateSidebarActiveSelection();
2606
2827
 
2607
2828
  scrollActiveSessionIntoView();
2608
2829
  }
@@ -2985,6 +3206,9 @@
2985
3206
  return;
2986
3207
  }
2987
3208
  state.sessionDetail = data && data.detail ? data.detail : null;
3209
+ if (state.agentTemplateModalOpen && targetSession === state.active) {
3210
+ fillAgentTemplateForm(state.sessionDetail || {});
3211
+ }
2988
3212
  } catch (e) {
2989
3213
  if (requestId !== state.sessionDetailRequestId) {
2990
3214
  return;
@@ -3272,6 +3496,14 @@
3272
3496
  });
3273
3497
  }
3274
3498
 
3499
+ if (agentTemplateBtn) {
3500
+ agentTemplateBtn.addEventListener('click', function () {
3501
+ openAgentTemplateModal().catch(function (e) {
3502
+ alert(e && e.message ? e.message : '加载 Agent 模板失败');
3503
+ });
3504
+ });
3505
+ }
3506
+
3275
3507
  if (configCancelBtn) {
3276
3508
  configCancelBtn.addEventListener('click', function () {
3277
3509
  closeConfigModal();
@@ -3336,6 +3568,25 @@
3336
3568
  });
3337
3569
  }
3338
3570
 
3571
+ if (agentTemplateCancelBtn) {
3572
+ agentTemplateCancelBtn.addEventListener('click', function () {
3573
+ closeAgentTemplateModal();
3574
+ syncUi();
3575
+ });
3576
+ }
3577
+
3578
+ if (agentTemplateResetBtn) {
3579
+ agentTemplateResetBtn.addEventListener('click', function () {
3580
+ resetAgentTemplateModal();
3581
+ });
3582
+ }
3583
+
3584
+ if (agentTemplateSaveBtn) {
3585
+ agentTemplateSaveBtn.addEventListener('click', function () {
3586
+ saveAgentTemplateModal();
3587
+ });
3588
+ }
3589
+
3339
3590
  if (createRun) {
3340
3591
  createRun.addEventListener('change', function () {
3341
3592
  applyCurrentRunDefaults();
@@ -3662,6 +3913,15 @@
3662
3913
  });
3663
3914
  }
3664
3915
 
3916
+ if (agentTemplateModal) {
3917
+ agentTemplateModal.addEventListener('click', function (event) {
3918
+ if (event.target === agentTemplateModal && !state.agentTemplateSaving) {
3919
+ closeAgentTemplateModal();
3920
+ syncUi();
3921
+ }
3922
+ });
3923
+ }
3924
+
3665
3925
  window.addEventListener('keydown', function (event) {
3666
3926
  if (event.key === 'Escape' && state.configModalOpen) {
3667
3927
  closeConfigModal();
@@ -3674,6 +3934,10 @@
3674
3934
  if (event.key === 'Escape' && state.directoryPicker.open) {
3675
3935
  closeDirectoryPicker();
3676
3936
  }
3937
+ if (event.key === 'Escape' && state.agentTemplateModalOpen) {
3938
+ closeAgentTemplateModal();
3939
+ syncUi();
3940
+ }
3677
3941
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
3678
3942
  closeMobileSessionPanel();
3679
3943
  }