@xcanwin/manyoyo 5.7.3 → 5.7.6

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,23 @@
63
63
  createRuns: {},
64
64
  sessionNodeMap: new Map(),
65
65
  sessionRenderMode: 'empty',
66
+ sidebarTreeLoaded: false,
67
+ sidebarTree: {
68
+ directories: {},
69
+ containers: {}
70
+ },
71
+ pendingActiveSessionScroll: false,
72
+ directoryPicker: {
73
+ open: false,
74
+ loading: false,
75
+ mode: '',
76
+ title: '',
77
+ tip: '',
78
+ currentPath: '',
79
+ basePath: '',
80
+ entries: [],
81
+ error: ''
82
+ },
66
83
  messageRequestId: 0,
67
84
  agentRun: {
68
85
  active: false,
@@ -99,6 +116,7 @@
99
116
  const viewDetailBtn = document.getElementById('viewDetailBtn');
100
117
  const viewConfigBtn = document.getElementById('viewConfigBtn');
101
118
  const viewCheckBtn = document.getElementById('viewCheckBtn');
119
+ const addAgentBtn = document.getElementById('addAgentBtn');
102
120
  const mobileSidebarClose = document.getElementById('mobileSidebarClose');
103
121
  const sidebarBackdrop = document.getElementById('sidebarBackdrop');
104
122
  const openConfigBtn = document.getElementById('openConfigBtn');
@@ -121,6 +139,8 @@
121
139
  const createContainerName = document.getElementById('createContainerName');
122
140
  const createHostPath = document.getElementById('createHostPath');
123
141
  const createContainerPath = document.getElementById('createContainerPath');
142
+ const pickHostPathBtn = document.getElementById('pickHostPathBtn');
143
+ const pickContainerPathBtn = document.getElementById('pickContainerPathBtn');
124
144
  const createImageName = document.getElementById('createImageName');
125
145
  const createImageVersion = document.getElementById('createImageVersion');
126
146
  const createContainerMode = document.getElementById('createContainerMode');
@@ -132,6 +152,15 @@
132
152
  const createEnv = document.getElementById('createEnv');
133
153
  const createEnvFile = document.getElementById('createEnvFile');
134
154
  const createVolumes = document.getElementById('createVolumes');
155
+ const directoryPickerModal = document.getElementById('directoryPickerModal');
156
+ const directoryPickerTitle = document.getElementById('directoryPickerTitle');
157
+ const directoryPickerTip = document.getElementById('directoryPickerTip');
158
+ const directoryPickerCurrent = document.getElementById('directoryPickerCurrent');
159
+ const directoryPickerList = document.getElementById('directoryPickerList');
160
+ const directoryPickerError = document.getElementById('directoryPickerError');
161
+ const directoryPickerCancelBtn = document.getElementById('directoryPickerCancelBtn');
162
+ const directoryPickerUpBtn = document.getElementById('directoryPickerUpBtn');
163
+ const directoryPickerSelectBtn = document.getElementById('directoryPickerSelectBtn');
135
164
  const activeTitle = document.getElementById('activeTitle');
136
165
  const activeMeta = document.getElementById('activeMeta');
137
166
  const activityCommandBtn = document.getElementById('activityCommandBtn');
@@ -183,12 +212,63 @@
183
212
  const GEMINI_YOLO_FLAG = '--yolo';
184
213
  const CODEX_DANGEROUS_FLAG = '--dangerously-bypass-approvals-and-sandbox';
185
214
  const OPENCODE_PERMISSION_KEY = 'OPENCODE_PERMISSION=';
215
+ const SIDEBAR_TREE_STORAGE_KEY = 'manyoyo.web.sidebarTree.v1';
186
216
  const markdownRenderer = window.ManyoyoMarkdown
187
217
  && typeof window.ManyoyoMarkdown.shouldRenderMessage === 'function'
188
218
  && typeof window.ManyoyoMarkdown.render === 'function'
189
219
  ? window.ManyoyoMarkdown
190
220
  : null;
191
221
 
222
+ function normalizeBooleanMap(source) {
223
+ const result = {};
224
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
225
+ return result;
226
+ }
227
+ Object.keys(source).forEach(function (key) {
228
+ if (typeof source[key] === 'boolean') {
229
+ result[String(key)] = source[key];
230
+ }
231
+ });
232
+ return result;
233
+ }
234
+
235
+ function loadSidebarTreeState() {
236
+ if (state.sidebarTreeLoaded) {
237
+ return;
238
+ }
239
+ state.sidebarTreeLoaded = true;
240
+ try {
241
+ if (!window.localStorage) {
242
+ return;
243
+ }
244
+ const raw = window.localStorage.getItem(SIDEBAR_TREE_STORAGE_KEY);
245
+ if (!raw) {
246
+ return;
247
+ }
248
+ const parsed = JSON.parse(raw);
249
+ state.sidebarTree = {
250
+ directories: normalizeBooleanMap(parsed && parsed.directories),
251
+ containers: normalizeBooleanMap(parsed && parsed.containers)
252
+ };
253
+ } catch (e) {
254
+ state.sidebarTree = {
255
+ directories: {},
256
+ containers: {}
257
+ };
258
+ }
259
+ }
260
+
261
+ function persistSidebarTreeState() {
262
+ try {
263
+ if (!window.localStorage) {
264
+ return;
265
+ }
266
+ window.localStorage.setItem(SIDEBAR_TREE_STORAGE_KEY, JSON.stringify(state.sidebarTree));
267
+ } catch (e) {
268
+ // 忽略浏览器存储异常,避免影响主流程
269
+ }
270
+ }
271
+
192
272
  function appendPlainMessageContent(bubble, content) {
193
273
  const pre = document.createElement('pre');
194
274
  pre.textContent = content == null ? '' : String(content);
@@ -390,6 +470,9 @@
390
470
  function roleName(role, message) {
391
471
  if (role === 'user') return '我';
392
472
  if (role === 'assistant') {
473
+ if (message && message.streamingReply) {
474
+ return 'AGENT 实时回复';
475
+ }
393
476
  if (message && message.streamTrace) {
394
477
  return 'AGENT 过程';
395
478
  }
@@ -437,6 +520,158 @@
437
520
  });
438
521
  }
439
522
 
