@xcanwin/manyoyo 5.7.2 → 5.7.4

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.
@@ -63,6 +63,17 @@
63
63
  createRuns: {},
64
64
  sessionNodeMap: new Map(),
65
65
  sessionRenderMode: 'empty',
66
+ directoryPicker: {
67
+ open: false,
68
+ loading: false,
69
+ mode: '',
70
+ title: '',
71
+ tip: '',
72
+ currentPath: '',
73
+ basePath: '',
74
+ entries: [],
75
+ error: ''
76
+ },
66
77
  messageRequestId: 0,
67
78
  agentRun: {
68
79
  active: false,
@@ -104,6 +115,7 @@
104
115
  const openConfigBtn = document.getElementById('openConfigBtn');
105
116
  const openCreateBtn = document.getElementById('openCreateBtn');
106
117
  const configModal = document.getElementById('configModal');
118
+ const configModalTitle = document.getElementById('configModalTitle');
107
119
  const configPath = document.getElementById('configPath');
108
120
  const configEditor = document.getElementById('configEditor');
109
121
  const configError = document.getElementById('configError');
@@ -120,6 +132,8 @@
120
132
  const createContainerName = document.getElementById('createContainerName');
121
133
  const createHostPath = document.getElementById('createHostPath');
122
134
  const createContainerPath = document.getElementById('createContainerPath');
135
+ const pickHostPathBtn = document.getElementById('pickHostPathBtn');
136
+ const pickContainerPathBtn = document.getElementById('pickContainerPathBtn');
123
137
  const createImageName = document.getElementById('createImageName');
124
138
  const createImageVersion = document.getElementById('createImageVersion');
125
139
  const createContainerMode = document.getElementById('createContainerMode');
@@ -131,6 +145,15 @@
131
145
  const createEnv = document.getElementById('createEnv');
132
146
  const createEnvFile = document.getElementById('createEnvFile');
133
147
  const createVolumes = document.getElementById('createVolumes');
148
+ const directoryPickerModal = document.getElementById('directoryPickerModal');
149
+ const directoryPickerTitle = document.getElementById('directoryPickerTitle');
150
+ const directoryPickerTip = document.getElementById('directoryPickerTip');
151
+ const directoryPickerCurrent = document.getElementById('directoryPickerCurrent');
152
+ const directoryPickerList = document.getElementById('directoryPickerList');
153
+ const directoryPickerError = document.getElementById('directoryPickerError');
154
+ const directoryPickerCancelBtn = document.getElementById('directoryPickerCancelBtn');
155
+ const directoryPickerUpBtn = document.getElementById('directoryPickerUpBtn');
156
+ const directoryPickerSelectBtn = document.getElementById('directoryPickerSelectBtn');
134
157
  const activeTitle = document.getElementById('activeTitle');
135
158
  const activeMeta = document.getElementById('activeMeta');
136
159
  const activityCommandBtn = document.getElementById('activityCommandBtn');
@@ -436,6 +459,32 @@
436
459
  });
437
460
  }
438
461
 
462
+ function normalizeSlashPath(value) {
463
+ return String(value || '').replace(/\\/g, '/');
464
+ }
465
+
466
+ function isChildPath(basePath, targetPath) {
467
+ const normalizedBase = normalizeSlashPath(basePath).replace(/\/+$/, '');
468
+ const normalizedTarget = normalizeSlashPath(targetPath).replace(/\/+$/, '');
469
+ if (!normalizedBase) {
470
+ return false;
471
+ }
472
+ return normalizedTarget === normalizedBase || normalizedTarget.startsWith(normalizedBase + '/');
473
+ }
474
+
475
+ function buildContainerPathFromHostSelection(baseHostPath, baseContainerPath, selectedHostPath) {
476
+ const normalizedBaseHost = normalizeSlashPath(baseHostPath).replace(/\/+$/, '');
477
+ const normalizedContainer = normalizeSlashPath(baseContainerPath).replace(/\/+$/, '') || '/workspace';
478
+ const normalizedSelected = normalizeSlashPath(selectedHostPath).replace(/\/+$/, '');
479
+ if (!normalizedBaseHost || !isChildPath(normalizedBaseHost, normalizedSelected)) {
480
+ return normalizedContainer;
481
+ }
482
+ const relative = normalizedSelected === normalizedBaseHost
483
+ ? ''
484
+ : normalizedSelected.slice(normalizedBaseHost.length + 1);
485
+ return relative ? `${normalizedContainer}/${relative}`.replace(/\/+/g, '/') : normalizedContainer;
486
+ }
487
+
439
488
  function setModalVisible(modalNode, visible) {
440
489
  if (!modalNode) return;
441
490
  modalNode.hidden = !visible;
@@ -466,6 +515,18 @@
466
515
  configError.textContent = text;
467
516
  }
468
517
 
518
+ function showDirectoryPickerError(message) {
519
+ if (!directoryPickerError) return;
520
+ const text = String(message || '').trim();
521
+ if (!text) {
522
+ directoryPickerError.hidden = true;
523
+ directoryPickerError.textContent = '';
524
+ return;
525
+ }
526
+ directoryPickerError.hidden = false;
527
+ directoryPickerError.textContent = text;
528
+ }
529
+
469
530
  function envMapToText(envMap) {
470
531
  if (!envMap || typeof envMap !== 'object') {
471
532
  return '';
@@ -621,9 +682,10 @@
621
682
  createAgentPromptCommand.value = value.agentPromptCommand || '';
622
683
  state.createAgentPromptAuto = false;
623
684
  createYolo.value = value.yolo || '';
624
- createEnv.value = envMapToText(value.env);
625
- createEnvFile.value = Array.isArray(value.envFile) ? value.envFile.join('\n') : '';
626
- createVolumes.value = Array.isArray(value.volumes) ? value.volumes.join('\n') : '';
685
+ // 敏感 env 与继承数组由服务端在创建时合并,前端表单默认不回显,避免泄露或重复提交。
686
+ createEnv.value = '';
687
+ createEnvFile.value = '';
688
+ createVolumes.value = '';
627
689
  updateCreateAgentPromptCommandFromCommand();
628
690
  }
629
691
 
@@ -792,7 +854,8 @@
792
854
  const status = sessionStatusInfo(session.status);
793
855
  const messageCount = safeMessageCount(session.messageCount);
794
856
  const updatedAt = formatDateTime(session.updatedAt) || '暂无更新';
795
- return `${status.label} · ${messageCount} 条对话 · ${updatedAt}`;
857
+ const containerName = session.containerName || '未绑定容器';
858
+ return `${containerName} · ${status.label} · ${messageCount} 条对话 · ${updatedAt}`;
796
859
  }
