@xcanwin/manyoyo 6.2.4 → 6.2.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.
@@ -71,10 +71,17 @@
71
71
  createRuns: {},
72
72
  sessionNodeMap: new Map(),
73
73
  sessionRenderMode: 'empty',
74
- sidebarTreeLoaded: false,
75
- sidebarTree: {
76
- directories: {},
77
- containers: {}
74
+ sidebarNavLoaded: false,
75
+ navLevel: 'containers',
76
+ navContainer: '',
77
+ containerSearchQuery: '',
78
+ cloneModal: {
79
+ open: false,
80
+ mode: '',
81
+ sourceContainerName: '',
82
+ nameDraft: '',
83
+ submitting: false,
84
+ error: ''
78
85
  },
79
86
  pendingActiveSessionScroll: false,
80
87
  directoryPicker: {
@@ -120,6 +127,8 @@
120
127
  };
121
128
 
122
129
  const sidebarNode = document.querySelector('.sidebar');
130
+ const sessionBreadcrumb = document.getElementById('sessionBreadcrumb');
131
+ const containerSearchInput = document.getElementById('containerSearchInput');
123
132
  const sessionList = document.getElementById('sessionList');
124
133
  const sessionCount = document.getElementById('sessionCount');
125
134
  const mobileSessionToggle = document.getElementById('mobileSessionToggle');
@@ -211,6 +220,13 @@
211
220
  const externalLinkUrl = document.getElementById('externalLinkUrl');
212
221
  const externalLinkCancelBtn = document.getElementById('externalLinkCancelBtn');
213
222
  const externalLinkOpenBtn = document.getElementById('externalLinkOpenBtn');
223
+ const cloneNameModal = document.getElementById('cloneNameModal');
224
+ const cloneNameTitle = document.getElementById('cloneNameTitle');
225
+ const cloneNameTip = document.getElementById('cloneNameTip');
226
+ const cloneNameInput = document.getElementById('cloneNameInput');
227
+ const cloneNameError = document.getElementById('cloneNameError');
228
+ const cloneNameCancelBtn = document.getElementById('cloneNameCancelBtn');
229
+ const cloneNameConfirmBtn = document.getElementById('cloneNameConfirmBtn');
214
230
  const MOBILE_LAYOUT_MEDIA = window.matchMedia('(max-width: 980px)');
215
231
  const MOBILE_COMPACT_MEDIA = window.matchMedia('(max-width: 640px)');
216
232
  const TERMINAL_FIT_DEBOUNCE_MS = 60;
@@ -248,58 +264,49 @@
248
264
  gemini: YOLO_COMMAND_MAP.gemini,
249
265
  opencode: YOLO_COMMAND_MAP.opencode
250
266
  };
251
- const SIDEBAR_TREE_STORAGE_KEY = 'manyoyo.web.sidebarTree.v1';
267
+ const SIDEBAR_NAV_STORAGE_KEY = 'manyoyo.web.sidebarNav.v1';
252
268
  const markdownRenderer = window.ManyoyoMarkdown
253
269
  && typeof window.ManyoyoMarkdown.shouldRenderMessage === 'function'
254
270
  && typeof window.ManyoyoMarkdown.render === 'function'
255
271
  ? window.ManyoyoMarkdown
256
272
  : null;
257
273
  let fileBrowser = null;
258
- function normalizeBooleanMap(source) {
259
- const result = {};
260
- if (!source || typeof source !== 'object' || Array.isArray(source)) {
261
- return result;
262
- }
263
- Object.keys(source).forEach(function (key) {
264
- if (typeof source[key] === 'boolean') {
265
- result[String(key)] = source[key];
266
- }
267
- });
268
- return result;
269
- }
270
274
 
