@xcanwin/manyoyo 5.7.3 → 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,
@@ -121,6 +132,8 @@
121
132
  const createContainerName = document.getElementById('createContainerName');
122
133
  const createHostPath = document.getElementById('createHostPath');
123
134
  const createContainerPath = document.getElementById('createContainerPath');
135
+ const pickHostPathBtn = document.getElementById('pickHostPathBtn');
136
+ const pickContainerPathBtn = document.getElementById('pickContainerPathBtn');
124
137
  const createImageName = document.getElementById('createImageName');
125
138
  const createImageVersion = document.getElementById('createImageVersion');
126
139
  const createContainerMode = document.getElementById('createContainerMode');
@@ -132,6 +145,15 @@
132
145
  const createEnv = document.getElementById('createEnv');
133
146
  const createEnvFile = document.getElementById('createEnvFile');
134
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');
135
157
  const activeTitle = document.getElementById('activeTitle');
136
158
  const activeMeta = document.getElementById('activeMeta');
137
159
  const activityCommandBtn = document.getElementById('activityCommandBtn');
@@ -437,6 +459,32 @@
437
459
  });
438
460
  }
439
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
+
440
488
  function setModalVisible(modalNode, visible) {
441
489
  if (!modalNode) return;
442
490
  modalNode.hidden = !visible;
@@ -467,6 +515,18 @@
467
515
  configError.textContent = text;
468
516
  }