523
+ function getSessionDirectoryPath(session) {
524
+ return String(session && session.hostPath ? session.hostPath : '').trim() || '未配置目录';
525
+ }
526
+
527
+ function getSessionContainerName(session) {
528
+ return String(session && session.containerName ? session.containerName : '').trim();
529
+ }
530
+
531
+ function findSessionByName(sessionName) {
532
+ const target = String(sessionName || '').trim();
533
+ if (!target) {
534
+ return null;
535
+ }
536
+ return state.sessions.find(function (session) {
537
+ return session && session.name === target;
538
+ }) || null;
539
+ }
540
+
541
+ function pruneSidebarTreeState() {
542
+ loadSidebarTreeState();
543
+
544
+ const validDirectories = new Set(state.sessions.map(getSessionDirectoryPath));
545
+ const validContainers = new Set(state.sessions.map(getSessionContainerName).filter(Boolean));
546
+ let changed = false;
547
+
548
+ Object.keys(state.sidebarTree.directories).forEach(function (key) {
549
+ if (!validDirectories.has(key)) {
550
+ delete state.sidebarTree.directories[key];
551
+ changed = true;
552
+ }
553
+ });
554
+
555
+ Object.keys(state.sidebarTree.containers).forEach(function (key) {
556
+ if (!validContainers.has(key)) {
557
+ delete state.sidebarTree.containers[key];
558
+ changed = true;
559
+ }
560
+ });
561
+
562
+ if (changed) {
563
+ persistSidebarTreeState();
564
+ }
565
+ }
566
+
567
+ function setSidebarDirectoryExpanded(directoryPath, expanded, options) {
568
+ const path = String(directoryPath || '').trim();
569
+ if (!path) {
570
+ return false;
571
+ }
572
+ loadSidebarTreeState();
573
+ const opts = options && typeof options === 'object' ? options : {};
574
+ if (state.sidebarTree.directories[path] === expanded) {
575
+ return false;
576
+ }
577
+ state.sidebarTree.directories[path] = expanded;
578
+ if (opts.persist !== false) {
579
+ persistSidebarTreeState();
580
+ }
581
+ return true;
582
+ }
583
+
584
+ function setSidebarContainerExpanded(containerName, expanded, options) {
585
+ const name = String(containerName || '').trim();
586
+ if (!name) {
587
+ return false;
588
+ }
589
+ loadSidebarTreeState();
590
+ const opts = options && typeof options === 'object' ? options : {};
591
+ if (state.sidebarTree.containers[name] === expanded) {
592
+ return false;
593
+ }
594
+ state.sidebarTree.containers[name] = expanded;
595
+ if (opts.persist !== false) {
596
+ persistSidebarTreeState();
597
+ }
598
+ return true;
599
+ }
600
+
601
+ function ensureSessionPathExpanded(sessionName, options) {
602
+ const session = findSessionByName(sessionName);
603
+ if (!session) {
604
+ return false;
605
+ }
606
+ const opts = options && typeof options === 'object' ? options : {};
607
+ const directoryPath = getSessionDirectoryPath(session);
608
+ const containerName = getSessionContainerName(session);
609
+ const changedDirectory = setSidebarDirectoryExpanded(directoryPath, true, { persist: false });
610
+ const changedContainer = setSidebarContainerExpanded(containerName, true, { persist: false });
611
+ if ((changedDirectory || changedContainer) && opts.persist !== false) {
612
+ persistSidebarTreeState();
613
+ }
614
+ return changedDirectory || changedContainer;
615
+ }
616
+
617
+ function directoryContainsActiveSession(directoryGroup) {
618
+ const groups = directoryGroup && Array.isArray(directoryGroup.containers) ? directoryGroup.containers : [];
619
+ return groups.some(function (containerGroup) {
620
+ return containerContainsActiveSession(containerGroup);
621
+ });
622
+ }
623
+
624
+ function containerContainsActiveSession(containerGroup) {
625
+ const sessions = containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions : [];
626
+ return sessions.some(function (session) {
627
+ return session && session.name === state.active;
628
+ });
629
+ }
630
+
631
+ function isDirectoryExpanded(directoryGroup) {
632
+ loadSidebarTreeState();
633
+ const key = String(directoryGroup && directoryGroup.path ? directoryGroup.path : '').trim();
634
+ if (key && typeof state.sidebarTree.directories[key] === 'boolean') {
635
+ return state.sidebarTree.directories[key];
636
+ }
637
+ return false;
638
+ }
639
+
640
+ function isContainerExpanded(containerGroup) {
641
+ loadSidebarTreeState();
642
+ const key = String(containerGroup && containerGroup.containerName ? containerGroup.containerName : '').trim();
643
+ if (key && typeof state.sidebarTree.containers[key] === 'boolean') {
644
+ return state.sidebarTree.containers[key];
645
+ }
646
+ return false;
647
+ }
648
+
649
+ function normalizeSlashPath(value) {
650
+ return String(value || '').replace(/\\/g, '/');
651
+ }
652
+
653
+ function isChildPath(basePath, targetPath) {
654
+ const normalizedBase = normalizeSlashPath(basePath).replace(/\/+$/, '');
655
+ const normalizedTarget = normalizeSlashPath(targetPath).replace(/\/+$/, '');
656
+ if (!normalizedBase) {
657
+ return false;
658
+ }
659
+ return normalizedTarget === normalizedBase || normalizedTarget.startsWith(normalizedBase + '/');
660
+ }
661
+
662
+ function buildContainerPathFromHostSelection(baseHostPath, baseContainerPath, selectedHostPath) {
663
+ const normalizedBaseHost = normalizeSlashPath(baseHostPath).replace(/\/+$/, '');
664
+ const normalizedContainer = normalizeSlashPath(baseContainerPath).replace(/\/+$/, '') || '/workspace';
665
+ const normalizedSelected = normalizeSlashPath(selectedHostPath).replace(/\/+$/, '');
666
+ if (!normalizedBaseHost || !isChildPath(normalizedBaseHost, normalizedSelected)) {
667
+ return normalizedContainer;
668
+ }
669
+ const relative = normalizedSelected === normalizedBaseHost
670
+ ? ''
671
+ : normalizedSelected.slice(normalizedBaseHost.length + 1);
672
+ return relative ? `${normalizedContainer}/${relative}`.replace(/\/+/g, '/') : normalizedContainer;
673
+ }
674
+
440
675
  function setModalVisible(modalNode, visible) {
441
676
  if (!modalNode) return;
442
677
  modalNode.hidden = !visible;
@@ -467,6 +702,18 @@
467
702
  configError.textContent = text;
468
703
  }
469
704
 