271
- function loadSidebarTreeState() {
272
- if (state.sidebarTreeLoaded) {
275
+ function loadSidebarNavState() {
276
+ if (state.sidebarNavLoaded) {
273
277
  return;
274
278
  }
275
- state.sidebarTreeLoaded = true;
279
+ state.sidebarNavLoaded = true;
276
280
  try {
277
281
  if (!window.localStorage) {
278
282
  return;
279
283
  }
280
- const raw = window.localStorage.getItem(SIDEBAR_TREE_STORAGE_KEY);
284
+ const raw = window.localStorage.getItem(SIDEBAR_NAV_STORAGE_KEY);
281
285
  if (!raw) {
282
286
  return;
283
287
  }
284
288
  const parsed = JSON.parse(raw);
285
- state.sidebarTree = {
286
- directories: normalizeBooleanMap(parsed && parsed.directories),
287
- containers: normalizeBooleanMap(parsed && parsed.containers)
288
- };
289
+ if (parsed && (parsed.navLevel === 'agents' || parsed.navLevel === 'containers')) {
290
+ state.navLevel = parsed.navLevel;
291
+ }
292
+ if (parsed && typeof parsed.navContainer === 'string') {
293
+ state.navContainer = parsed.navContainer;
294
+ }
289
295
  } catch (e) {
290
- state.sidebarTree = {
291
- directories: {},
292
- containers: {}
293
- };
296
+ state.navLevel = 'containers';
297
+ state.navContainer = '';
294
298
  }
295
299
  }
296
300
 
297
- function persistSidebarTreeState() {
301
+ function persistSidebarNavState() {
298
302
  try {
299
303
  if (!window.localStorage) {
300
304
  return;
301
305
  }
302
- window.localStorage.setItem(SIDEBAR_TREE_STORAGE_KEY, JSON.stringify(state.sidebarTree));
306
+ window.localStorage.setItem(SIDEBAR_NAV_STORAGE_KEY, JSON.stringify({
307
+ navLevel: state.navLevel,
308
+ navContainer: state.navContainer
309
+ }));
303
310
  } catch (e) {
304
311
  // 忽略浏览器存储异常,避免影响主流程
305
312
  }
@@ -686,87 +693,33 @@
686
693
  }) || null;
687
694
  }
688
695
 
689
- function pruneSidebarTreeState() {
690
- loadSidebarTreeState();
691
-
692
- const validDirectories = new Set(state.sessions.map(getSessionDirectoryPath));
693
- const validContainers = new Set(state.sessions.map(getSessionContainerName).filter(Boolean));
694
- let changed = false;
695
-
696
- Object.keys(state.sidebarTree.directories).forEach(function (key) {
697
- if (!validDirectories.has(key)) {
698
- delete state.sidebarTree.directories[key];
699
- changed = true;
700
- }
701
- });
702
-
703
- Object.keys(state.sidebarTree.containers).forEach(function (key) {
704
- if (!validContainers.has(key)) {
705
- delete state.sidebarTree.containers[key];
706
- changed = true;
707
- }
708
- });
709
-
710
- if (changed) {
711
- persistSidebarTreeState();
712
- }
713
- }
714
-
715
- function setSidebarDirectoryExpanded(directoryPath, expanded, options) {
716
- const path = String(directoryPath || '').trim();
717
- if (!path) {
718
- return false;
719
- }
720
- loadSidebarTreeState();
721
- const opts = options && typeof options === 'object' ? options : {};
722
- if (state.sidebarTree.directories[path] === expanded) {
723
- return false;
724
- }
725
- state.sidebarTree.directories[path] = expanded;
726
- if (opts.persist !== false) {
727
- persistSidebarTreeState();
728
- }
729
- return true;
730
- }
731
-
732
- function setSidebarContainerExpanded(containerName, expanded, options) {
733
- const name = String(containerName || '').trim();
734
- if (!name) {
735
- return false;
736
- }
737
- loadSidebarTreeState();
738
- const opts = options && typeof options === 'object' ? options : {};
739
- if (state.sidebarTree.containers[name] === expanded) {
740
- return false;
696
+ function pruneSidebarNavState() {
697
+ loadSidebarNavState();
698
+ if (state.navLevel !== 'agents') {
699
+ return;
741
700
  }
742
- state.sidebarTree.containers[name] = expanded;
743
- if (opts.persist !== false) {
744
- persistSidebarTreeState();
701
+ const validContainers = new Set(state.sessions.map(getSessionContainerName).filter(Boolean));
702
+ if (!validContainers.has(state.navContainer)) {
703
+ state.navLevel = 'containers';
704
+ state.navContainer = '';
705
+ persistSidebarNavState();
745
706
  }
746
- return true;
747
707
  }
748
708
 
749
- function ensureSessionPathExpanded(sessionName, options) {
709
+ function ensureSessionNavPath(sessionName, options) {
750
710
  const session = findSessionByName(sessionName);
751
711
  if (!session) {
752
712
  return false;
753
713
  }
754
714
  const opts = options && typeof options === 'object' ? options : {};
755
- const directoryPath = getSessionDirectoryPath(session);
756
715
  const containerName = getSessionContainerName(session);
757
- const changedDirectory = setSidebarDirectoryExpanded(directoryPath, true, { persist: false });
758
- const changedContainer = setSidebarContainerExpanded(containerName, true, { persist: false });
759
- if ((changedDirectory || changedContainer) && opts.persist !== false) {
760
- persistSidebarTreeState();
716
+ const changed = state.navLevel !== 'agents' || state.navContainer !== containerName;
717
+ state.navLevel = 'agents';
718
+ state.navContainer = containerName;
719
+ if (changed && opts.persist !== false) {
720
+ persistSidebarNavState();
761
721
  }
762
- return changedDirectory || changedContainer;
763
- }
764
-
765
- function directoryContainsActiveSession(directoryGroup) {
766
- const groups = directoryGroup && Array.isArray(directoryGroup.containers) ? directoryGroup.containers : [];
767
- return groups.some(function (containerGroup) {
768
- return containerContainsActiveSession(containerGroup);
769
- });
722
+ return changed;
770
723
  }
771
724
 
772
725
  function containerContainsActiveSession(containerGroup) {
@@ -776,24 +729,6 @@
776
729
  });
777
730
  }
778
731
 
779
- function isDirectoryExpanded(directoryGroup) {
780
- loadSidebarTreeState();
781
- const key = String(directoryGroup && directoryGroup.path ? directoryGroup.path : '').trim();
782
- if (key && typeof state.sidebarTree.directories[key] === 'boolean') {
783
- return state.sidebarTree.directories[key];
784
- }
785
- return false;
786
- }
787
-
788
- function isContainerExpanded(containerGroup) {
789
- loadSidebarTreeState();
790
- const key = String(containerGroup && containerGroup.containerName ? containerGroup.containerName : '').trim();
791
- if (key && typeof state.sidebarTree.containers[key] === 'boolean') {
792
- return state.sidebarTree.containers[key];
793
- }
794
- return false;
795
- }
796
-
797
732
  function setModalVisible(modalNode, visible) {
798
733
  if (!modalNode) return;
799
734
  modalNode.hidden = !visible;
@@ -1414,6 +1349,9 @@
1414
1349
  state.agentTemplateModalOpen = true;
1415
1350
  fillAgentTemplateForm(detail);
1416
1351
  syncUi();
1352
+ if (isMobileLayout()) {
1353
+ return;
1354
+ }
1417
1355
  if (isActiveAgentOverrideEditable() && agentCliSelect) {
1418
1356
  agentCliSelect.focus();
1419
1357
  } else if (containerCliSelect) {
@@ -1861,7 +1799,9 @@
1861
1799
  state.terminal.connected = true;
1862
1800
  state.terminal.status = '已连接';
1863
1801
  if (state.terminal.term) {
1864
- state.terminal.term.focus();
1802
+ if (!isMobileLayout()) {
1803
+ state.terminal.term.focus();
1804
+ }
1865
1805
  scheduleTerminalFit(true);
1866
1806
  }
1867
1807
  syncUi();
@@ -1999,7 +1939,7 @@
1999
1939
  if (!state.terminal.connected && !state.terminal.connecting && !isActiveSessionHistoryOnly()) {
2000
1940
  connectTerminal();
2001
1941
  }
2002
- if (state.terminal.term) {
1942
+ if (state.terminal.term && !isMobileLayout()) {
2003
1943
  state.terminal.term.focus();
2004
1944
  }
2005
1945
  }
@@ -2423,7 +2363,7 @@
2423
2363
  }
2424
2364
  document.body.classList.toggle(
2425
2365
  'modal-open',
2426
- state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen
2366
+ state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen || state.cloneModal.open
2427
2367
  );
2428
2368
  if (composer) {
2429
2369
  composer.hidden = !activityTab;
@@ -2839,7 +2779,7 @@
2839
2779
  disconnectTerminal('会话切换,终端已断开', true);
2840
2780
  }
2841
2781
  state.active = sessionName;
2842
- ensureSessionPathExpanded(sessionName);
2782
+ ensureSessionNavPath(sessionName);
2843
2783
  state.sessionDetail = null;
2844
2784
  state.sessionDetailError = '';
2845
2785
  if (isMobileLayout()) {
@@ -2894,48 +2834,33 @@
2894
2834
  }
2895
2835
  }
2896
2836
 
2897
- function groupSessionsByDirectory(sessions) {
2837
+ function groupSessionsByContainer(sessions) {
2898
2838
  const groups = new Map();
2899
2839
  (Array.isArray(sessions) ? sessions : []).forEach(function (session) {
2900
- const directoryPath = String(session && session.hostPath ? session.hostPath : '').trim() || '未配置目录';
2901
- if (!groups.has(directoryPath)) {
2902
- groups.set(directoryPath, {
2903
- path: directoryPath,
2904
- updatedAt: session && session.updatedAt ? session.updatedAt : '',
2905
- containers: new Map()
2906
- });
2907
- }
2908
- const directoryGroup = groups.get(directoryPath);
2909
- if (session && session.updatedAt && (!directoryGroup.updatedAt || new Date(session.updatedAt).getTime() > new Date(directoryGroup.updatedAt).getTime())) {
2910
- directoryGroup.updatedAt = session.updatedAt;
2840
+ const containerName = getSessionContainerName(session);
2841
+ if (!containerName) {
2842
+ return;
2911
2843
  }
2912
-
2913
- const containerName = String(session && session.containerName ? session.containerName : '');
2914
- if (!directoryGroup.containers.has(containerName)) {
2915
- directoryGroup.containers.set(containerName, {
2844
+ if (!groups.has(containerName)) {
2845
+ groups.set(containerName, {
2916
2846
  containerName: containerName,
2847
+ containerRemark: session && session.containerRemark ? session.containerRemark : '',
2917
2848
  status: session && session.status ? session.status : 'history',
2918
2849
  image: session && session.image ? session.image : '',
2850
+ hostPath: getSessionDirectoryPath(session),
2919
2851
  updatedAt: session && session.updatedAt ? session.updatedAt : '',
2920
2852
  sessions: []
2921
2853
  });
2922
2854
  }
2923
- const containerGroup = directoryGroup.containers.get(containerName);
2855
+ const containerGroup = groups.get(containerName);
2924
2856
  containerGroup.sessions.push(session);
2925
2857
  if (session && session.updatedAt && (!containerGroup.updatedAt || new Date(session.updatedAt).getTime() > new Date(containerGroup.updatedAt).getTime())) {
2926
2858
  containerGroup.updatedAt = session.updatedAt;
2927
2859
  }
2928
2860
  });
2929
2861
  return Array.from(groups.values()).map(function (group) {
2930
- group.containers = Array.from(group.containers.values()).sort(function (a, b) {
2931
- const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2932
- const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
2933
- return timeB - timeA;
2934
- });
2935
- group.containers.forEach(function (containerGroup) {
2936
- containerGroup.sessions.sort(function (a, b) {
2937
- return compareSessionByCreatedDesc(a, b);
2938
- });
2862
+ group.sessions.sort(function (a, b) {
2863
+ return compareSessionByCreatedDesc(a, b);
2939
2864
  });
2940
2865
  return group;
2941
2866
  }).sort(function (a, b) {
@@ -3163,254 +3088,272 @@
3163
3088
  }
3164
3089
  }
3165
3090
 
3166
- function createDisclosureButton(expanded, label) {
3167
- const button = document.createElement('button');
3168
- button.type = 'button';
3169
- button.className = 'disclosure-toggle';
3170
- button.dataset.disclosureLabel = label || '';
3171
- button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
3172
- button.setAttribute('aria-label', `${expanded ? '折叠' : '展开'}${label ? ` ${label}` : ''}`);
3173
- button.innerHTML = '<svg viewBox="0 0 12 12" aria-hidden="true" focusable="false"><path d="M4 2.5L8 6L4 9.5"></path></svg>';
3174
- return button;
3175
- }
3176
-
3177
- function createTreePrefixSegment() {
3178
- const segment = document.createElement('span');
3179
- segment.className = 'tree-prefix-segment';
3180
- segment.setAttribute('aria-hidden', 'true');
3181
- return segment;
3182
- }
3183
-
3184
- function createTreePrefix(ancestorHasNext, isLastSibling, options) {
3091
+ function createSessionCard(options) {
3185
3092
  const opts = options && typeof options === 'object' ? options : {};
3186
- const prefix = document.createElement('div');
3187
- prefix.className = 'tree-prefix';
3188
-
3189
- (Array.isArray(ancestorHasNext) ? ancestorHasNext : []).forEach(function () {
3190
- prefix.appendChild(createTreePrefixSegment());
3191
- });
3192
-
3193
- const branch = document.createElement('span');
3194
- branch.className = `tree-prefix-branch ${isLastSibling ? 'is-last' : 'is-mid'}`;
3195
- branch.setAttribute('aria-hidden', 'true');
3196
- prefix.appendChild(branch);
3197
-
3198
- let disclosure = null;
3199
- let disclosureWrap = null;
3200
- if (opts.expandable) {
3201
- disclosure = createDisclosureButton(!!opts.expanded, opts.label || '');
3202
- disclosureWrap = document.createElement('span');
3203
- disclosureWrap.className = 'tree-prefix-toggle';
3204
- disclosureWrap.appendChild(disclosure);
3205
- prefix.appendChild(disclosureWrap);
3206
- } else {
3207
- const leaf = document.createElement('span');
3208
- leaf.className = 'tree-prefix-leaf';
3209
- leaf.setAttribute('aria-hidden', 'true');
3210
- prefix.appendChild(leaf);
3211
- }
3212
-
3213
- return { root: prefix, disclosure, disclosureWrap };
3214
- }
3215
-
3216
- function setDisclosureExpanded(control, expanded) {
3217
- if (!control) {
3218
- return;
3219
- }
3220
- const nextValue = expanded ? 'true' : 'false';
3221
- if (control.button) {
3222
- control.button.setAttribute('aria-expanded', nextValue);
3223
- }
3224
- if (control.disclosure) {
3225
- control.disclosure.setAttribute('aria-expanded', nextValue);
3226
- const label = control.disclosure.dataset.disclosureLabel || '';
3227
- control.disclosure.setAttribute('aria-label', `${expanded ? '折叠' : '展开'}${label ? ` ${label}` : ''}`);
3228
- }
3229
- }
3230
-
3231
- function createTreeItem(options) {
3232
- const opts = options && typeof options === 'object' ? options : {};
3233
- const row = document.createElement('div');
3234
- row.className = `tree-node-row tree-node-row-${opts.kind || 'item'} ${opts.rowClassName || ''}`.trim();
3235
- row.classList.toggle('has-active', !!opts.hasActive);
3236
- row.setAttribute('role', 'none');
3237
-
3238
- const prefixControl = createTreePrefix(opts.ancestorHasNext, !!opts.isLastSibling, {
3239
- expandable: !!opts.expandable,
3240
- expanded: !!opts.expanded,
3241
- label: opts.disclosureLabel || opts.title || ''
3242
- });
3243
- row.appendChild(prefixControl.root);
3093
+ const card = document.createElement('div');
3094
+ card.className = `session-card session-card-${opts.kind || 'item'} ${opts.className || ''}`.trim();
3095
+ card.classList.toggle('has-active', !!opts.hasActive);
3244
3096
 
3245
3097
  const button = document.createElement('button');
3246
3098
  button.type = 'button';
3247
- button.className = `tree-node-button tree-node-button-${opts.kind || 'item'} ${opts.className || ''}`.trim();
3248
- button.setAttribute('role', 'treeitem');
3249
- button.setAttribute('aria-level', String(opts.level || 1));
3250
- if (opts.expandable) {
3251
- button.setAttribute('aria-expanded', opts.expanded ? 'true' : 'false');
3252
- }
3099
+ button.className = 'session-card-button';
3100
+ button.classList.toggle('active', !!opts.active);
3253
3101
  if (opts.kind === 'agent') {
3254
3102
  button.setAttribute('aria-selected', opts.active ? 'true' : 'false');
3255
3103
  }
3256
- button.classList.toggle('active', !!opts.active);
3104
+ button.addEventListener('click', function () {
3105
+ if (typeof opts.onClick === 'function') {
3106
+ opts.onClick();
3107
+ }
3108
+ });
3257
3109
 
3258
3110
  const main = document.createElement('div');
3259
- main.className = 'tree-node-main';
3111
+ main.className = 'session-card-main';
3260
3112
 
3261
- const title = document.createElement('div');
3262
- title.className = opts.titleClassName || 'tree-node-title';
3113
+ const titleRow = document.createElement('div');
3114
+ titleRow.className = 'session-card-title-row';
3115
+ const title = document.createElement('span');
3116
+ title.className = 'session-card-title';
3263
3117
  title.textContent = opts.title || '';
3264
3118
  title.title = opts.title || '';
3265
- main.appendChild(title);
3119
+ titleRow.appendChild(title);
3120
+ if (opts.statusLabel) {
3121
+ const statusBadge = document.createElement('span');
3122
+ statusBadge.className = `tree-node-status ${opts.statusTone || 'unknown'}`;
3123
+ statusBadge.textContent = opts.statusLabel;
3124
+ titleRow.appendChild(statusBadge);
3125
+ }
3126
+ main.appendChild(titleRow);
3266
3127
 
3267
3128
  if (opts.meta) {
3268
3129
  const meta = document.createElement('div');
3269
- meta.className = `tree-node-meta ${opts.metaClassName || ''}`.trim();
3130
+ meta.className = 'session-card-meta';
3270
3131
  meta.textContent = opts.meta;
3271
3132
  main.appendChild(meta);
3272
3133
  }
3134
+ if (opts.subMeta) {
3135
+ const subMeta = document.createElement('div');
3136
+ subMeta.className = 'session-card-submeta';
3137
+ subMeta.textContent = opts.subMeta;
3138
+ subMeta.title = opts.subMeta;
3139
+ main.appendChild(subMeta);
3140
+ }
3273
3141
 
3274
3142
  button.appendChild(main);
3275
- row.appendChild(button);
3143
+ card.appendChild(button);
3276
3144
  if (opts.overlayNode) {
3277
- row.appendChild(opts.overlayNode);
3145
+ card.appendChild(opts.overlayNode);
3278
3146
  }
3279
-
3280
- const control = {
3281
- root: row,
3282
- button,
3283
- disclosure: prefixControl.disclosure
3284
- };
3285
- setDisclosureExpanded(control, !!opts.expanded);
3286
- return control;
3147
+ return { root: card, button };
3287
3148
  }
3288
3149
 
3289
- function buildSessionTreeNodes(grouped) {
3290
- return (Array.isArray(grouped) ? grouped : []).map(function (directoryGroup) {
3291
- return {
3292
- kind: 'directory',
3293
- title: directoryGroup.path,
3294
- disclosureLabel: directoryGroup.path,
3295
- expanded: isDirectoryExpanded(directoryGroup),
3296
- hasActive: directoryContainsActiveSession(directoryGroup),
3297
- onToggle: function (expanded) {
3298
- setSidebarDirectoryExpanded(directoryGroup.path, expanded);
3299
- },
3300
- children: (Array.isArray(directoryGroup.containers) ? directoryGroup.containers : []).map(function (containerGroup) {
3301
- const status = sessionStatusInfo(containerGroup && containerGroup.status);
3302
- const historyClassName = status.tone === 'history' ? 'tree-node-tone-history' : '';
3303
- const containerName = String(containerGroup && containerGroup.containerName ? containerGroup.containerName : '').trim() || '未命名容器';
3304
- const containerMenu = createTreeNodeMenu([
3305
- {
3306
- label: '新建 AGENT',
3307
- onClick: function () {
3308
- createAgentSession(containerName);
3309
- }
3310
- },
3311
- {
3312
- label: '删除容器',
3313
- danger: true,
3314
- onClick: function () {
3315
- removeContainerByName(containerName);
3316
- }
3317
- }
3318
- ]);
3319
-
3320
- return {
3321
- kind: 'container',
3322
- title: containerName,
3323
- meta: status.label,
3324
- metaClassName: `tree-node-status ${status.tone}`,
3325
- className: historyClassName,
3326
- disclosureLabel: containerName,
3327
- expanded: isContainerExpanded(containerGroup),
3328
- hasActive: containerContainsActiveSession(containerGroup),
3329
- overlayNode: containerMenu,
3330
- onToggle: function (expanded) {
3331
- setSidebarContainerExpanded(containerGroup.containerName, expanded);
3332
- },
3333
- children: (Array.isArray(containerGroup.sessions) ? containerGroup.sessions : []).map(function (session) {
3334
- const agentTitle = session.agentName || session.name;
3335
- return {
3336
- kind: 'agent',
3337
- title: agentTitle,
3338
- meta: formatDateTime(session.updatedAt) || '暂无更新',
3339
- className: historyClassName,
3340
- active: state.active === session.name,
3341
- sessionName: session.name,
3342
- overlayNode: createTreeNodeMenu([
3343
- {
3344
- label: '删除 AGENT',
3345
- danger: true,
3346
- onClick: function () {
3347
- removeAgentSessionByName(session.name, agentTitle);
3348
- }
3349
- }
3350
- ])
3351
- };
3352
- })
3353
- };
3354
- })
3355
- };
3356
- });
3150
+ function navigateTo(level, params) {
3151
+ const opts = params && typeof params === 'object' ? params : {};
3152
+ if (level === 'agents') {
3153
+ const containerName = String(opts.container || '').trim();
3154
+ if (!containerName) {
3155
+ return;
3156
+ }
3157
+ state.navLevel = 'agents';
3158
+ state.navContainer = containerName;
3159
+ } else {
3160
+ state.navLevel = 'containers';
3161
+ state.navContainer = '';
3162
+ }
3163
+ persistSidebarNavState();
3164
+ renderSessions();
3357
3165
  }
3358
3166
 
3359
- function renderSessionTreeNodes(nodes, parentNode, ancestorHasNext) {
3360
- (Array.isArray(nodes) ? nodes : []).forEach(function (node, index) {
3361
- const isLastSibling = index === nodes.length - 1;
3362
- const item = createTreeItem({
3363
- kind: node.kind,
3364
- title: node.title,
3365
- meta: node.meta,
3366
- className: node.className,
3367
- metaClassName: node.metaClassName,
3368
- disclosureLabel: node.disclosureLabel,
3369
- expandable: Array.isArray(node.children) && node.children.length > 0,
3370
- expanded: !!node.expanded,
3371
- active: !!node.active,
3372
- hasActive: !!node.hasActive,
3373
- level: ancestorHasNext.length + 1,
3374
- ancestorHasNext: ancestorHasNext,
3375
- isLastSibling: isLastSibling,
3376
- overlayNode: node.overlayNode
3167
+ function renderBreadcrumb() {
3168
+ if (!sessionBreadcrumb) {
3169
+ return;
3170
+ }
3171
+ sessionBreadcrumb.innerHTML = '';
3172
+
3173
+ const rootCrumb = document.createElement('button');
3174
+ rootCrumb.type = 'button';
3175
+ rootCrumb.className = 'crumb-item';
3176
+ rootCrumb.textContent = '全部容器';
3177
+ if (state.navLevel === 'containers') {
3178
+ rootCrumb.classList.add('is-current');
3179
+ rootCrumb.disabled = true;
3180
+ } else {
3181
+ rootCrumb.addEventListener('click', function () {
3182
+ navigateTo('containers');
3377
3183
  });
3184
+ }
3185
+ sessionBreadcrumb.appendChild(rootCrumb);
3186
+
3187
+ if (state.navLevel === 'agents' && state.navContainer) {
3188
+ const sep = document.createElement('span');
3189
+ sep.className = 'crumb-sep';
3190
+ sep.setAttribute('aria-hidden', 'true');
3191
+ sep.textContent = '›';
3192
+ sessionBreadcrumb.appendChild(sep);
3193
+
3194
+ const containerCrumb = document.createElement('span');
3195
+ containerCrumb.className = 'crumb-item is-current';
3196
+ containerCrumb.textContent = state.navContainer;
3197
+ containerCrumb.title = state.navContainer;
3198
+ sessionBreadcrumb.appendChild(containerCrumb);
3199
+ }
3378
3200
 
3379
- if (node.kind === 'agent') {
3380
- item.button.dataset.sessionName = node.sessionName || '';
3381
- state.sessionNodeMap.set(node.sessionName, item.button);
3382
- item.button.addEventListener('click', function () {
3383
- handleSessionItemClick(node.sessionName || '');
3201
+ const activeSession = getActiveSession();
3202
+ if (activeSession) {
3203
+ const activeContainerName = getSessionContainerName(activeSession);
3204
+ const isActiveVisible = state.navLevel === 'agents' && state.navContainer === activeContainerName;
3205
+ if (!isActiveVisible) {
3206
+ const jumpBtn = document.createElement('button');
3207
+ jumpBtn.type = 'button';
3208
+ jumpBtn.className = 'crumb-jump-active';
3209
+ jumpBtn.textContent = '回到当前会话';
3210
+ jumpBtn.addEventListener('click', function () {
3211
+ navigateTo('agents', { container: activeContainerName });
3384
3212
  });
3385
- parentNode.appendChild(item.root);
3386
- return;
3213
+ sessionBreadcrumb.appendChild(jumpBtn);
3387
3214
  }
3215
+ }
3216
+ }
3217
+
3218
+ function renderContainerLevel(groups) {
3219
+ sessionList.innerHTML = '';
3220
+ if (!groups.length) {
3221
+ const empty = document.createElement('div');
3222
+ empty.className = 'empty';
3223
+ empty.textContent = state.containerSearchQuery.trim() ? '没有匹配的容器' : '暂无 manyoyo 容器';
3224
+ sessionList.appendChild(empty);
3225
+ return;
3226
+ }
3227
+ groups.forEach(function (containerGroup) {
3228
+ const status = sessionStatusInfo(containerGroup.status);
3229
+ const item = createSessionCard({
3230
+ kind: 'container',
3231
+ className: status.tone === 'history' ? 'tree-node-tone-history' : '',
3232
+ title: containerGroup.containerRemark || containerGroup.containerName,
3233
+ meta: containerGroup.containerRemark ? containerGroup.containerName : '',
3234
+ statusLabel: status.label,
3235
+ statusTone: status.tone,
3236
+ subMeta: containerGroup.hostPath,
3237
+ hasActive: containerContainsActiveSession(containerGroup),
3238
+ onClick: function () {
3239
+ navigateTo('agents', { container: containerGroup.containerName });
3240
+ },
3241
+ overlayNode: createTreeNodeMenu([
3242
+ {
3243
+ label: '新建 AGENT',
3244
+ onClick: function () {
3245
+ createAgentSession(containerGroup.containerName);
3246
+ }
3247
+ },
3248
+ {
3249
+ label: '编辑备注',
3250
+ onClick: function () {
3251
+ editContainerRemark(containerGroup);
3252
+ }
3253
+ },
3254
+ {
3255
+ label: '创建相同配置容器',
3256
+ onClick: function () {
3257
+ openCloneModal('clone-config', containerGroup.containerName);
3258
+ }
3259
+ },
3260
+ {
3261
+ label: '复制容器',
3262
+ onClick: function () {
3263
+ openCloneModal('duplicate', containerGroup.containerName);
3264
+ }
3265
+ },
3266
+ {
3267
+ label: '删除容器',
3268
+ danger: true,
3269
+ onClick: function () {
3270
+ removeContainerByName(containerGroup.containerName);
3271
+ }
3272
+ }
3273
+ ])
3274
+ });
3275
+ sessionList.appendChild(item.root);
3276
+ });
3277
+ }
3388
3278
 
3389
- const block = document.createElement('section');
3390
- block.className = `tree-node-block tree-node-block-${node.kind}`;
3391
- block.classList.toggle('has-active', !!node.hasActive);
3392
- block.appendChild(item.root);
3279
+ async function editContainerRemark(containerGroup) {
3280
+ const current = containerGroup.containerRemark || '';
3281
+ const input = window.prompt('设置容器备注(留空清除备注)', current);
3282
+ if (input === null) {
3283
+ return;
3284
+ }
3285
+ const remark = String(input || '').trim();
3286
+ try {
3287
+ await api('/api/sessions/' + encodeURIComponent(containerGroup.containerName) + '/container-remark', {
3288
+ method: 'POST',
3289
+ body: JSON.stringify({ remark })
3290
+ });
3291
+ await refreshSessionsSilent({ preferredName: state.active });
3292
+ } catch (e) {
3293
+ alert(e.message);
3294
+ }
3295
+ }
3393
3296
 
3394
- const childrenNode = document.createElement('div');
3395
- childrenNode.className = `tree-node-children tree-node-children-${node.kind}`;
3396
- childrenNode.setAttribute('role', 'group');
3397
- childrenNode.hidden = !node.expanded;
3398
- renderSessionTreeNodes(node.children || [], childrenNode, ancestorHasNext.concat(!isLastSibling));
3399
- block.appendChild(childrenNode);
3400
- parentNode.appendChild(block);
3297
+ async function editAgentRemark(session) {
3298
+ const current = session.agentRemark || '';
3299
+ const input = window.prompt('设置 AGENT 备注(留空清除备注)', current);
3300
+ if (input === null) {
3301
+ return;
3302
+ }
3303
+ const remark = String(input || '').trim();
3304
+ try {
3305
+ await api('/api/sessions/' + encodeURIComponent(session.name) + '/agent-remark', {
3306
+ method: 'POST',
3307
+ body: JSON.stringify({ remark })
3308
+ });
3309
+ await refreshSessionsSilent({ preferredName: state.active });
3310
+ } catch (e) {
3311
+ alert(e.message);
3312
+ }
3313
+ }
3401
3314
 
3402
- const toggleNode = function () {
3403
- const nextExpanded = childrenNode.hidden;
3404
- if (typeof node.onToggle === 'function') {
3405
- node.onToggle(nextExpanded);
3406
- }
3407
- setDisclosureExpanded(item, nextExpanded);
3408
- childrenNode.hidden = !nextExpanded;
3409
- };
3410
- item.button.addEventListener('click', toggleNode);
3411
- if (item.disclosure) {
3412
- item.disclosure.addEventListener('click', toggleNode);
3413
- }
3315
+ function renderAgentLevel(containerGroup) {
3316
+ sessionList.innerHTML = '';
3317
+ const sessions = containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions : [];
3318
+ if (!sessions.length) {
3319
+ const empty = document.createElement('div');
3320
+ empty.className = 'empty';
3321
+ empty.textContent = '该容器下暂无 AGENT';
3322
+ sessionList.appendChild(empty);
3323
+ return;
3324
+ }
3325
+ sessions.forEach(function (session) {
3326
+ const agentTitle = session.agentName || session.name;
3327
+ const displayTitle = session.agentRemark || agentTitle;
3328
+ const dateLabel = formatDateTime(session.updatedAt) || '暂无更新';
3329
+ const item = createSessionCard({
3330
+ kind: 'agent',
3331
+ title: displayTitle,
3332
+ meta: session.agentRemark ? agentTitle : dateLabel,
3333
+ subMeta: session.agentRemark ? dateLabel : '',
3334
+ active: state.active === session.name,
3335
+ onClick: function () {
3336
+ handleSessionItemClick(session.name);
3337
+ },
3338
+ overlayNode: createTreeNodeMenu([
3339
+ {
3340
+ label: '编辑备注',
3341
+ onClick: function () {
3342
+ editAgentRemark(session);
3343
+ }
3344
+ },
3345
+ {
3346
+ label: '删除 AGENT',
3347
+ danger: true,
3348
+ onClick: function () {
3349
+ removeAgentSessionByName(session.name, agentTitle);
3350
+ }
3351
+ }
3352
+ ])
3353
+ });
3354
+ item.button.dataset.sessionName = session.name;
3355
+ state.sessionNodeMap.set(session.name, item.button);
3356
+ sessionList.appendChild(item.root);
3414
3357
  });
3415
3358
  }
3416
3359
 
@@ -3429,45 +3372,24 @@
3429
3372
 
3430
3373
  function updateSidebarActiveSelection() {
3431
3374
  state.sessionNodeMap.forEach(function (buttonNode, sessionName) {
3432
- const isActive = sessionName === state.active;
3433
3375
  if (!buttonNode) {
3434
3376
  return;
3435
3377
  }
3378
+ const isActive = sessionName === state.active;
3436
3379
  buttonNode.classList.toggle('active', isActive);
3437
3380
  buttonNode.setAttribute('aria-selected', isActive ? 'true' : 'false');
3438
3381
  });
3439
-
3440
- if (!sessionList) {
3441
- return;
3442
- }
3443
-
3444
- sessionList.querySelectorAll('.tree-node-block.has-active').forEach(function (blockNode) {
3445
- blockNode.classList.remove('has-active');
3446
- });
3447
-
3448
- const activeNode = state.sessionNodeMap.get(state.active);
3449
- let cursor = activeNode ? activeNode.parentElement : null;
3450
- while (cursor && cursor !== sessionList) {
3451
- if (cursor.classList && cursor.classList.contains('tree-node-children')) {
3452
- const blockNode = cursor.parentElement;
3453
- if (blockNode && blockNode.classList && blockNode.classList.contains('tree-node-block')) {
3454
- blockNode.classList.add('has-active');
3455
- }
3456
- }
3457
- cursor = cursor.parentElement;
3458
- }
3382
+ renderBreadcrumb();
3459
3383
  }
3460
3384
 
3461
3385
  function renderSessions() {
3462
- const directoryCount = new Set(state.sessions.map(function (session) {
3463
- return String(session && session.hostPath ? session.hostPath : '').trim() || '未配置目录';
3464
- }).filter(Boolean)).size;
3465
3386
  const containerCount = new Set(state.sessions.map(function (session) {
3466
3387
  return session && session.containerName ? session.containerName : '';
3467
3388
  }).filter(Boolean)).size;
3468
3389
  sessionCount.textContent = state.loadingSessions
3469
3390
  ? '加载中...'
3470
- : `${directoryCount} 个 目录 / ${containerCount} 个容器 / ${state.sessions.length} 个 AGENT`;
3391
+ : `${containerCount} 个容器 / ${state.sessions.length} 个 AGENT`;
3392
+ renderBreadcrumb();
3471
3393
 
3472
3394
  if (state.loadingSessions) {
3473
3395
  renderSessionsLoading();
@@ -3485,18 +3407,153 @@
3485
3407
  return;
3486
3408
  }
3487
3409
 
3488
- sessionList.innerHTML = '';
3489
3410
  state.sessionNodeMap.clear();
3490
- state.sessionRenderMode = 'tree';
3411
+ state.sessionRenderMode = 'cards';
3491
3412
 
3492
- const grouped = groupSessionsByDirectory(state.sessions);
3493
- const treeNodes = buildSessionTreeNodes(grouped);
3494
- renderSessionTreeNodes(treeNodes, sessionList, []);
3495
- updateSidebarActiveSelection();
3413
+ const groups = groupSessionsByContainer(state.sessions);
3414
+ let activeGroup = null;
3415
+ if (state.navLevel === 'agents') {
3416
+ activeGroup = groups.find(function (group) {
3417
+ return group.containerName === state.navContainer;
3418
+ }) || null;
3419
+ if (!activeGroup) {
3420
+ state.navLevel = 'containers';
3421
+ state.navContainer = '';
3422
+ persistSidebarNavState();
3423
+ renderBreadcrumb();
3424
+ }
3425
+ }
3496
3426
 
3427
+ if (containerSearchInput) {
3428
+ containerSearchInput.hidden = state.navLevel !== 'containers';
3429
+ }
3430
+
3431
+ if (state.navLevel === 'agents' && activeGroup) {
3432
+ renderAgentLevel(activeGroup);
3433
+ } else {
3434
+ renderContainerLevel(filterContainerGroupsByQuery(groups, state.containerSearchQuery));
3435
+ }
3436
+
3437
+ updateSidebarActiveSelection();
3497
3438
  scrollActiveSessionIntoView();
3498
3439
  }
3499
3440
 
3441
+ function filterContainerGroupsByQuery(groups, query) {
3442
+ const text = String(query || '').trim().toLowerCase();
3443
+ if (!text) {
3444
+ return groups;
3445
+ }
3446
+ return groups.filter(function (group) {
3447
+ return group.containerName.toLowerCase().includes(text)
3448
+ || String(group.hostPath || '').toLowerCase().includes(text);
3449
+ });
3450
+ }
3451
+
3452
+ function suggestCloneContainerName(baseName) {
3453
+ const taken = new Set(state.sessions.map(getSessionContainerName).filter(Boolean));
3454
+ const escapedBase = baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3455
+ const pattern = new RegExp(`^${escapedBase}-copy(\\d+)$`);
3456
+ let maxN = 0;
3457
+ taken.forEach(function (name) {
3458
+ const matched = name.match(pattern);
3459
+ if (matched) {
3460
+ const n = Number(matched[1]);
3461
+ if (Number.isFinite(n) && n > maxN) {
3462
+ maxN = n;
3463
+ }
3464
+ }
3465
+ });
3466
+ return `${baseName}-copy${maxN + 1}`;
3467
+ }
3468
+
3469
+ function openCloneModal(mode, sourceContainerName) {
3470
+ const source = String(sourceContainerName || '').trim();
3471
+ if (!source) {
3472
+ return;
3473
+ }
3474
+ state.cloneModal.open = true;
3475
+ state.cloneModal.mode = mode;
3476
+ state.cloneModal.sourceContainerName = source;
3477
+ state.cloneModal.nameDraft = suggestCloneContainerName(source);
3478
+ state.cloneModal.submitting = false;
3479
+ state.cloneModal.error = '';
3480
+ renderCloneModal();
3481
+ syncUi();
3482
+ }
3483
+
3484
+ function closeCloneModal() {
3485
+ state.cloneModal.open = false;
3486
+ state.cloneModal.mode = '';
3487
+ state.cloneModal.sourceContainerName = '';
3488
+ state.cloneModal.nameDraft = '';
3489
+ state.cloneModal.submitting = false;
3490
+ state.cloneModal.error = '';
3491
+ renderCloneModal();
3492
+ syncUi();
3493
+ }
3494
+
3495
+ function showCloneModalError(message) {
3496
+ if (!cloneNameError) {
3497
+ return;
3498
+ }
3499
+ const text = String(message || '').trim();
3500
+ cloneNameError.hidden = !text;
3501
+ cloneNameError.textContent = text;
3502
+ }
3503
+
3504
+ function renderCloneModal() {
3505
+ if (!cloneNameModal) {
3506
+ return;
3507
+ }
3508
+ setModalVisible(cloneNameModal, state.cloneModal.open);
3509
+ if (cloneNameTitle) {
3510
+ cloneNameTitle.textContent = state.cloneModal.mode === 'duplicate' ? '复制容器' : '创建相同配置容器';
3511
+ }
3512
+ if (cloneNameTip) {
3513
+ cloneNameTip.textContent = state.cloneModal.mode === 'duplicate'
3514
+ ? `将复制「${state.cloneModal.sourceContainerName}」的配置与全部 AGENT 对话历史到新容器。`
3515
+ : `将以「${state.cloneModal.sourceContainerName}」的配置创建一个新容器,不复制对话历史。`;
3516
+ }
3517
+ if (cloneNameInput && document.activeElement !== cloneNameInput) {
3518
+ cloneNameInput.value = state.cloneModal.nameDraft;
3519
+ }
3520
+ if (cloneNameConfirmBtn) {
3521
+ cloneNameConfirmBtn.disabled = state.cloneModal.submitting;
3522
+ }
3523
+ if (cloneNameCancelBtn) {
3524
+ cloneNameCancelBtn.disabled = state.cloneModal.submitting;
3525
+ }
3526
+ showCloneModalError(state.cloneModal.error);
3527
+ }
3528
+
3529
+ async function submitCloneModal() {
3530
+ if (state.cloneModal.submitting) {
3531
+ return;
3532
+ }
3533
+ const name = String(state.cloneModal.nameDraft || '').trim();
3534
+ const mode = state.cloneModal.mode;
3535
+ const source = state.cloneModal.sourceContainerName;
3536
+ state.cloneModal.submitting = true;
3537
+ showCloneModalError('');
3538
+ renderCloneModal();
3539
+ try {
3540
+ const endpoint = mode === 'duplicate' ? 'duplicate' : 'clone-config';
3541
+ const data = await api('/api/sessions/' + encodeURIComponent(source) + '/' + endpoint, {
3542
+ method: 'POST',
3543
+ body: JSON.stringify(name ? { containerName: name } : {})
3544
+ });
3545
+ closeCloneModal();
3546
+ await loadSessions(data.name);
3547
+ if (data.resumeMayFail) {
3548
+ alert('复制完成。注意:AGENT 对话历史已复制,但底层 CLI 会话续接可能失败,如遇失败请以新对话继续。');
3549
+ }
3550
+ } catch (e) {
3551
+ state.cloneModal.submitting = false;
3552
+ showCloneModalError(e.message);
3553
+ renderCloneModal();
3554
+ }
3555
+ }
3556
+
3500
3557
  function renderMessagesLoading() {
3501
3558
  messagesNode.innerHTML = '';
3502
3559
  state.messageRenderKeys = [];
@@ -3725,7 +3782,7 @@
3725
3782
  function applySessionsSnapshot(rawSessions, preferredName, preferredContainerName) {
3726
3783
  const previousActive = state.active;
3727
3784
  state.sessions = Array.isArray(rawSessions) ? rawSessions : [];
3728
- pruneSidebarTreeState();
3785
+ pruneSidebarNavState();
3729
3786
 
3730
3787
  if (typeof preferredName === 'string' && preferredName.trim()) {
3731
3788
  state.active = preferredName.trim();
@@ -3740,7 +3797,7 @@
3740
3797
  state.active = findLatestCreatedSessionName(state.sessions, preferredContainerName) || state.sessions[0].name;
3741
3798
  }
3742
3799
  if (state.active && state.active !== previousActive) {
3743
- ensureSessionPathExpanded(state.active);
3800
+ ensureSessionNavPath(state.active);
3744
3801
  state.pendingActiveSessionScroll = true;
3745
3802
  }
3746
3803
  if (state.terminal.sessionName && state.terminal.sessionName !== state.active) {
@@ -4506,7 +4563,9 @@
4506
4563
  renderMessages(state.messages, { forceFullRender: true });
4507
4564
  closeComposerOptionsMenu();
4508
4565
  syncUi();
4509
- commandInput.focus();
4566
+ if (!isMobileLayout()) {
4567
+ commandInput.focus();
4568
+ }
4510
4569
  });
4511
4570
  }
4512
4571
 
@@ -4516,7 +4575,9 @@
4516
4575
  renderMessages(state.messages, { forceFullRender: true });
4517
4576
  closeComposerOptionsMenu();
4518
4577
  syncUi();
4519
- commandInput.focus();
4578
+ if (!isMobileLayout()) {
4579
+ commandInput.focus();
4580
+ }
4520
4581
  });
4521
4582
  }
4522
4583
 
@@ -4530,7 +4591,9 @@
4530
4591
  viewActivityBtn.addEventListener('click', function () {
4531
4592
  closeWorkspaceSwitcherMenu();
4532
4593
  setActiveTab('activity');
4533
- commandInput.focus();
4594
+ if (!isMobileLayout()) {
4595
+ commandInput.focus();
4596
+ }
4534
4597
  });
4535
4598
  }
4536
4599
 
@@ -4672,6 +4735,37 @@
4672
4735
  });
4673
4736
  }
4674
4737
 
4738
+ if (cloneNameModal) {
4739
+ cloneNameModal.addEventListener('click', function (event) {
4740
+ if (event.target === cloneNameModal && !state.cloneModal.submitting) {
4741
+ closeCloneModal();
4742
+ }
4743
+ });
4744
+ }
4745
+ if (cloneNameCancelBtn) {
4746
+ cloneNameCancelBtn.addEventListener('click', function () {
4747
+ if (!state.cloneModal.submitting) {
4748
+ closeCloneModal();
4749
+ }
4750
+ });
4751
+ }
4752
+ if (cloneNameConfirmBtn) {
4753
+ cloneNameConfirmBtn.addEventListener('click', function () {
4754
+ submitCloneModal();
4755
+ });
4756
+ }
4757
+ if (cloneNameInput) {
4758
+ cloneNameInput.addEventListener('input', function () {
4759
+ state.cloneModal.nameDraft = cloneNameInput.value;
4760
+ });
4761
+ }
4762
+ if (containerSearchInput) {
4763
+ containerSearchInput.addEventListener('input', function () {
4764
+ state.containerSearchQuery = containerSearchInput.value;
4765
+ renderSessions();
4766
+ });
4767
+ }
4768
+
4675
4769
  window.addEventListener('keydown', function (event) {
4676
4770
  if (event.key === 'Escape' && state.configModalOpen) {
4677
4771
  closeConfigModal();
@@ -4692,6 +4786,9 @@
4692
4786
  closeExternalLinkModalView();
4693
4787
  syncUi();
4694
4788
  }
4789
+ if (event.key === 'Escape' && state.cloneModal.open && !state.cloneModal.submitting) {
4790
+ closeCloneModal();
4791
+ }
4695
4792
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
4696
4793
  closeMobileSessionPanel();
4697
4794
  }
@@ -4802,7 +4899,7 @@
4802
4899
  disconnectTerminal('', true);
4803
4900
  });
4804
4901
 
4805
- loadSidebarTreeState();
4902
+ loadSidebarNavState();
4806
4903
  renderSessions();
4807
4904
  renderMessages(state.messages);
4808
4905
  setMobileSessionPanel(false);