469
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
+
470
530
  function envMapToText(envMap) {
471
531
  if (!envMap || typeof envMap !== 'object') {
472
532
  return '';
@@ -794,7 +854,8 @@
794
854
  const status = sessionStatusInfo(session.status);
795
855
  const messageCount = safeMessageCount(session.messageCount);
796
856
  const updatedAt = formatDateTime(session.updatedAt) || '暂无更新';
797
- return `${status.label} · ${messageCount} 条对话 · ${updatedAt}`;
857
+ const containerName = session.containerName || '未绑定容器';
858
+ return `${containerName} · ${status.label} · ${messageCount} 条对话 · ${updatedAt}`;
798
859
  }
799
860
 
800
861
  function buildMessageMetaLines(message) {
@@ -1309,7 +1370,8 @@
1309
1370
  if (detailSummary) {
1310
1371
  detailSummary.innerHTML = '';
1311
1372
  renderKeyValueCard(detailSummary, '会话概览', [
1312
- { label: '会话', value: detail.name || state.active },
1373
+ { label: 'AGENT', value: detail.agentName || detail.name || state.active },
1374
+ { label: '容器', value: detail.containerName || '—' },
1313
1375
  { label: '状态', value: status.label, tone: status.tone },
1314
1376
  { label: '镜像', value: detail.image || applied.imageName || '—' },
1315
1377
  { label: '最近更新', value: updatedText },
@@ -1332,7 +1394,8 @@
1332
1394
  if (configSummary) {
1333
1395
  configSummary.innerHTML = '';
1334
1396
  renderKeyValueCard(configSummary, '基础配置', [
1335
- { label: 'containerName', value: applied.containerName || detail.name || state.active },
1397
+ { label: 'AGENT', value: detail.agentName || '—' },
1398
+ { label: 'containerName', value: applied.containerName || detail.containerName || '—' },
1336
1399
  { label: 'imageName', value: applied.imageName || detail.image || '—' },
1337
1400
  { label: 'imageVersion', value: applied.imageVersion || '—' },
1338
1401
  { label: 'containerMode', value: applied.containerMode || 'default' }
@@ -1406,8 +1469,9 @@
1406
1469
  commandInput.value = '';
1407
1470
  }
1408
1471
  } else {
1409
- activeTitle.textContent = state.active;
1410
- activeMeta.textContent = buildActiveMeta(getActiveSession());
1472
+ const activeSession = getActiveSession();
1473
+ activeTitle.textContent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
1474
+ activeMeta.textContent = buildActiveMeta(activeSession);
1411
1475
  }
1412
1476
 
1413
1477
  const activityTab = state.activeTab === 'activity';
@@ -1522,6 +1586,13 @@
1522
1586
  if (createModal) {
1523
1587
  createModal.hidden = !state.createModalOpen;
1524
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
+ );
1525
1596
  if (!state.active) {
1526
1597
  sendState.textContent = '未选择会话';
1527
1598
  } else if (agentMode && !agentEnabled) {
@@ -1718,6 +1789,7 @@
1718
1789
  function closeCreateModal() {
1719
1790
  state.createModalOpen = false;
1720
1791
  setModalVisible(createModal, false);
1792
+ closeDirectoryPicker();
1721
1793
  showCreateError('');
1722
1794
  }
1723
1795
 
@@ -1726,57 +1798,159 @@
1726
1798
  showCreateError('');
1727
1799
  }
1728
1800
 
1729
- function renderSessionsLoading() {
1730
- state.sessionNodeMap.clear();
1731
- state.sessionRenderMode = 'loading';
1732
- sessionList.innerHTML = '';
1733
- for (let i = 0; i < 3; i++) {
1734
- const skeleton = document.createElement('div');
1735
- skeleton.className = 'skeleton session';
1736
- 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 || '选择目录';
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;
1737
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
+ });
1738
1849
  }
1739
1850
 
1740
- function getSessionRenderKey(session) {
1741
- return [
1742
- String(session && session.name ? session.name : ''),
1743
- String(session && session.status ? session.status : ''),
1744
- String(safeMessageCount(session && session.messageCount)),
1745
- String(session && session.updatedAt ? session.updatedAt : ''),
1746
- String(session && session.image ? session.image : '')
1747
- ].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
+ }
1748
1877
  }
1749
1878
 
1750
- function renderSessionActiveState() {
1751
- for (const [name, node] of state.sessionNodeMap.entries()) {
1752
- node.classList.toggle('active', state.active === name);
1753
- }
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();
1754
1891
  }
1755
1892
 
1756
- function updateSessionRow(row, session, index) {
1757
- if (!row || !session) return;
1758
- const status = sessionStatusInfo(session.status);
1759
- row.style.setProperty('--item-index', String(index));
1760
- row.classList.toggle('active', state.active === session.name);
1761
- row.classList.toggle('history-only', status.tone === 'history');
1762
- row.classList.toggle('status-running', status.tone === 'running');
1763
- row.classList.toggle('status-stopped', status.tone === 'stopped');
1764
- row.classList.toggle('status-history', status.tone === 'history');
1765
- row.classList.toggle('status-unknown', status.tone === 'unknown');
1766
- if (row.__sessionNameNode) {
1767
- row.__sessionNameNode.textContent = session.name;
1768
- }
1769
- if (row.__statusBadgeNode) {
1770
- row.__statusBadgeNode.className = `session-status ${status.tone}`;
1771
- row.__statusBadgeNode.textContent = status.label;
1893
+ function applyPickedDirectory() {
1894
+ const picker = state.directoryPicker;
1895
+ if (!picker.currentPath) {
1896
+ return;
1772
1897
  }
1773
- if (row.__messageCountNode) {
1774
- 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() || '/';
1775
1940
  }
1776
- if (row.__timeNode) {
1777
- 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);
1778
1953
  }
1779
- row.__renderKey = getSessionRenderKey(session);
1780
1954
  }
1781
1955
 
1782
1956
  function handleSessionItemClick(sessionName) {
@@ -1807,7 +1981,7 @@
1807
1981
  }
1808
1982
  }
1809
1983
  }
1810
- renderSessionActiveState();
1984
+ renderSessions();
1811
1985
  syncUi();
1812
1986
  Promise.all([
1813
1987
  loadMessagesForSession(sessionName),
@@ -1817,49 +1991,113 @@
1817
1991
  });
1818
1992
  }
1819
1993
 
1820
- 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) {
1821
2055
  const status = sessionStatusInfo(session.status);
1822
2056
  const btn = document.createElement('button');
1823
2057
  btn.type = 'button';
1824
- btn.className = 'session-item';
2058
+ btn.className = 'agent-item';
1825
2059
  btn.dataset.sessionName = session.name;
2060
+ btn.style.setProperty('--item-index', String(index));
2061
+ btn.classList.toggle('active', state.active === session.name);
1826
2062
 
1827
2063
  const sessionName = document.createElement('div');
1828
- sessionName.className = 'session-name';
2064
+ sessionName.className = 'agent-name';
2065
+ sessionName.textContent = session.agentName || session.name;
1829
2066
 
1830
2067
  const meta = document.createElement('div');
1831
- meta.className = 'session-meta';
2068
+ meta.className = 'agent-meta';
1832
2069
 
1833
2070
  const statusBadge = document.createElement('span');
1834
2071
  statusBadge.className = `session-status ${status.tone}`;
2072
+ statusBadge.textContent = status.label;
1835
2073
 
1836
2074
  const messageCount = document.createElement('span');
1837
2075
  messageCount.className = 'session-count';
2076
+ messageCount.textContent = `${safeMessageCount(session.messageCount)} 条`;
1838
2077
 
1839
2078
  meta.appendChild(statusBadge);
1840
2079
  meta.appendChild(messageCount);
1841
2080
 
1842
2081
  const time = document.createElement('div');
1843
- time.className = 'session-time';
2082
+ time.className = 'agent-time';
2083
+ time.textContent = formatDateTime(session.updatedAt) || '暂无更新';
1844
2084
 
1845
2085
  btn.appendChild(sessionName);
1846
2086
  btn.appendChild(meta);
1847
2087
  btn.appendChild(time);
1848
- btn.__sessionNameNode = sessionName;
1849
- btn.__statusBadgeNode = statusBadge;
1850
- btn.__messageCountNode = messageCount;
1851
- btn.__timeNode = time;
1852
-
1853
2088
  btn.addEventListener('click', function () {
1854
2089
  handleSessionItemClick(btn.dataset.sessionName || '');
1855
2090
  });
1856
-
1857
- updateSessionRow(btn, session, index);
1858
2091
  return btn;
1859
2092
  }
1860
2093
 
1861
2094
  function renderSessions() {
1862
- 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} 个容器`;
1863
2101
 
1864
2102
  if (state.loadingSessions) {
1865
2103
  renderSessionsLoading();
@@ -1877,49 +2115,74 @@
1877
2115
  return;
1878
2116
  }
1879
2117
 
1880
- if (state.sessionRenderMode !== 'list') {
1881
- sessionList.innerHTML = '';
1882
- state.sessionNodeMap.clear();
1883
- state.sessionRenderMode = 'list';
1884
- }
1885
-
1886
- const nextNameSet = new Set();
1887
- state.sessions.forEach(function (session, index) {
1888
- nextNameSet.add(session.name);
1889
- let row = state.sessionNodeMap.get(session.name);
1890
- if (!row) {
1891
- row = createSessionRow(session, index);
1892
- state.sessionNodeMap.set(session.name, row);
1893
- } else if (row.__renderKey !== getSessionRenderKey(session)) {
1894
- updateSessionRow(row, session, index);
1895
- } else {
1896
- row.style.setProperty('--item-index', String(index));
1897
- }
2118
+ sessionList.innerHTML = '';
2119
+ state.sessionNodeMap.clear();
2120
+ state.sessionRenderMode = 'tree';
1898
2121
 
1899
- const currentAtIndex = sessionList.children[index];
1900
- if (currentAtIndex !== row) {
1901
- sessionList.insertBefore(row, currentAtIndex || null);
1902
- }
1903
- });
2122
+ const grouped = groupSessionsByDirectory(state.sessions);
2123
+ let itemIndex = 0;
1904
2124
 
1905
- const removeNames = [];
1906
- for (const existingName of state.sessionNodeMap.keys()) {
1907
- if (!nextNameSet.has(existingName)) {
1908
- removeNames.push(existingName);
1909
- }
1910
- }
1911
- removeNames.forEach(function (name) {
1912
- const row = state.sessionNodeMap.get(name);
1913
- if (row && row.parentNode === sessionList) {
1914
- sessionList.removeChild(row);
1915
- }
1916
- state.sessionNodeMap.delete(name);
1917
- });
2125
+ grouped.forEach(function (directoryGroup) {
2126
+ const group = document.createElement('section');
2127
+ group.className = 'workbench-group';
1918
2128
 
1919
- while (sessionList.children.length > state.sessions.length) {
1920
- sessionList.removeChild(sessionList.lastChild);
1921
- }
1922
- 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
+ });
1923
2186
  }
1924
2187
 
1925
2188
  function renderMessagesLoading() {
@@ -2522,6 +2785,38 @@
2522
2785
  });
2523
2786
  }
2524
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
+
2525
2820
  if (createRun) {
2526
2821
  createRun.addEventListener('change', function () {
2527
2822
  applyCurrentRunDefaults();
@@ -2822,6 +3117,14 @@
2822
3117
  });
2823
3118
  }
2824
3119
 
3120
+ if (directoryPickerModal) {
3121
+ directoryPickerModal.addEventListener('click', function (event) {
3122
+ if (event.target === directoryPickerModal && !state.directoryPicker.loading) {
3123
+ closeDirectoryPicker();
3124
+ }
3125
+ });
3126
+ }
3127
+
2825
3128
  window.addEventListener('keydown', function (event) {
2826
3129
  if (event.key === 'Escape' && state.configModalOpen) {
2827
3130
  closeConfigModal();
@@ -2831,6 +3134,9 @@
2831
3134
  closeCreateModal();
2832
3135
  syncUi();
2833
3136
  }
3137
+ if (event.key === 'Escape' && state.directoryPicker.open) {
3138
+ closeDirectoryPicker();
3139
+ }
2834
3140
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
2835
3141
  closeMobileSessionPanel();
2836
3142
  }
@@ -2877,7 +3183,9 @@
2877
3183
  removeBtn.addEventListener('click', async function () {
2878
3184
  if (!state.active) return;
2879
3185
  closeMobileActionsMenu();
2880
- const yes = confirm('确认删除容器 ' + state.active + ' ?');
3186
+ const activeSession = getActiveSession();
3187
+ const targetContainer = activeSession && activeSession.containerName ? activeSession.containerName : state.active;
3188
+ const yes = confirm('确认删除容器 ' + targetContainer + ' ?');
2881
3189
  if (!yes) return;
2882
3190
  try {
2883
3191
  const current = state.active;
@@ -2896,7 +3204,9 @@
2896
3204
  removeAllBtn.addEventListener('click', async function () {
2897
3205
  if (!state.active) return;
2898
3206
  closeMobileActionsMenu();
2899
- const yes = confirm('确认删除对话 ' + state.active + ' ?');
3207
+ const activeSession = getActiveSession();
3208
+ const targetAgent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
3209
+ const yes = confirm('确认删除对话 ' + targetAgent + ' ?');
2900
3210
  if (!yes) return;
2901
3211
  try {
2902
3212
  const current = state.active;