797
860
 
798
861
  function buildMessageMetaLines(message) {
@@ -1236,9 +1299,9 @@
1236
1299
  function renderSessionDetailPanels() {
1237
1300
  const detail = state.sessionDetail;
1238
1301
  if (!state.active) {
1239
- renderEmptyInspector(detailSummary, '详情视图', '选择左侧会话后,这里会显示会话概览、Agent 状态与运行参数。');
1240
- renderEmptyInspector(configSummary, '配置视图', '选择会话后可查看当前容器会话的运行参数摘要。');
1241
- renderEmptyInspector(checkSummary, '检查视图', '选择会话后可查看当前会话的基础健康检查。');
1302
+ renderEmptyInspector(detailSummary, '详情视图', '选择左侧会话后,这里会显示会话概览、Agent 运行状态与最近活动。');
1303
+ renderEmptyInspector(configSummary, '配置视图', '选择会话后可查看当前容器会话的生效配置摘要。');
1304
+ renderEmptyInspector(checkSummary, '检查视图', '选择会话后可查看当前会话的诊断结论与最近问题。');
1242
1305
  return;
1243
1306
  }
1244
1307
  if (state.loadingSessionDetail) {
@@ -1258,55 +1321,98 @@
1258
1321
  const applied = detail.applied || {};
1259
1322
  const status = sessionStatusInfo(detail.status);
1260
1323
  const updatedText = formatDateTime(detail.updatedAt) || '暂无更新';
1324
+ const lastResumeText = detail.lastResumeAt ? formatDateTime(detail.lastResumeAt) : '暂无';
1325
+ const latestTimestampText = detail.latestTimestamp ? formatDateTime(detail.latestTimestamp) : '暂无';
1326
+ const latestRoleMap = {
1327
+ user: '我',
1328
+ assistant: 'Agent',
1329
+ system: '系统'
1330
+ };
1331
+ const latestRoleLabel = latestRoleMap[String(detail.latestRole || '').toLowerCase()] || (detail.latestRole || '暂无');
1332
+ const imageVersionValid = /^\d+\.\d+\.\d+-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(String(applied.imageVersion || ''));
1333
+ let resumeStatusValue = '未执行';
1334
+ let resumeStatusTone = 'warn';
1335
+ let resumeStatusDetail = detail.resumeSupported
1336
+ ? '支持 resume,但当前会话还没有最近一次执行记录。'
1337
+ : '当前 Agent 程序或模板不支持 resume。';
1338
+ if (detail.lastResumeOk === true) {
1339
+ resumeStatusValue = '最近成功';
1340
+ resumeStatusTone = 'ok';
1341
+ resumeStatusDetail = `最近一次 resume 成功,时间:${lastResumeText}。`;
1342
+ } else if (detail.lastResumeOk === false) {
1343
+ resumeStatusValue = '最近失败';
1344
+ resumeStatusTone = 'danger';
1345
+ resumeStatusDetail = detail.lastResumeError
1346
+ ? `最近一次 resume 失败:${detail.lastResumeError}`
1347
+ : `最近一次 resume 失败,时间:${lastResumeText}。`;
1348
+ } else if (!detail.resumeSupported) {
1349
+ resumeStatusValue = '不支持';
1350
+ }
1351
+
1352
+ const commandEntries = [];
1353
+ if (applied.shellPrefix) {
1354
+ commandEntries.push({ label: 'shellPrefix', value: applied.shellPrefix });
1355
+ }
1356
+ if (applied.shell) {
1357
+ commandEntries.push({ label: 'shell', value: applied.shell });
1358
+ }
1359
+ if (applied.shellSuffix) {
1360
+ commandEntries.push({ label: 'shellSuffix', value: applied.shellSuffix });
1361
+ }
1362
+ if (applied.defaultCommand && applied.defaultCommand !== applied.shell) {
1363
+ commandEntries.push({ label: '启动命令', value: applied.defaultCommand });
1364
+ } else if (!applied.shell) {
1365
+ commandEntries.push({ label: '启动命令', value: applied.defaultCommand || '—' });
1366
+ }
1367
+ commandEntries.push({ label: 'Agent 模板', value: detail.agentPromptCommand || '—' });
1368
+ commandEntries.push({ label: 'yolo', value: applied.yolo || '—' });
1261
1369
 
1262
1370
  if (detailSummary) {
1263
1371
  detailSummary.innerHTML = '';
1264
1372
  renderKeyValueCard(detailSummary, '会话概览', [
1265
- { label: '会话', value: detail.name || state.active },
1373
+ { label: 'AGENT', value: detail.agentName || detail.name || state.active },
1374
+ { label: '容器', value: detail.containerName || '—' },
1266
1375
  { label: '状态', value: status.label, tone: status.tone },
1267
1376
  { label: '镜像', value: detail.image || applied.imageName || '—' },
1268
1377
  { label: '最近更新', value: updatedText },
1269
1378
  { label: '消息数', value: String(safeMessageCount(detail.messageCount)) }
1270
1379
  ]);
1271
- renderKeyValueCard(detailSummary, 'Agent 上下文', [
1380
+ renderKeyValueCard(detailSummary, 'Agent 运行', [
1272
1381
  { label: '已启用', value: detail.agentEnabled ? '是' : '否', tone: detail.agentEnabled ? 'ok' : 'warn' },
1273
1382
  { label: '程序', value: detail.agentProgram || '—' },
1274
1383
  { label: '支持 resume', value: detail.resumeSupported ? '是' : '否', tone: detail.resumeSupported ? 'ok' : 'warn' },
1275
- { label: '最近 resume', value: detail.lastResumeAt ? formatDateTime(detail.lastResumeAt) : '暂无' },
1384
+ { label: '最近 resume', value: lastResumeText },
1276
1385
  { label: '最近结果', value: detail.lastResumeOk == null ? '暂无' : (detail.lastResumeOk ? '成功' : '失败'), tone: detail.lastResumeOk == null ? 'info' : (detail.lastResumeOk ? 'ok' : 'danger') }
1277
1386
  ]);
1278
- renderKeyValueCard(detailSummary, '运行参数', [
1279
- { label: 'hostPath', value: applied.hostPath || '—' },
1280
- { label: 'containerPath', value: applied.containerPath || '—' },
1281
- { label: 'imageVersion', value: applied.imageVersion || '—' },
1282
- { label: 'containerMode', value: applied.containerMode || 'default' },
1283
- { label: 'env/vol/ports', value: `${applied.envCount || 0} / ${applied.volumeCount || 0} / ${applied.portCount || 0}` }
1387
+ renderKeyValueCard(detailSummary, '最近活动', [
1388
+ { label: '最近角色', value: latestRoleLabel },
1389
+ { label: '最近时间', value: latestTimestampText },
1390
+ { label: 'resume 状态', value: resumeStatusValue, tone: resumeStatusTone }
1284
1391
  ]);
1285
1392
  }
1286
1393
 
1287
1394
  if (configSummary) {
1288
1395
  configSummary.innerHTML = '';
1289
- renderKeyValueCard(configSummary, '配置摘要', [
1290
- { label: 'containerName', value: applied.containerName || detail.name || state.active },
1291
- { label: 'hostPath', value: applied.hostPath || '—' },
1292
- { label: 'containerPath', value: applied.containerPath || '—' },
1396
+ renderKeyValueCard(configSummary, '基础配置', [
1397
+ { label: 'AGENT', value: detail.agentName || '—' },
1398
+ { label: 'containerName', value: applied.containerName || detail.containerName || '—' },
1293
1399
  { label: 'imageName', value: applied.imageName || detail.image || '—' },
1294
1400
  { label: 'imageVersion', value: applied.imageVersion || '—' },
1295
1401
  { label: 'containerMode', value: applied.containerMode || 'default' }
1296
- ], { actionLabel: '打开配置', actionId: 'configSummaryOpenBtn' });
1297
- renderKeyValueCard(configSummary, '命令与 Agent', [
1298
- { label: 'shellPrefix', value: applied.shellPrefix || '—' },
1299
- { label: 'shell', value: applied.shell || '—' },
1300
- { label: 'shellSuffix', value: applied.shellSuffix || '—' },
1301
- { label: '默认命令', value: applied.defaultCommand || '—' },
1302
- { label: 'Agent 模板', value: detail.agentPromptCommand || '—' },
1303
- { label: 'yolo', value: applied.yolo || '—' }
1304
1402
  ]);
1403
+ renderKeyValueCard(configSummary, '路径与资源', [
1404
+ { label: 'hostPath', value: applied.hostPath || '—' },
1405
+ { label: 'containerPath', value: applied.containerPath || '—' },
1406
+ { label: 'env 数量', value: String(applied.envCount || 0) },
1407
+ { label: 'volume 数量', value: String(applied.volumeCount || 0) },
1408
+ { label: 'port 数量', value: String(applied.portCount || 0) }
1409
+ ]);
1410
+ renderKeyValueCard(configSummary, '命令与 Agent', commandEntries);
1305
1411
  }
1306
1412
 
1307
1413
  if (checkSummary) {
1308
1414
  checkSummary.innerHTML = '';
1309
- renderCheckCard(checkSummary, '基础检查', [
1415
+ renderCheckCard(checkSummary, '运行检查', [
1310
1416
  {
1311
1417
  label: '容器状态',
1312
1418
  value: status.label,
@@ -1314,28 +1420,32 @@
1314
1420
  detail: status.tone === 'running' ? '容器处于可交互状态。' : '当前不是活跃运行态,部分功能可能受限。'
1315
1421
  },
1316
1422
  {
1317
- label: 'Agent 模板',
1423
+ label: 'Agent 输入',
1318
1424
  value: detail.agentEnabled ? '已配置' : '未配置',
1319
1425
  tone: detail.agentEnabled ? 'ok' : 'warn',
1320
1426
  detail: detail.agentEnabled ? '活动页可直接发送 Agent 提示词。' : '当前会话不支持 Agent 模式。'
1321
1427
  },
1322
1428
  {
1323
- label: 'Resume 能力',
1324
- value: detail.resumeSupported ? '支持' : '不支持',
1325
- tone: detail.resumeSupported ? 'ok' : 'warn',
1326
- detail: detail.resumeSupported ? '可以尝试基于历史继续 Agent 会话。' : '当前 Agent 程序或模板不支持 resume。'
1429
+ label: 'Resume 健康',
1430
+ value: resumeStatusValue,
1431
+ tone: resumeStatusTone,
1432
+ detail: resumeStatusDetail
1327
1433
  },
1328
1434
  {
1329
- label: '镜像版本格式',
1330
- value: applied.imageVersion || '缺失',
1331
- tone: /^\d+\.\d+\.\d+-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(String(applied.imageVersion || '')) ? 'ok' : 'danger',
1332
- detail: '建议保持 x.y.z-后缀 格式,便于 manyoyo 的版本校验。'
1435
+ label: '镜像版本',
1436
+ value: imageVersionValid ? '格式正常' : '格式异常',
1437
+ tone: imageVersionValid ? 'ok' : 'danger',
1438
+ detail: applied.imageVersion
1439
+ ? `当前值:${applied.imageVersion}。建议保持 x.y.z-后缀 格式,便于 manyoyo 的版本校验。`
1440
+ : '缺少 imageVersion,manyoyo 的版本校验会失效。'
1333
1441
  },
1334
1442
  {
1335
- label: '工作目录',
1336
- value: applied.hostPath && applied.containerPath ? '完整' : '缺失',
1443
+ label: '工作目录映射',
1444
+ value: applied.hostPath && applied.containerPath ? '已配置' : '缺失',
1337
1445
  tone: applied.hostPath && applied.containerPath ? 'ok' : 'danger',
1338
- detail: 'hostPath / containerPath 是容器会话最关键的上下文。'
1446
+ detail: applied.hostPath && applied.containerPath
1447
+ ? '宿主目录与容器目录都已配置。'
1448
+ : 'hostPath / containerPath 是容器会话最关键的上下文。'
1339
1449
  }
1340
1450
  ]);
1341
1451
  if (detail.lastResumeError) {
@@ -1349,13 +1459,6 @@
1349
1459
  ]);
1350
1460
  }
1351
1461
  }
1352
-
1353
- const configSummaryOpenBtn = document.getElementById('configSummaryOpenBtn');
1354
- if (configSummaryOpenBtn) {
1355
- configSummaryOpenBtn.addEventListener('click', function () {
1356
- openConfigModal();
1357
- });
1358
- }
1359
1462
  }
1360
1463
 
1361
1464
  function syncUi() {
@@ -1366,8 +1469,9 @@
1366
1469
  commandInput.value = '';
1367
1470
  }
1368
1471
  } else {
1369
- activeTitle.textContent = state.active;
1370
- activeMeta.textContent = buildActiveMeta(getActiveSession());
1472
+ const activeSession = getActiveSession();
1473
+ activeTitle.textContent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
1474
+ activeMeta.textContent = buildActiveMeta(activeSession);
1371
1475
  }
1372
1476
 
1373
1477
  const activityTab = state.activeTab === 'activity';
@@ -1444,7 +1548,7 @@
1444
1548
  openConfigBtn.disabled = state.configLoading || state.configSaving;
1445
1549
  }
1446
1550
  if (configSaveBtn) {
1447
- configSaveBtn.disabled = state.configLoading || state.configSaving;
1551
+ configSaveBtn.disabled = true;
1448
1552
  }
1449
1553
  if (configReloadBtn) {
1450
1554
  configReloadBtn.disabled = state.configLoading || state.configSaving;
@@ -1482,6 +1586,13 @@
1482
1586
  if (createModal) {
1483
1587
  createModal.hidden = !state.createModalOpen;
1484
1588
  }
1589
+ if (directoryPickerModal) {
1590
+ directoryPickerModal.hidden = !state.directoryPicker.open;
1591
+ }
1592
+ document.body.classList.toggle(
1593
+ 'modal-open',
1594
+ state.configModalOpen || state.createModalOpen || state.directoryPicker.open
1595
+ );
1485
1596
  if (!state.active) {
1486
1597
  sendState.textContent = '未选择会话';
1487
1598
  } else if (agentMode && !agentEnabled) {
@@ -1612,11 +1723,22 @@
1612
1723
  syncUi();
1613
1724
  try {
1614
1725
  const config = await fetchConfigSnapshot();
1726
+ if (configModalTitle) {
1727
+ configModalTitle.textContent = '查看配置摘要 (~/.manyoyo/manyoyo.json)';
1728
+ }
1615
1729
  if (configPath) {
1616
- configPath.textContent = config.path || '';
1730
+ const lines = [config.path || ''];
1731
+ if (config.notice) {
1732
+ lines.push(config.notice);
1733
+ }
1734
+ configPath.textContent = lines.filter(Boolean).join('\n');
1617
1735
  }
1618
1736
  if (configEditor) {
1619
- configEditor.value = typeof config.raw === 'string' ? config.raw : '';
1737
+ configEditor.readOnly = true;
1738
+ configEditor.value = stringifyPrettyJson({
1739
+ defaults: config.defaults || {},
1740
+ runs: config.parsed && config.parsed.runs ? config.parsed.runs : {}
1741
+ });
1620
1742
  }
1621
1743
  if (config.parseError) {
1622
1744
  showConfigError('当前文件存在解析错误:' + config.parseError);
@@ -1637,23 +1759,7 @@
1637
1759
  }
1638
1760
 
1639
1761
  async function saveConfig() {
1640
- state.configSaving = true;
1641
- showConfigError('');
1642
- syncUi();
1643
- try {
1644
- await api('/api/config', {
1645
- method: 'PUT',
1646
- body: JSON.stringify({ raw: configEditor ? configEditor.value : '' })
1647
- });
1648
- await fetchConfigSnapshot();
1649
- showConfigError('');
1650
- alert('配置已保存。后续新建会读取最新配置。');
1651
- } catch (e) {
1652
- showConfigError(e.message);
1653
- } finally {
1654
- state.configSaving = false;
1655
- syncUi();
1656
- }
1762
+ alert('Web 端已禁用明文配置编辑,请在本地 ~/.manyoyo/manyoyo.json 中维护敏感配置。');
1657
1763
  }
1658
1764
 
1659
1765
  async function openCreateModal() {
@@ -1683,6 +1789,7 @@
1683
1789
  function closeCreateModal() {
1684
1790
  state.createModalOpen = false;
1685
1791
  setModalVisible(createModal, false);
1792
+ closeDirectoryPicker();
1686
1793
  showCreateError('');
1687
1794
  }
1688
1795
 
@@ -1691,57 +1798,159 @@
1691
1798
  showCreateError('');
1692
1799
  }
1693
1800
 
1694
- function renderSessionsLoading() {
1695
- state.sessionNodeMap.clear();
1696
- state.sessionRenderMode = 'loading';
1697
- sessionList.innerHTML = '';
1698
- for (let i = 0; i < 3; i++) {
1699
- const skeleton = document.createElement('div');
1700
- skeleton.className = 'skeleton session';
1701
- sessionList.appendChild(skeleton);
1801
+ function renderDirectoryPicker() {
1802
+ if (!directoryPickerModal) return;
1803
+ const picker = state.directoryPicker;
1804
+ setModalVisible(directoryPickerModal, picker.open);
1805
+ if (directoryPickerTitle) {
1806
+ directoryPickerTitle.textContent = picker.title || '选择目录';
1702
1807
  }
1808
+ if (directoryPickerTip) {
1809
+ directoryPickerTip.textContent = picker.tip || '';
1810
+ }
1811
+ if (directoryPickerCurrent) {
1812
+ directoryPickerCurrent.textContent = picker.currentPath || '未选择目录';
1813
+ }
1814
+ showDirectoryPickerError(picker.error);
1815
+ if (directoryPickerUpBtn) {
1816
+ directoryPickerUpBtn.disabled = picker.loading || !picker.currentPath || !picker.parentPath;
1817
+ }
1818
+ if (directoryPickerSelectBtn) {
1819
+ directoryPickerSelectBtn.disabled = picker.loading || !picker.currentPath;
1820
+ }
1821
+ if (!directoryPickerList) {
1822
+ return;
1823
+ }
1824
+ directoryPickerList.innerHTML = '';
1825
+ if (picker.loading) {
1826
+ const loading = document.createElement('div');
1827
+ loading.className = 'empty';
1828
+ loading.textContent = '目录加载中...';
1829
+ directoryPickerList.appendChild(loading);
1830
+ return;
1831
+ }
1832
+ if (!picker.entries.length) {
1833
+ const empty = document.createElement('div');
1834
+ empty.className = 'empty';
1835
+ empty.textContent = '当前目录下没有可选子目录';
1836
+ directoryPickerList.appendChild(empty);
1837
+ return;
1838
+ }
1839
+ picker.entries.forEach(function (entry) {
1840
+ const btn = document.createElement('button');
1841
+ btn.type = 'button';
1842
+ btn.className = 'dir-picker-item secondary';
1843
+ btn.textContent = entry.name;
1844
+ btn.addEventListener('click', function () {
1845
+ loadDirectoryPicker(entry.path);
1846
+ });
1847
+ directoryPickerList.appendChild(btn);
1848
+ });
1703
1849
  }
1704
1850
 
1705
- function getSessionRenderKey(session) {
1706
- return [
1707
- String(session && session.name ? session.name : ''),
1708
- String(session && session.status ? session.status : ''),
1709
- String(safeMessageCount(session && session.messageCount)),
1710
- String(session && session.updatedAt ? session.updatedAt : ''),
1711
- String(session && session.image ? session.image : '')
1712
- ].join('|');
1851
+ async function loadDirectoryPicker(targetPath) {
1852
+ const picker = state.directoryPicker;
1853
+ picker.loading = true;
1854
+ picker.error = '';
1855
+ if (targetPath) {
1856
+ picker.currentPath = targetPath;
1857
+ }
1858
+ renderDirectoryPicker();
1859
+ try {
1860
+ const params = new URLSearchParams();
1861
+ params.set('path', picker.currentPath || '/');
1862
+ if (picker.basePath) {
1863
+ params.set('basePath', picker.basePath);
1864
+ }
1865
+ const data = await api('/api/fs/directories?' + params.toString());
1866
+ picker.currentPath = data.currentPath || picker.currentPath;
1867
+ picker.basePath = data.basePath || picker.basePath || '';
1868
+ picker.parentPath = data.parentPath || '';
1869
+ picker.entries = Array.isArray(data.entries) ? data.entries : [];
1870
+ } catch (e) {
1871
+ picker.error = e && e.message ? e.message : '目录加载失败';
1872
+ picker.entries = [];
1873
+ } finally {
1874
+ picker.loading = false;
1875
+ renderDirectoryPicker();
1876
+ }
1713
1877
  }
1714
1878
 
1715
- function renderSessionActiveState() {
1716
- for (const [name, node] of state.sessionNodeMap.entries()) {
1717
- node.classList.toggle('active', state.active === name);
1718
- }
1879
+ function closeDirectoryPicker() {
1880
+ state.directoryPicker.open = false;
1881
+ state.directoryPicker.loading = false;
1882
+ state.directoryPicker.mode = '';
1883
+ state.directoryPicker.title = '';
1884
+ state.directoryPicker.tip = '';
1885
+ state.directoryPicker.currentPath = '';
1886
+ state.directoryPicker.basePath = '';
1887
+ state.directoryPicker.parentPath = '';
1888
+ state.directoryPicker.entries = [];
1889
+ state.directoryPicker.error = '';
1890
+ renderDirectoryPicker();
1719
1891
  }
1720
1892
 
1721
- function updateSessionRow(row, session, index) {
1722
- if (!row || !session) return;
1723
- const status = sessionStatusInfo(session.status);
1724
- row.style.setProperty('--item-index', String(index));
1725
- row.classList.toggle('active', state.active === session.name);
1726
- row.classList.toggle('history-only', status.tone === 'history');
1727
- row.classList.toggle('status-running', status.tone === 'running');
1728
- row.classList.toggle('status-stopped', status.tone === 'stopped');
1729
- row.classList.toggle('status-history', status.tone === 'history');
1730
- row.classList.toggle('status-unknown', status.tone === 'unknown');
1731
- if (row.__sessionNameNode) {
1732
- row.__sessionNameNode.textContent = session.name;
1733
- }
1734
- if (row.__statusBadgeNode) {
1735
- row.__statusBadgeNode.className = `session-status ${status.tone}`;
1736
- row.__statusBadgeNode.textContent = status.label;
1893
+ function applyPickedDirectory() {
1894
+ const picker = state.directoryPicker;
1895
+ if (!picker.currentPath) {
1896
+ return;
1737
1897
  }
1738
- if (row.__messageCountNode) {
1739
- row.__messageCountNode.textContent = `${safeMessageCount(session.messageCount)} 条`;
1898
+ if (picker.mode === 'host') {
1899
+ createHostPath.value = picker.currentPath;
1900
+ if (!(createContainerPath.value || '').trim()) {
1901
+ createContainerPath.value = '/workspace';
1902
+ }
1903
+ } else if (picker.mode === 'container') {
1904
+ const mapped = buildContainerPathFromHostSelection(
1905
+ picker.basePath,
1906
+ (createContainerPath.value || '').trim() || '/workspace',
1907
+ picker.currentPath
1908
+ );
1909
+ createContainerPath.value = mapped;
1910
+ }
1911
+ closeDirectoryPicker();
1912
+ }
1913
+
1914
+ function openDirectoryPicker(mode) {
1915
+ const picker = state.directoryPicker;
1916
+ picker.open = true;
1917
+ picker.loading = false;
1918
+ picker.error = '';
1919
+ picker.entries = [];
1920
+ picker.parentPath = '';
1921
+ if (mode === 'container') {
1922
+ const baseHostPath = (createHostPath.value || '').trim();
1923
+ if (!baseHostPath) {
1924
+ showCreateError('请先选择 hostPath,再选择 containerPath。');
1925
+ picker.open = false;
1926
+ renderDirectoryPicker();
1927
+ return;
1928
+ }
1929
+ picker.mode = 'container';
1930
+ picker.title = '选择 containerPath 对应目录';
1931
+ picker.tip = '从 hostPath 下选择子目录,结果会映射到容器路径。';
1932
+ picker.basePath = baseHostPath;
1933
+ picker.currentPath = baseHostPath;
1934
+ } else {
1935
+ picker.mode = 'host';
1936
+ picker.title = '选择 hostPath';
1937
+ picker.tip = '浏览宿主机目录,选中后会回填 create 表单。';
1938
+ picker.basePath = '';
1939
+ picker.currentPath = (createHostPath.value || '').trim() || '/';
1740
1940
  }
1741
- if (row.__timeNode) {
1742
- row.__timeNode.textContent = formatDateTime(session.updatedAt) || '暂无更新';
1941
+ renderDirectoryPicker();
1942
+ loadDirectoryPicker(picker.currentPath);
1943
+ }
1944
+
1945
+ function renderSessionsLoading() {
1946
+ state.sessionNodeMap.clear();
1947
+ state.sessionRenderMode = 'loading';
1948
+ sessionList.innerHTML = '';
1949
+ for (let i = 0; i < 3; i++) {
1950
+ const skeleton = document.createElement('div');
1951
+ skeleton.className = 'skeleton session';
1952
+ sessionList.appendChild(skeleton);
1743
1953
  }
1744
- row.__renderKey = getSessionRenderKey(session);
1745
1954
  }
1746
1955
 
1747
1956
  function handleSessionItemClick(sessionName) {
@@ -1772,7 +1981,7 @@
1772
1981
  }
1773
1982
  }
1774
1983
  }
1775
- renderSessionActiveState();
1984
+ renderSessions();
1776
1985
  syncUi();
1777
1986
  Promise.all([
1778
1987
  loadMessagesForSession(sessionName),
@@ -1782,49 +1991,113 @@
1782
1991
  });
1783
1992
  }
1784
1993
 
1785
- function createSessionRow(session, index) {
1994
+ async function createAgentSession(containerName) {
1995
+ const targetContainer = String(containerName || '').trim();
1996
+ if (!targetContainer) {
1997
+ return;
1998
+ }
1999
+ try {
2000
+ const data = await api('/api/sessions/' + encodeURIComponent(targetContainer) + '/agents', {
2001
+ method: 'POST',
2002
+ body: JSON.stringify({})
2003
+ });
2004
+ state.activeTab = 'activity';
2005
+ state.mode = 'agent';
2006
+ await loadSessions(data.name);
2007
+ if (isMobileLayout()) {
2008
+ closeMobileSessionPanel();
2009
+ }
2010
+ } catch (e) {
2011
+ alert(e.message);
2012
+ }
2013
+ }
2014
+
2015
+ function groupSessionsByDirectory(sessions) {
2016
+ const groups = new Map();
2017
+ (Array.isArray(sessions) ? sessions : []).forEach(function (session) {
2018
+ const directoryPath = String(session && session.hostPath ? session.hostPath : '').trim() || '未配置目录';
2019
+ if (!groups.has(directoryPath)) {
2020
+ groups.set(directoryPath, {
2021
+ path: directoryPath,
2022
+ updatedAt: session && session.updatedAt ? session.updatedAt : '',
2023
+ containers: new Map()
2024
+ });
2025
+ }
2026
+ const directoryGroup = groups.get(directoryPath);
2027
+ if (session && session.updatedAt && (!directoryGroup.updatedAt || new Date(session.updatedAt).getTime() > new Date(directoryGroup.updatedAt).getTime())) {
2028
+ directoryGroup.updatedAt = session.updatedAt;
2029
+ }
2030
+
2031
+ const containerName = String(session && session.containerName ? session.containerName : '');
2032
+ if (!directoryGroup.containers.has(containerName)) {
2033
+ directoryGroup.containers.set(containerName, {
2034
+ containerName: containerName,
2035
+ status: session && session.status ? session.status : 'history',
2036
+ image: session && session.image ? session.image : '',
2037
+ updatedAt: session && session.updatedAt ? session.updatedAt : '',
2038
+ sessions: []
2039
+ });
2040
+ }
2041
+ const containerGroup = directoryGroup.containers.get(containerName);
2042
+ containerGroup.sessions.push(session);
2043
+ if (session && session.updatedAt && (!containerGroup.updatedAt || new Date(session.updatedAt).getTime() > new Date(containerGroup.updatedAt).getTime())) {
2044
+ containerGroup.updatedAt = session.updatedAt;
2045
+ }
2046
+ });
2047
+ return Array.from(groups.values()).sort(function (a, b) {
2048
+ const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2049
+ const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
2050
+ return timeB - timeA;
2051
+ });
2052
+ }
2053
+
2054
+ function createAgentRow(session, index) {
1786
2055
  const status = sessionStatusInfo(session.status);
1787
2056
  const btn = document.createElement('button');
1788
2057
  btn.type = 'button';
1789
- btn.className = 'session-item';
2058
+ btn.className = 'agent-item';
1790
2059
  btn.dataset.sessionName = session.name;
2060
+ btn.style.setProperty('--item-index', String(index));
2061
+ btn.classList.toggle('active', state.active === session.name);
1791
2062
 
1792
2063
  const sessionName = document.createElement('div');
1793
- sessionName.className = 'session-name';
2064
+ sessionName.className = 'agent-name';
2065
+ sessionName.textContent = session.agentName || session.name;
1794
2066
 
1795
2067
  const meta = document.createElement('div');
1796
- meta.className = 'session-meta';
2068
+ meta.className = 'agent-meta';
1797
2069
 
1798
2070
  const statusBadge = document.createElement('span');
1799
2071
  statusBadge.className = `session-status ${status.tone}`;
2072
+ statusBadge.textContent = status.label;
1800
2073
 
1801
2074
  const messageCount = document.createElement('span');
1802
2075
  messageCount.className = 'session-count';
2076
+ messageCount.textContent = `${safeMessageCount(session.messageCount)} 条`;
1803
2077
 
1804
2078
  meta.appendChild(statusBadge);
1805
2079
  meta.appendChild(messageCount);
1806
2080
 
1807
2081
  const time = document.createElement('div');
1808
- time.className = 'session-time';
2082
+ time.className = 'agent-time';
2083
+ time.textContent = formatDateTime(session.updatedAt) || '暂无更新';
1809
2084
 
1810
2085
  btn.appendChild(sessionName);
1811
2086
  btn.appendChild(meta);
1812
2087
  btn.appendChild(time);
1813
- btn.__sessionNameNode = sessionName;
1814
- btn.__statusBadgeNode = statusBadge;
1815
- btn.__messageCountNode = messageCount;
1816
- btn.__timeNode = time;
1817
-
1818
2088
  btn.addEventListener('click', function () {
1819
2089
  handleSessionItemClick(btn.dataset.sessionName || '');
1820
2090
  });
1821
-
1822
- updateSessionRow(btn, session, index);
1823
2091
  return btn;
1824
2092
  }
1825
2093
 
1826
2094
  function renderSessions() {
1827
- sessionCount.textContent = state.loadingSessions ? '加载中...' : `${state.sessions.length} 个`;
2095
+ const containerCount = new Set(state.sessions.map(function (session) {
2096
+ return session && session.containerName ? session.containerName : '';
2097
+ }).filter(Boolean)).size;
2098
+ sessionCount.textContent = state.loadingSessions
2099
+ ? '加载中...'
2100
+ : `${state.sessions.length} 个 AGENT / ${containerCount} 个容器`;
1828
2101
 
1829
2102
  if (state.loadingSessions) {
1830
2103
  renderSessionsLoading();
@@ -1842,49 +2115,74 @@
1842
2115
  return;
1843
2116
  }
1844
2117
 
1845
- if (state.sessionRenderMode !== 'list') {
1846
- sessionList.innerHTML = '';
1847
- state.sessionNodeMap.clear();
1848
- state.sessionRenderMode = 'list';
1849
- }
1850
-
1851
- const nextNameSet = new Set();
1852
- state.sessions.forEach(function (session, index) {
1853
- nextNameSet.add(session.name);
1854
- let row = state.sessionNodeMap.get(session.name);
1855
- if (!row) {
1856
- row = createSessionRow(session, index);
1857
- state.sessionNodeMap.set(session.name, row);
1858
- } else if (row.__renderKey !== getSessionRenderKey(session)) {
1859
- updateSessionRow(row, session, index);
1860
- } else {
1861
- row.style.setProperty('--item-index', String(index));
1862
- }
2118
+ sessionList.innerHTML = '';
2119
+ state.sessionNodeMap.clear();
2120
+ state.sessionRenderMode = 'tree';
1863
2121
 
1864
- const currentAtIndex = sessionList.children[index];
1865
- if (currentAtIndex !== row) {
1866
- sessionList.insertBefore(row, currentAtIndex || null);
1867
- }
1868
- });
2122
+ const grouped = groupSessionsByDirectory(state.sessions);
2123
+ let itemIndex = 0;
1869
2124
 
1870
- const removeNames = [];
1871
- for (const existingName of state.sessionNodeMap.keys()) {
1872
- if (!nextNameSet.has(existingName)) {
1873
- removeNames.push(existingName);
1874
- }
1875
- }
1876
- removeNames.forEach(function (name) {
1877
- const row = state.sessionNodeMap.get(name);
1878
- if (row && row.parentNode === sessionList) {
1879
- sessionList.removeChild(row);
1880
- }
1881
- state.sessionNodeMap.delete(name);
1882
- });
2125
+ grouped.forEach(function (directoryGroup) {
2126
+ const group = document.createElement('section');
2127
+ group.className = 'workbench-group';
1883
2128
 
1884
- while (sessionList.children.length > state.sessions.length) {
1885
- sessionList.removeChild(sessionList.lastChild);
1886
- }
1887
- renderSessionActiveState();
2129
+ const groupHead = document.createElement('div');
2130
+ groupHead.className = 'workbench-group-head';
2131
+ groupHead.innerHTML = `
2132
+ <div class="workbench-group-kicker">目录</div>
2133
+ <div class="workbench-group-title">${escapeHtml(directoryGroup.path)}</div>
2134
+ `;
2135
+ group.appendChild(groupHead);
2136
+
2137
+ const containerStack = document.createElement('div');
2138
+ containerStack.className = 'container-stack';
2139
+
2140
+ Array.from(directoryGroup.containers.values()).forEach(function (containerGroup) {
2141
+ const status = sessionStatusInfo(containerGroup.status);
2142
+ const containerCard = document.createElement('section');
2143
+ containerCard.className = 'container-card';
2144
+
2145
+ const containerHead = document.createElement('div');
2146
+ containerHead.className = 'container-card-head';
2147
+
2148
+ const containerInfo = document.createElement('div');
2149
+ containerInfo.className = 'container-card-info';
2150
+ containerInfo.innerHTML = `
2151
+ <div class="container-card-kicker">容器</div>
2152
+ <div class="container-card-title">${escapeHtml(containerGroup.containerName)}</div>
2153
+ <div class="container-card-meta">
2154
+ <span class="session-status ${status.tone}">${escapeHtml(status.label)}</span>
2155
+ <span>${escapeHtml(formatDateTime(containerGroup.updatedAt) || '暂无更新')}</span>
2156
+ </div>
2157
+ `;
2158
+
2159
+ const addAgentBtn = document.createElement('button');
2160
+ addAgentBtn.type = 'button';
2161
+ addAgentBtn.className = 'secondary add-agent-btn';
2162
+ addAgentBtn.textContent = '新建AGENT';
2163
+ addAgentBtn.addEventListener('click', function () {
2164
+ createAgentSession(containerGroup.containerName);
2165
+ });
2166
+
2167
+ containerHead.appendChild(containerInfo);
2168
+ containerHead.appendChild(addAgentBtn);
2169
+ containerCard.appendChild(containerHead);
2170
+
2171
+ const agentList = document.createElement('div');
2172
+ agentList.className = 'agent-list';
2173
+ containerGroup.sessions.forEach(function (session) {
2174
+ const row = createAgentRow(session, itemIndex);
2175
+ state.sessionNodeMap.set(session.name, row);
2176
+ agentList.appendChild(row);
2177
+ itemIndex += 1;
2178
+ });
2179
+ containerCard.appendChild(agentList);
2180
+ containerStack.appendChild(containerCard);
2181
+ });
2182
+
2183
+ group.appendChild(containerStack);
2184
+ sessionList.appendChild(group);
2185
+ });
1888
2186
  }
1889
2187
 
1890
2188
  function renderMessagesLoading() {
@@ -2487,6 +2785,38 @@
2487
2785
  });
2488
2786
  }
2489
2787
 
2788
+ if (pickHostPathBtn) {
2789
+ pickHostPathBtn.addEventListener('click', function () {
2790
+ openDirectoryPicker('host');
2791
+ });
2792
+ }
2793
+
2794
+ if (pickContainerPathBtn) {
2795
+ pickContainerPathBtn.addEventListener('click', function () {
2796
+ openDirectoryPicker('container');
2797
+ });
2798
+ }
2799
+
2800
+ if (directoryPickerCancelBtn) {
2801
+ directoryPickerCancelBtn.addEventListener('click', function () {
2802
+ closeDirectoryPicker();
2803
+ });
2804
+ }
2805
+
2806
+ if (directoryPickerUpBtn) {
2807
+ directoryPickerUpBtn.addEventListener('click', function () {
2808
+ if (state.directoryPicker.parentPath) {
2809
+ loadDirectoryPicker(state.directoryPicker.parentPath);
2810
+ }
2811
+ });
2812
+ }
2813
+
2814
+ if (directoryPickerSelectBtn) {
2815
+ directoryPickerSelectBtn.addEventListener('click', function () {
2816
+ applyPickedDirectory();
2817
+ });
2818
+ }
2819
+
2490
2820
  if (createRun) {
2491
2821
  createRun.addEventListener('change', function () {
2492
2822
  applyCurrentRunDefaults();
@@ -2787,6 +3117,14 @@
2787
3117
  });
2788
3118
  }
2789
3119
 
3120
+ if (directoryPickerModal) {
3121
+ directoryPickerModal.addEventListener('click', function (event) {
3122
+ if (event.target === directoryPickerModal && !state.directoryPicker.loading) {
3123
+ closeDirectoryPicker();
3124
+ }
3125
+ });
3126
+ }
3127
+
2790
3128
  window.addEventListener('keydown', function (event) {
2791
3129
  if (event.key === 'Escape' && state.configModalOpen) {
2792
3130
  closeConfigModal();
@@ -2796,6 +3134,9 @@
2796
3134
  closeCreateModal();
2797
3135
  syncUi();
2798
3136
  }
3137
+ if (event.key === 'Escape' && state.directoryPicker.open) {
3138
+ closeDirectoryPicker();
3139
+ }
2799
3140
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
2800
3141
  closeMobileSessionPanel();
2801
3142
  }
@@ -2842,7 +3183,9 @@
2842
3183
  removeBtn.addEventListener('click', async function () {
2843
3184
  if (!state.active) return;
2844
3185
  closeMobileActionsMenu();
2845
- const yes = confirm('确认删除容器 ' + state.active + ' ?');
3186
+ const activeSession = getActiveSession();
3187
+ const targetContainer = activeSession && activeSession.containerName ? activeSession.containerName : state.active;
3188
+ const yes = confirm('确认删除容器 ' + targetContainer + ' ?');
2846
3189
  if (!yes) return;
2847
3190
  try {
2848
3191
  const current = state.active;
@@ -2861,7 +3204,9 @@
2861
3204
  removeAllBtn.addEventListener('click', async function () {
2862
3205
  if (!state.active) return;
2863
3206
  closeMobileActionsMenu();
2864
- const yes = confirm('确认删除对话 ' + state.active + ' ?');
3207
+ const activeSession = getActiveSession();
3208
+ const targetAgent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
3209
+ const yes = confirm('确认删除对话 ' + targetAgent + ' ?');
2865
3210
  if (!yes) return;
2866
3211
  try {
2867
3212
  const current = state.active;