705
+ function showDirectoryPickerError(message) {
706
+ if (!directoryPickerError) return;
707
+ const text = String(message || '').trim();
708
+ if (!text) {
709
+ directoryPickerError.hidden = true;
710
+ directoryPickerError.textContent = '';
711
+ return;
712
+ }
713
+ directoryPickerError.hidden = false;
714
+ directoryPickerError.textContent = text;
715
+ }
716
+
470
717
  function envMapToText(envMap) {
471
718
  if (!envMap || typeof envMap !== 'object') {
472
719
  return '';
@@ -794,7 +1041,8 @@
794
1041
  const status = sessionStatusInfo(session.status);
795
1042
  const messageCount = safeMessageCount(session.messageCount);
796
1043
  const updatedAt = formatDateTime(session.updatedAt) || '暂无更新';
797
- return `${status.label} · ${messageCount} 条对话 · ${updatedAt}`;
1044
+ const containerName = session.containerName || '未绑定容器';
1045
+ return `${containerName} · ${status.label} · ${messageCount} 条对话 · ${updatedAt}`;
798
1046
  }
799
1047
 
800
1048
  function buildMessageMetaLines(message) {
@@ -1309,7 +1557,8 @@
1309
1557
  if (detailSummary) {
1310
1558
  detailSummary.innerHTML = '';
1311
1559
  renderKeyValueCard(detailSummary, '会话概览', [
1312
- { label: '会话', value: detail.name || state.active },
1560
+ { label: 'AGENT', value: detail.agentName || detail.name || state.active },
1561
+ { label: '容器', value: detail.containerName || '—' },
1313
1562
  { label: '状态', value: status.label, tone: status.tone },
1314
1563
  { label: '镜像', value: detail.image || applied.imageName || '—' },
1315
1564
  { label: '最近更新', value: updatedText },
@@ -1332,7 +1581,8 @@
1332
1581
  if (configSummary) {
1333
1582
  configSummary.innerHTML = '';
1334
1583
  renderKeyValueCard(configSummary, '基础配置', [
1335
- { label: 'containerName', value: applied.containerName || detail.name || state.active },
1584
+ { label: 'AGENT', value: detail.agentName || '—' },
1585
+ { label: 'containerName', value: applied.containerName || detail.containerName || '—' },
1336
1586
  { label: 'imageName', value: applied.imageName || detail.image || '—' },
1337
1587
  { label: 'imageVersion', value: applied.imageVersion || '—' },
1338
1588
  { label: 'containerMode', value: applied.containerMode || 'default' }
@@ -1406,8 +1656,9 @@
1406
1656
  commandInput.value = '';
1407
1657
  }
1408
1658
  } else {
1409
- activeTitle.textContent = state.active;
1410
- activeMeta.textContent = buildActiveMeta(getActiveSession());
1659
+ const activeSession = getActiveSession();
1660
+ activeTitle.textContent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
1661
+ activeMeta.textContent = buildActiveMeta(activeSession);
1411
1662
  }
1412
1663
 
1413
1664
  const activityTab = state.activeTab === 'activity';
@@ -1460,6 +1711,9 @@
1460
1711
  const activeAgentRunning = isAgentRunActiveForSession(state.active);
1461
1712
  const busy = state.loadingSessions || state.loadingMessages || state.sending;
1462
1713
  refreshBtn.disabled = busy;
1714
+ if (addAgentBtn) {
1715
+ addAgentBtn.disabled = !state.active || busy;
1716
+ }
1463
1717
  removeBtn.disabled = !state.active || busy;
1464
1718
  removeAllBtn.disabled = !state.active || busy;
1465
1719
  sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && !agentEnabled);
@@ -1522,6 +1776,13 @@
1522
1776
  if (createModal) {
1523
1777
  createModal.hidden = !state.createModalOpen;
1524
1778
  }
1779
+ if (directoryPickerModal) {
1780
+ directoryPickerModal.hidden = !state.directoryPicker.open;
1781
+ }
1782
+ document.body.classList.toggle(
1783
+ 'modal-open',
1784
+ state.configModalOpen || state.createModalOpen || state.directoryPicker.open
1785
+ );
1525
1786
  if (!state.active) {
1526
1787
  sendState.textContent = '未选择会话';
1527
1788
  } else if (agentMode && !agentEnabled) {
@@ -1718,6 +1979,7 @@
1718
1979
  function closeCreateModal() {
1719
1980
  state.createModalOpen = false;
1720
1981
  setModalVisible(createModal, false);
1982
+ closeDirectoryPicker();
1721
1983
  showCreateError('');
1722
1984
  }
1723
1985
 
@@ -1726,57 +1988,159 @@
1726
1988
  showCreateError('');
1727
1989
  }
1728
1990
 
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);
1991
+ function renderDirectoryPicker() {
1992
+ if (!directoryPickerModal) return;
1993
+ const picker = state.directoryPicker;
1994
+ setModalVisible(directoryPickerModal, picker.open);
1995
+ if (directoryPickerTitle) {
1996
+ directoryPickerTitle.textContent = picker.title || '选择目录';
1997
+ }
1998
+ if (directoryPickerTip) {
1999
+ directoryPickerTip.textContent = picker.tip || '';
2000
+ }
2001
+ if (directoryPickerCurrent) {
2002
+ directoryPickerCurrent.textContent = picker.currentPath || '未选择目录';
2003
+ }
2004
+ showDirectoryPickerError(picker.error);
2005
+ if (directoryPickerUpBtn) {
2006
+ directoryPickerUpBtn.disabled = picker.loading || !picker.currentPath || !picker.parentPath;
2007
+ }
2008
+ if (directoryPickerSelectBtn) {
2009
+ directoryPickerSelectBtn.disabled = picker.loading || !picker.currentPath;
2010
+ }
2011
+ if (!directoryPickerList) {
2012
+ return;
2013
+ }
2014
+ directoryPickerList.innerHTML = '';
2015
+ if (picker.loading) {
2016
+ const loading = document.createElement('div');
2017
+ loading.className = 'empty';
2018
+ loading.textContent = '目录加载中...';
2019
+ directoryPickerList.appendChild(loading);
2020
+ return;
1737
2021
  }
2022
+ if (!picker.entries.length) {
2023
+ const empty = document.createElement('div');
2024
+ empty.className = 'empty';
2025
+ empty.textContent = '当前目录下没有可选子目录';
2026
+ directoryPickerList.appendChild(empty);
2027
+ return;
2028
+ }
2029
+ picker.entries.forEach(function (entry) {
2030
+ const btn = document.createElement('button');
2031
+ btn.type = 'button';
2032
+ btn.className = 'dir-picker-item secondary';
2033
+ btn.textContent = entry.name;
2034
+ btn.addEventListener('click', function () {
2035
+ loadDirectoryPicker(entry.path);
2036
+ });
2037
+ directoryPickerList.appendChild(btn);
2038
+ });
1738
2039
  }
1739
2040
 
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('|');
2041
+ async function loadDirectoryPicker(targetPath) {
2042
+ const picker = state.directoryPicker;
2043
+ picker.loading = true;
2044
+ picker.error = '';
2045
+ if (targetPath) {
2046
+ picker.currentPath = targetPath;
2047
+ }
2048
+ renderDirectoryPicker();
2049
+ try {
2050
+ const params = new URLSearchParams();
2051
+ params.set('path', picker.currentPath || '/');
2052
+ if (picker.basePath) {
2053
+ params.set('basePath', picker.basePath);
2054
+ }
2055
+ const data = await api('/api/fs/directories?' + params.toString());
2056
+ picker.currentPath = data.currentPath || picker.currentPath;
2057
+ picker.basePath = data.basePath || picker.basePath || '';
2058
+ picker.parentPath = data.parentPath || '';
2059
+ picker.entries = Array.isArray(data.entries) ? data.entries : [];
2060
+ } catch (e) {
2061
+ picker.error = e && e.message ? e.message : '目录加载失败';
2062
+ picker.entries = [];
2063
+ } finally {
2064
+ picker.loading = false;
2065
+ renderDirectoryPicker();
2066
+ }
1748
2067
  }
1749
2068
 
1750
- function renderSessionActiveState() {
1751
- for (const [name, node] of state.sessionNodeMap.entries()) {
1752
- node.classList.toggle('active', state.active === name);
1753
- }
2069
+ function closeDirectoryPicker() {
2070
+ state.directoryPicker.open = false;
2071
+ state.directoryPicker.loading = false;
2072
+ state.directoryPicker.mode = '';
2073
+ state.directoryPicker.title = '';
2074
+ state.directoryPicker.tip = '';
2075
+ state.directoryPicker.currentPath = '';
2076
+ state.directoryPicker.basePath = '';
2077
+ state.directoryPicker.parentPath = '';
2078
+ state.directoryPicker.entries = [];
2079
+ state.directoryPicker.error = '';
2080
+ renderDirectoryPicker();
1754
2081
  }
1755
2082
 
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;
2083
+ function applyPickedDirectory() {
2084
+ const picker = state.directoryPicker;
2085
+ if (!picker.currentPath) {
2086
+ return;
1772
2087
  }
1773
- if (row.__messageCountNode) {
1774
- row.__messageCountNode.textContent = `${safeMessageCount(session.messageCount)} 条`;
2088
+ if (picker.mode === 'host') {
2089
+ createHostPath.value = picker.currentPath;
2090
+ if (!(createContainerPath.value || '').trim()) {
2091
+ createContainerPath.value = '/workspace';
2092
+ }
2093
+ } else if (picker.mode === 'container') {
2094
+ const mapped = buildContainerPathFromHostSelection(
2095
+ picker.basePath,
2096
+ (createContainerPath.value || '').trim() || '/workspace',
2097
+ picker.currentPath
2098
+ );
2099
+ createContainerPath.value = mapped;
2100
+ }
2101
+ closeDirectoryPicker();
2102
+ }
2103
+
2104
+ function openDirectoryPicker(mode) {
2105
+ const picker = state.directoryPicker;
2106
+ picker.open = true;
2107
+ picker.loading = false;
2108
+ picker.error = '';
2109
+ picker.entries = [];
2110
+ picker.parentPath = '';
2111
+ if (mode === 'container') {
2112
+ const baseHostPath = (createHostPath.value || '').trim();
2113
+ if (!baseHostPath) {
2114
+ showCreateError('请先选择 hostPath,再选择 containerPath。');
2115
+ picker.open = false;
2116
+ renderDirectoryPicker();
2117
+ return;
2118
+ }
2119
+ picker.mode = 'container';
2120
+ picker.title = '选择 containerPath 对应目录';
2121
+ picker.tip = '从 hostPath 下选择子目录,结果会映射到容器路径。';
2122
+ picker.basePath = baseHostPath;
2123
+ picker.currentPath = baseHostPath;
2124
+ } else {
2125
+ picker.mode = 'host';
2126
+ picker.title = '选择 hostPath';
2127
+ picker.tip = '浏览宿主机目录,选中后会回填 create 表单。';
2128
+ picker.basePath = '';
2129
+ picker.currentPath = (createHostPath.value || '').trim() || '/';
1775
2130
  }
1776
- if (row.__timeNode) {
1777
- row.__timeNode.textContent = formatDateTime(session.updatedAt) || '暂无更新';
2131
+ renderDirectoryPicker();
2132
+ loadDirectoryPicker(picker.currentPath);
2133
+ }
2134
+
2135
+ function renderSessionsLoading() {
2136
+ state.sessionNodeMap.clear();
2137
+ state.sessionRenderMode = 'loading';
2138
+ sessionList.innerHTML = '';
2139
+ for (let i = 0; i < 3; i++) {
2140
+ const skeleton = document.createElement('div');
2141
+ skeleton.className = 'skeleton session';
2142
+ sessionList.appendChild(skeleton);
1778
2143
  }
1779
- row.__renderKey = getSessionRenderKey(session);
1780
2144
  }
1781
2145
 
1782
2146
  function handleSessionItemClick(sessionName) {
@@ -1792,6 +2156,7 @@
1792
2156
  disconnectTerminal('会话切换,终端已断开', true);
1793
2157
  }
1794
2158
  state.active = sessionName;
2159
+ ensureSessionPathExpanded(sessionName);
1795
2160
  state.sessionDetail = null;
1796
2161
  state.sessionDetailError = '';
1797
2162
  if (isMobileLayout()) {
@@ -1807,7 +2172,7 @@
1807
2172
  }
1808
2173
  }
1809
2174
  }
1810
- renderSessionActiveState();
2175
+ renderSessions();
1811
2176
  syncUi();
1812
2177
  Promise.all([
1813
2178
  loadMessagesForSession(sessionName),
@@ -1817,49 +2182,187 @@
1817
2182
  });
1818
2183
  }
1819
2184
 
1820
- function createSessionRow(session, index) {
2185
+ async function createAgentSession(containerName) {
2186
+ const targetContainer = String(containerName || '').trim();
2187
+ if (!targetContainer) {
2188
+ return;
2189
+ }
2190
+ try {
2191
+ const data = await api('/api/sessions/' + encodeURIComponent(targetContainer) + '/agents', {
2192
+ method: 'POST',
2193
+ body: JSON.stringify({})
2194
+ });
2195
+ state.activeTab = 'activity';
2196
+ state.mode = 'agent';
2197
+ await loadSessions(data.name);
2198
+ if (isMobileLayout()) {
2199
+ closeMobileSessionPanel();
2200
+ }
2201
+ } catch (e) {
2202
+ alert(e.message);
2203
+ }
2204
+ }
2205
+
2206
+ function groupSessionsByDirectory(sessions) {
2207
+ const groups = new Map();
2208
+ (Array.isArray(sessions) ? sessions : []).forEach(function (session) {
2209
+ const directoryPath = String(session && session.hostPath ? session.hostPath : '').trim() || '未配置目录';
2210
+ if (!groups.has(directoryPath)) {
2211
+ groups.set(directoryPath, {
2212
+ path: directoryPath,
2213
+ updatedAt: session && session.updatedAt ? session.updatedAt : '',
2214
+ containers: new Map()
2215
+ });
2216
+ }
2217
+ const directoryGroup = groups.get(directoryPath);
2218
+ if (session && session.updatedAt && (!directoryGroup.updatedAt || new Date(session.updatedAt).getTime() > new Date(directoryGroup.updatedAt).getTime())) {
2219
+ directoryGroup.updatedAt = session.updatedAt;
2220
+ }
2221
+
2222
+ const containerName = String(session && session.containerName ? session.containerName : '');
2223
+ if (!directoryGroup.containers.has(containerName)) {
2224
+ directoryGroup.containers.set(containerName, {
2225
+ containerName: containerName,
2226
+ status: session && session.status ? session.status : 'history',
2227
+ image: session && session.image ? session.image : '',
2228
+ updatedAt: session && session.updatedAt ? session.updatedAt : '',
2229
+ sessions: []
2230
+ });
2231
+ }
2232
+ const containerGroup = directoryGroup.containers.get(containerName);
2233
+ containerGroup.sessions.push(session);
2234
+ if (session && session.updatedAt && (!containerGroup.updatedAt || new Date(session.updatedAt).getTime() > new Date(containerGroup.updatedAt).getTime())) {
2235
+ containerGroup.updatedAt = session.updatedAt;
2236
+ }
2237
+ });
2238
+ return Array.from(groups.values()).map(function (group) {
2239
+ group.containers = Array.from(group.containers.values()).sort(function (a, b) {
2240
+ const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2241
+ const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
2242
+ return timeB - timeA;
2243
+ });
2244
+ group.containers.forEach(function (containerGroup) {
2245
+ containerGroup.sessions.sort(function (a, b) {
2246
+ const timeA = a && a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2247
+ const timeB = b && b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
2248
+ return timeB - timeA;
2249
+ });
2250
+ });
2251
+ return group;
2252
+ }).sort(function (a, b) {
2253
+ const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2254
+ const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
2255
+ return timeB - timeA;
2256
+ });
2257
+ }
2258
+
2259
+ function createTreeMetaText(parts) {
2260
+ return (parts || []).filter(Boolean).join(' · ');
2261
+ }
2262
+
2263
+ function createTreeToggle(options) {
2264
+ const opts = options && typeof options === 'object' ? options : {};
2265
+ const button = document.createElement('button');
2266
+ button.type = 'button';
2267
+ button.className = `tree-toggle ${opts.className || ''}`.trim();
2268
+ button.setAttribute('aria-expanded', opts.expanded ? 'true' : 'false');
2269
+
2270
+ const main = document.createElement('div');
2271
+ main.className = 'tree-toggle-main';
2272
+
2273
+ const kicker = document.createElement('div');
2274
+ kicker.className = opts.kickerClassName || 'workbench-group-kicker';
2275
+ kicker.textContent = opts.kicker || '';
2276
+ main.appendChild(kicker);
2277
+
2278
+ const title = document.createElement('div');
2279
+ title.className = opts.titleClassName || 'workbench-group-title';
2280
+ title.textContent = opts.title || '';
2281
+ main.appendChild(title);
2282
+
2283
+ const metaText = createTreeMetaText(opts.metaParts);
2284
+ if (metaText) {
2285
+ const meta = document.createElement('div');
2286
+ meta.className = 'tree-toggle-meta';
2287
+ meta.textContent = metaText;
2288
+ main.appendChild(meta);
2289
+ }
2290
+
2291
+ const caret = document.createElement('span');
2292
+ caret.className = 'tree-toggle-caret';
2293
+ caret.setAttribute('aria-hidden', 'true');
2294
+ caret.textContent = '›';
2295
+
2296
+ button.appendChild(main);
2297
+ button.appendChild(caret);
2298
+
2299
+ if (typeof opts.onClick === 'function') {
2300
+ button.addEventListener('click', opts.onClick);
2301
+ }
2302
+
2303
+ return button;
2304
+ }
2305
+
2306
+ function createAgentRow(session, index) {
1821
2307
  const status = sessionStatusInfo(session.status);
1822
2308
  const btn = document.createElement('button');
1823
2309
  btn.type = 'button';
1824
- btn.className = 'session-item';
2310
+ btn.className = 'agent-item';
1825
2311
  btn.dataset.sessionName = session.name;
2312
+ btn.style.setProperty('--item-index', String(index));
2313
+ btn.classList.toggle('active', state.active === session.name);
1826
2314
 
1827
2315
  const sessionName = document.createElement('div');
1828
- sessionName.className = 'session-name';
2316
+ sessionName.className = 'agent-name';
2317
+ sessionName.textContent = session.agentName || session.name;
1829
2318
 
1830
2319
  const meta = document.createElement('div');
1831
- meta.className = 'session-meta';
2320
+ meta.className = 'agent-meta';
1832
2321
 
1833
2322
  const statusBadge = document.createElement('span');
1834
2323
  statusBadge.className = `session-status ${status.tone}`;
2324
+ statusBadge.textContent = status.label;
1835
2325
 
1836
2326
  const messageCount = document.createElement('span');
1837
2327
  messageCount.className = 'session-count';
2328
+ messageCount.textContent = `${safeMessageCount(session.messageCount)} 条`;
1838
2329
 
1839
2330
  meta.appendChild(statusBadge);
1840
2331
  meta.appendChild(messageCount);
1841
2332
 
1842
2333
  const time = document.createElement('div');
1843
- time.className = 'session-time';
2334
+ time.className = 'agent-time';
2335
+ time.textContent = formatDateTime(session.updatedAt) || '暂无更新';
1844
2336
 
1845
2337
  btn.appendChild(sessionName);
1846
2338
  btn.appendChild(meta);
1847
2339
  btn.appendChild(time);
1848
- btn.__sessionNameNode = sessionName;
1849
- btn.__statusBadgeNode = statusBadge;
1850
- btn.__messageCountNode = messageCount;
1851
- btn.__timeNode = time;
1852
-
1853
2340
  btn.addEventListener('click', function () {
1854
2341
  handleSessionItemClick(btn.dataset.sessionName || '');
1855
2342
  });
1856
-
1857
- updateSessionRow(btn, session, index);
1858
2343
  return btn;
1859
2344
  }
1860
2345
 
2346
+ function scrollActiveSessionIntoView() {
2347
+ if (!state.pendingActiveSessionScroll || !state.active) {
2348
+ return;
2349
+ }
2350
+ state.pendingActiveSessionScroll = false;
2351
+ const targetNode = state.sessionNodeMap.get(state.active);
2352
+ if (targetNode && typeof targetNode.scrollIntoView === 'function') {
2353
+ targetNode.scrollIntoView({
2354
+ block: 'nearest'
2355
+ });
2356
+ }
2357
+ }
2358
+
1861
2359
  function renderSessions() {
1862
- sessionCount.textContent = state.loadingSessions ? '加载中...' : `${state.sessions.length} 个`;
2360
+ const containerCount = new Set(state.sessions.map(function (session) {
2361
+ return session && session.containerName ? session.containerName : '';
2362
+ }).filter(Boolean)).size;
2363
+ sessionCount.textContent = state.loadingSessions
2364
+ ? '加载中...'
2365
+ : `${state.sessions.length} 个 AGENT / ${containerCount} 个容器`;
1863
2366
 
1864
2367
  if (state.loadingSessions) {
1865
2368
  renderSessionsLoading();
@@ -1877,49 +2380,104 @@
1877
2380
  return;
1878
2381
  }
1879
2382
 
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
- }
2383
+ sessionList.innerHTML = '';
2384
+ state.sessionNodeMap.clear();
2385
+ state.sessionRenderMode = 'tree';
2386
+
2387
+ const grouped = groupSessionsByDirectory(state.sessions);
2388
+ let itemIndex = 0;
2389
+
2390
+ grouped.forEach(function (directoryGroup) {
2391
+ const directoryExpanded = isDirectoryExpanded(directoryGroup);
2392
+ const directoryHasActive = directoryContainsActiveSession(directoryGroup);
2393
+ const group = document.createElement('section');
2394
+ group.className = 'workbench-group';
2395
+ group.classList.toggle('has-active', directoryHasActive);
2396
+
2397
+ const groupHead = createTreeToggle({
2398
+ className: 'workbench-group-head',
2399
+ kickerClassName: 'workbench-group-kicker',
2400
+ titleClassName: 'workbench-group-title',
2401
+ kicker: '目录',
2402
+ title: directoryGroup.path,
2403
+ expanded: directoryExpanded,
2404
+ metaParts: [
2405
+ `${directoryGroup.containers.length} 个容器`,
2406
+ `${directoryGroup.containers.reduce(function (count, containerGroup) {
2407
+ return count + containerGroup.sessions.length;
2408
+ }, 0)} 个AGENT`,
2409
+ formatDateTime(directoryGroup.updatedAt) || '暂无更新'
2410
+ ],
2411
+ onClick: function () {
2412
+ setSidebarDirectoryExpanded(directoryGroup.path, !directoryExpanded);
2413
+ renderSessions();
2414
+ }
2415
+ });
2416
+ group.appendChild(groupHead);
2417
+
2418
+ const containerStack = document.createElement('div');
2419
+ containerStack.className = 'container-stack workbench-group-body';
2420
+ containerStack.hidden = !directoryExpanded;
2421
+
2422
+ directoryGroup.containers.forEach(function (containerGroup) {
2423
+ const containerExpanded = isContainerExpanded(containerGroup);
2424
+ const containerHasActive = containerContainsActiveSession(containerGroup);
2425
+ const status = sessionStatusInfo(containerGroup.status);
2426
+ const containerCard = document.createElement('section');
2427
+ containerCard.className = 'container-card';
2428
+ containerCard.classList.toggle('has-active', containerHasActive);
2429
+
2430
+ const containerHead = document.createElement('div');
2431
+ containerHead.className = 'container-card-head';
2432
+
2433
+ const containerInfo = createTreeToggle({
2434
+ className: 'container-toggle',
2435
+ kickerClassName: 'container-card-kicker',
2436
+ titleClassName: 'container-card-title',
2437
+ kicker: '容器',
2438
+ title: containerGroup.containerName,
2439
+ expanded: containerExpanded,
2440
+ metaParts: [
2441
+ status.label,
2442
+ `${containerGroup.sessions.length} 个AGENT`,
2443
+ formatDateTime(containerGroup.updatedAt) || '暂无更新'
2444
+ ],
2445
+ onClick: function () {
2446
+ setSidebarContainerExpanded(containerGroup.containerName, !containerExpanded);
2447
+ renderSessions();
2448
+ }
2449
+ });
1898
2450
 
1899
- const currentAtIndex = sessionList.children[index];
1900
- if (currentAtIndex !== row) {
1901
- sessionList.insertBefore(row, currentAtIndex || null);
1902
- }
1903
- });
2451
+ const addAgentBtn = document.createElement('button');
2452
+ addAgentBtn.type = 'button';
2453
+ addAgentBtn.className = 'secondary add-agent-btn';
2454
+ addAgentBtn.textContent = '新建AGENT';
2455
+ addAgentBtn.addEventListener('click', function () {
2456
+ createAgentSession(containerGroup.containerName);
2457
+ });
1904
2458
 
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);
2459
+ containerHead.appendChild(containerInfo);
2460
+ containerHead.appendChild(addAgentBtn);
2461
+ containerCard.appendChild(containerHead);
2462
+
2463
+ const agentList = document.createElement('div');
2464
+ agentList.className = 'agent-list';
2465
+ agentList.hidden = !containerExpanded;
2466
+ containerGroup.sessions.forEach(function (session) {
2467
+ const row = createAgentRow(session, itemIndex);
2468
+ state.sessionNodeMap.set(session.name, row);
2469
+ agentList.appendChild(row);
2470
+ itemIndex += 1;
2471
+ });
2472
+ containerCard.appendChild(agentList);
2473
+ containerStack.appendChild(containerCard);
2474
+ });
2475
+
2476
+ group.appendChild(containerStack);
2477
+ sessionList.appendChild(group);
1917
2478
  });
1918
2479
 
1919
- while (sessionList.children.length > state.sessions.length) {
1920
- sessionList.removeChild(sessionList.lastChild);
1921
- }
1922
- renderSessionActiveState();
2480
+ scrollActiveSessionIntoView();
1923
2481
  }
1924
2482
 
1925
2483
  function renderMessagesLoading() {
@@ -1953,6 +2511,11 @@
1953
2511
  }
1954
2512
 
1955
2513
  function getMessageRenderKey(msg, index) {
2514
+ if (msg && msg.id && msg.streamingReply) {
2515
+ const content = msg.content ? String(msg.content) : '';
2516
+ const timestamp = msg.timestamp ? String(msg.timestamp) : '';
2517
+ return `id:${msg.id}|streaming|${timestamp}|${content}`;
2518
+ }
1956
2519
  if (msg && msg.id && msg.streamTrace) {
1957
2520
  const content = msg.content ? String(msg.content) : '';
1958
2521
  const timestamp = msg.timestamp ? String(msg.timestamp) : '';
@@ -2004,14 +2567,39 @@
2004
2567
  const bubble = document.createElement('div');
2005
2568
  bubble.className = 'bubble';
2006
2569
 
2570
+ const isStreamingReply = Boolean(msg && msg.streamingReply);
2007
2571
  const shouldRenderStructuredTrace = Boolean(
2008
2572
  msg
2009
2573
  && msg.streamTrace
2010
2574
  && Array.isArray(msg.traceEvents)
2011
2575
  && msg.traceEvents.length
2012
2576
  );
2013
- const shouldRenderMarkdown = Boolean(!msg.streamTrace && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
2014
- if (shouldRenderStructuredTrace) {
2577
+ const shouldRenderMarkdown = Boolean(!msg.streamTrace && !msg.streamingReply && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
2578
+ if (isStreamingReply) {
2579
+ bubble.classList.add('streaming-reply');
2580
+ var replyContent = String(msg.content || '');
2581
+ if (replyContent && markdownRenderer && typeof markdownRenderer.render === 'function') {
2582
+ var mdNode = document.createElement('div');
2583
+ mdNode.className = 'md-content';
2584
+ var rendered = '';
2585
+ try {
2586
+ rendered = String(markdownRenderer.render(replyContent) || '');
2587
+ } catch (e) {
2588
+ rendered = '';
2589
+ }
2590
+ if (rendered) {
2591
+ mdNode.innerHTML = rendered;
2592
+ bubble.appendChild(mdNode);
2593
+ } else {
2594
+ appendPlainMessageContent(bubble, replyContent);
2595
+ }
2596
+ } else {
2597
+ appendPlainMessageContent(bubble, replyContent);
2598
+ }
2599
+ var cursor = document.createElement('span');
2600
+ cursor.className = 'streaming-cursor';
2601
+ bubble.appendChild(cursor);
2602
+ } else if (shouldRenderStructuredTrace) {
2015
2603
  appendStructuredTraceContent(bubble, msg);
2016
2604
  } else if (shouldRenderMarkdown) {
2017
2605
  const markdownNode = document.createElement('div');
@@ -2102,7 +2690,9 @@
2102
2690
  }
2103
2691
 
2104
2692
  function applySessionsSnapshot(rawSessions, preferredName) {
2693
+ const previousActive = state.active;
2105
2694
  state.sessions = Array.isArray(rawSessions) ? rawSessions : [];
2695
+ pruneSidebarTreeState();
2106
2696
 
2107
2697
  if (typeof preferredName === 'string' && preferredName.trim()) {
2108
2698
  state.active = preferredName.trim();
@@ -2116,6 +2706,10 @@
2116
2706
  if (!state.active && state.sessions.length) {
2117
2707
  state.active = state.sessions[0].name;
2118
2708
  }
2709
+ if (state.active && state.active !== previousActive) {
2710
+ ensureSessionPathExpanded(state.active);
2711
+ state.pendingActiveSessionScroll = true;
2712
+ }
2119
2713
  if (state.terminal.sessionName && state.terminal.sessionName !== state.active) {
2120
2714
  disconnectTerminal('会话已变化,终端已断开', true);
2121
2715
  }
@@ -2375,11 +2969,55 @@
2375
2969
  });
2376
2970
  }
2377
2971
 
2972
+ function appendStreamingReplyLocal(sessionName) {
2973
+ var replyMessage = {
2974
+ id: createLocalMessageId('local-streaming-reply'),
2975
+ role: 'assistant',
2976
+ content: '',
2977
+ timestamp: new Date().toISOString(),
2978
+ mode: 'agent',
2979
+ streamingReply: true
2980
+ };
2981
+ if (state.active === sessionName) {
2982
+ state.messages.push(replyMessage);
2983
+ }
2984
+ return replyMessage.id;
2985
+ }
2986
+
2987
+ function updateStreamingReplyLocal(sessionName, replyMessageId, content) {
2988
+ if (state.active !== sessionName) {
2989
+ return;
2990
+ }
2991
+ for (var i = state.messages.length - 1; i >= 0; i -= 1) {
2992
+ var message = state.messages[i];
2993
+ if (!message || String(message.id || '') !== String(replyMessageId || '')) {
2994
+ continue;
2995
+ }
2996
+ message.content = String(content || '');
2997
+ message.timestamp = new Date().toISOString();
2998
+ return;
2999
+ }
3000
+ }
3001
+
3002
+ function removeStreamingReplyLocal(sessionName, replyMessageId) {
3003
+ if (state.active !== sessionName) {
3004
+ return;
3005
+ }
3006
+ for (var i = state.messages.length - 1; i >= 0; i -= 1) {
3007
+ var message = state.messages[i];
3008
+ if (message && String(message.id || '') === String(replyMessageId || '') && message.streamingReply) {
3009
+ state.messages.splice(i, 1);
3010
+ return;
3011
+ }
3012
+ }
3013
+ }
3014
+
2378
3015
  async function sendAgentPromptStream(sessionName, inputText, pendingMessage) {
2379
3016
  const traceMessageId = appendAgentTraceMessageLocal(sessionName);
2380
3017
  const traceLines = ['[执行过程]', '等待 Agent 启动…'];
2381
3018
  let finalResult = null;
2382
3019
  let streamError = null;
3020
+ let streamingReplyId = null;
2383
3021
 
2384
3022
  state.agentRun.active = true;
2385
3023
  state.agentRun.sessionName = sessionName;
@@ -2429,6 +3067,20 @@
2429
3067
  pushTraceLine(event.text || '', event.traceEvent || null);
2430
3068
  return;
2431
3069
  }
3070
+ if (event.type === 'content_delta') {
3071
+ var content = String(event.content || '').trim();
3072
+ if (!content) {
3073
+ return;
3074
+ }
3075
+ if (!streamingReplyId) {
3076
+ streamingReplyId = appendStreamingReplyLocal(sessionName);
3077
+ }
3078
+ updateStreamingReplyLocal(sessionName, streamingReplyId, content);
3079
+ if (state.active === sessionName) {
3080
+ renderMessages(state.messages, { stickToBottom: true });
3081
+ }
3082
+ return;
3083
+ }
2432
3084
  if (event.type === 'result') {
2433
3085
  finalResult = event;
2434
3086
  if (event.interrupted) {
@@ -2448,6 +3100,9 @@
2448
3100
  streamError = e;
2449
3101
  }
2450
3102
  } finally {
3103
+ if (streamingReplyId) {
3104
+ removeStreamingReplyLocal(sessionName, streamingReplyId);
3105
+ }
2451
3106
  const pendingIndex = confirmPendingUserMessage(sessionName, pendingMessage.id);
2452
3107
  if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
2453
3108
  if (pendingIndex < messagesNode.children.length) {
@@ -2522,6 +3177,38 @@
2522
3177
  });
2523
3178
  }
2524
3179
 
3180
+ if (pickHostPathBtn) {
3181
+ pickHostPathBtn.addEventListener('click', function () {
3182
+ openDirectoryPicker('host');
3183
+ });
3184
+ }
3185
+
3186
+ if (pickContainerPathBtn) {
3187
+ pickContainerPathBtn.addEventListener('click', function () {
3188
+ openDirectoryPicker('container');
3189
+ });
3190
+ }
3191
+
3192
+ if (directoryPickerCancelBtn) {
3193
+ directoryPickerCancelBtn.addEventListener('click', function () {
3194
+ closeDirectoryPicker();
3195
+ });
3196
+ }
3197
+
3198
+ if (directoryPickerUpBtn) {
3199
+ directoryPickerUpBtn.addEventListener('click', function () {
3200
+ if (state.directoryPicker.parentPath) {
3201
+ loadDirectoryPicker(state.directoryPicker.parentPath);
3202
+ }
3203
+ });
3204
+ }
3205
+
3206
+ if (directoryPickerSelectBtn) {
3207
+ directoryPickerSelectBtn.addEventListener('click', function () {
3208
+ applyPickedDirectory();
3209
+ });
3210
+ }
3211
+
2525
3212
  if (createRun) {
2526
3213
  createRun.addEventListener('change', function () {
2527
3214
  applyCurrentRunDefaults();
@@ -2804,6 +3491,18 @@
2804
3491
  });
2805
3492
  }
2806
3493
 
3494
+ if (addAgentBtn) {
3495
+ addAgentBtn.addEventListener('click', function () {
3496
+ const activeSession = getActiveSession();
3497
+ const targetContainer = activeSession ? getSessionContainerName(activeSession) : '';
3498
+ if (!targetContainer) {
3499
+ return;
3500
+ }
3501
+ closeMobileActionsMenu();
3502
+ createAgentSession(targetContainer);
3503
+ });
3504
+ }
3505
+
2807
3506
  if (configModal) {
2808
3507
  configModal.addEventListener('click', function (event) {
2809
3508
  if (event.target === configModal && !state.configSaving) {
@@ -2822,6 +3521,14 @@
2822
3521
  });
2823
3522
  }
2824
3523
 
3524
+ if (directoryPickerModal) {
3525
+ directoryPickerModal.addEventListener('click', function (event) {
3526
+ if (event.target === directoryPickerModal && !state.directoryPicker.loading) {
3527
+ closeDirectoryPicker();
3528
+ }
3529
+ });
3530
+ }
3531
+
2825
3532
  window.addEventListener('keydown', function (event) {
2826
3533
  if (event.key === 'Escape' && state.configModalOpen) {
2827
3534
  closeConfigModal();
@@ -2831,6 +3538,9 @@
2831
3538
  closeCreateModal();
2832
3539
  syncUi();
2833
3540
  }
3541
+ if (event.key === 'Escape' && state.directoryPicker.open) {
3542
+ closeDirectoryPicker();
3543
+ }
2834
3544
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
2835
3545
  closeMobileSessionPanel();
2836
3546
  }
@@ -2877,7 +3587,9 @@
2877
3587
  removeBtn.addEventListener('click', async function () {
2878
3588
  if (!state.active) return;
2879
3589
  closeMobileActionsMenu();
2880
- const yes = confirm('确认删除容器 ' + state.active + ' ?');
3590
+ const activeSession = getActiveSession();
3591
+ const targetContainer = activeSession && activeSession.containerName ? activeSession.containerName : state.active;
3592
+ const yes = confirm('确认删除容器 ' + targetContainer + ' ?');
2881
3593
  if (!yes) return;
2882
3594
  try {
2883
3595
  const current = state.active;
@@ -2896,7 +3608,9 @@
2896
3608
  removeAllBtn.addEventListener('click', async function () {
2897
3609
  if (!state.active) return;
2898
3610
  closeMobileActionsMenu();
2899
- const yes = confirm('确认删除对话 ' + state.active + ' ?');
3611
+ const activeSession = getActiveSession();
3612
+ const targetAgent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
3613
+ const yes = confirm('确认删除对话 ' + targetAgent + ' ?');
2900
3614
  if (!yes) return;
2901
3615
  try {
2902
3616
  const current = state.active;
@@ -2913,6 +3627,7 @@
2913
3627
  disconnectTerminal('', true);
2914
3628
  });
2915
3629
 
3630
+ loadSidebarTreeState();
2916
3631
  renderSessions();
2917
3632
  renderMessages(state.messages);
2918
3633
  setMobileSessionPanel(false);