@xcanwin/manyoyo 6.2.5 → 6.2.7

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;
@@ -2428,7 +2363,7 @@
2428
2363
  }
2429
2364
  document.body.classList.toggle(
2430
2365
  'modal-open',
2431
- 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
2432
2367
  );
2433
2368
  if (composer) {
2434
2369
  composer.hidden = !activityTab;
@@ -2844,7 +2779,7 @@
2844
2779
  disconnectTerminal('会话切换,终端已断开', true);
2845
2780
  }
2846
2781
  state.active = sessionName;
2847
- ensureSessionPathExpanded(sessionName);
2782
+ ensureSessionNavPath(sessionName);
2848
2783
  state.sessionDetail = null;
2849
2784
  state.sessionDetailError = '';
2850
2785
  if (isMobileLayout()) {
@@ -2899,48 +2834,33 @@
2899
2834
  }
2900
2835
  }
2901
2836
 
2902
- function groupSessionsByDirectory(sessions) {
2837
+ function groupSessionsByContainer(sessions) {
2903
2838
  const groups = new Map();
2904
2839
  (Array.isArray(sessions) ? sessions : []).forEach(function (session) {
2905
- const directoryPath = String(session && session.hostPath ? session.hostPath : '').trim() || '未配置目录';
2906
- if (!groups.has(directoryPath)) {
2907
- groups.set(directoryPath, {
2908
- path: directoryPath,
2909
- updatedAt: session && session.updatedAt ? session.updatedAt : '',
2910
- containers: new Map()
2911
- });
2912
- }
2913
- const directoryGroup = groups.get(directoryPath);
2914
- if (session && session.updatedAt && (!directoryGroup.updatedAt || new Date(session.updatedAt).getTime() > new Date(directoryGroup.updatedAt).getTime())) {
2915
- directoryGroup.updatedAt = session.updatedAt;
2840
+ const containerName = getSessionContainerName(session);
2841
+ if (!containerName) {
2842
+ return;
2916
2843
  }
2917
-
2918
- const containerName = String(session && session.containerName ? session.containerName : '');
2919
- if (!directoryGroup.containers.has(containerName)) {
2920
- directoryGroup.containers.set(containerName, {
2844
+ if (!groups.has(containerName)) {
2845
+ groups.set(containerName, {
2921
2846
  containerName: containerName,
2847
+ containerRemark: session && session.containerRemark ? session.containerRemark : '',
2922
2848
  status: session && session.status ? session.status : 'history',
2923
2849
  image: session && session.image ? session.image : '',
2850
+ hostPath: getSessionDirectoryPath(session),
2924
2851
  updatedAt: session && session.updatedAt ? session.updatedAt : '',
2925
2852
  sessions: []
2926
2853
  });
2927
2854
  }
2928
- const containerGroup = directoryGroup.containers.get(containerName);
2855
+ const containerGroup = groups.get(containerName);
2929
2856
  containerGroup.sessions.push(session);
2930
2857
  if (session && session.updatedAt && (!containerGroup.updatedAt || new Date(session.updatedAt).getTime() > new Date(containerGroup.updatedAt).getTime())) {
2931
2858
  containerGroup.updatedAt = session.updatedAt;
2932
2859
  }
2933
2860
  });
2934
2861
  return Array.from(groups.values()).map(function (group) {
2935
- group.containers = Array.from(group.containers.values()).sort(function (a, b) {
2936
- const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2937
- const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
2938
- return timeB - timeA;
2939
- });
2940
- group.containers.forEach(function (containerGroup) {
2941
- containerGroup.sessions.sort(function (a, b) {
2942
- return compareSessionByCreatedDesc(a, b);
2943
- });
2862
+ group.sessions.sort(function (a, b) {
2863
+ return compareSessionByCreatedDesc(a, b);
2944
2864
  });
2945
2865
  return group;
2946
2866
  }).sort(function (a, b) {
@@ -3168,254 +3088,272 @@
3168
3088
  }
3169
3089
  }
3170
3090
 
3171
- function createDisclosureButton(expanded, label) {
3172
- const button = document.createElement('button');
3173
- button.type = 'button';
3174
- button.className = 'disclosure-toggle';
3175
- button.dataset.disclosureLabel = label || '';
3176
- button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
3177
- button.setAttribute('aria-label', `${expanded ? '折叠' : '展开'}${label ? ` ${label}` : ''}`);
3178
- button.innerHTML = '<svg viewBox="0 0 12 12" aria-hidden="true" focusable="false"><path d="M4 2.5L8 6L4 9.5"></path></svg>';
3179
- return button;
3180
- }
3181
-
3182
- function createTreePrefixSegment() {
3183
- const segment = document.createElement('span');
3184
- segment.className = 'tree-prefix-segment';
3185
- segment.setAttribute('aria-hidden', 'true');
3186
- return segment;
3187
- }
3188
-
3189
- function createTreePrefix(ancestorHasNext, isLastSibling, options) {
3190
- const opts = options && typeof options === 'object' ? options : {};
3191
- const prefix = document.createElement('div');
3192
- prefix.className = 'tree-prefix';
3193
-
3194
- (Array.isArray(ancestorHasNext) ? ancestorHasNext : []).forEach(function () {
3195
- prefix.appendChild(createTreePrefixSegment());
3196
- });
3197
-
3198
- const branch = document.createElement('span');
3199
- branch.className = `tree-prefix-branch ${isLastSibling ? 'is-last' : 'is-mid'}`;
3200
- branch.setAttribute('aria-hidden', 'true');
3201
- prefix.appendChild(branch);
3202
-
3203
- let disclosure = null;
3204
- let disclosureWrap = null;
3205
- if (opts.expandable) {
3206
- disclosure = createDisclosureButton(!!opts.expanded, opts.label || '');
3207
- disclosureWrap = document.createElement('span');
3208
- disclosureWrap.className = 'tree-prefix-toggle';
3209
- disclosureWrap.appendChild(disclosure);
3210
- prefix.appendChild(disclosureWrap);
3211
- } else {
3212
- const leaf = document.createElement('span');
3213
- leaf.className = 'tree-prefix-leaf';
3214
- leaf.setAttribute('aria-hidden', 'true');
3215
- prefix.appendChild(leaf);
3216
- }
3217
-
3218
- return { root: prefix, disclosure, disclosureWrap };
3219
- }
3220
-
3221
- function setDisclosureExpanded(control, expanded) {
3222
- if (!control) {
3223
- return;
3224
- }
3225
- const nextValue = expanded ? 'true' : 'false';
3226
- if (control.button) {
3227
- control.button.setAttribute('aria-expanded', nextValue);
3228
- }
3229
- if (control.disclosure) {
3230
- control.disclosure.setAttribute('aria-expanded', nextValue);
3231
- const label = control.disclosure.dataset.disclosureLabel || '';
3232
- control.disclosure.setAttribute('aria-label', `${expanded ? '折叠' : '展开'}${label ? ` ${label}` : ''}`);
3233
- }
3234
- }
3235
-
3236
- function createTreeItem(options) {
3091
+ function createSessionCard(options) {
3237
3092
  const opts = options && typeof options === 'object' ? options : {};
3238
- const row = document.createElement('div');
3239
- row.className = `tree-node-row tree-node-row-${opts.kind || 'item'} ${opts.rowClassName || ''}`.trim();
3240
- row.classList.toggle('has-active', !!opts.hasActive);
3241
- row.setAttribute('role', 'none');
3242
-
3243
- const prefixControl = createTreePrefix(opts.ancestorHasNext, !!opts.isLastSibling, {
3244
- expandable: !!opts.expandable,
3245
- expanded: !!opts.expanded,
3246
- label: opts.disclosureLabel || opts.title || ''
3247
- });
3248
- 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);
3249
3096
 
3250
3097
  const button = document.createElement('button');
3251
3098
  button.type = 'button';
3252
- button.className = `tree-node-button tree-node-button-${opts.kind || 'item'} ${opts.className || ''}`.trim();
3253
- button.setAttribute('role', 'treeitem');
3254
- button.setAttribute('aria-level', String(opts.level || 1));
3255
- if (opts.expandable) {
3256
- button.setAttribute('aria-expanded', opts.expanded ? 'true' : 'false');
3257
- }
3099
+ button.className = 'session-card-button';
3100
+ button.classList.toggle('active', !!opts.active);
3258
3101
  if (opts.kind === 'agent') {
3259
3102
  button.setAttribute('aria-selected', opts.active ? 'true' : 'false');
3260
3103
  }
3261
- button.classList.toggle('active', !!opts.active);
3104
+ button.addEventListener('click', function () {
3105
+ if (typeof opts.onClick === 'function') {
3106
+ opts.onClick();
3107
+ }
3108
+ });
3262
3109
 
3263
3110
  const main = document.createElement('div');
3264
- main.className = 'tree-node-main';
3111
+ main.className = 'session-card-main';
3265
3112
 
3266
- const title = document.createElement('div');
3267
- 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';
3268
3117
  title.textContent = opts.title || '';
3269
3118
  title.title = opts.title || '';
3270
- 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);
3271
3127
 
3272
3128
  if (opts.meta) {
3273
3129
  const meta = document.createElement('div');
3274
- meta.className = `tree-node-meta ${opts.metaClassName || ''}`.trim();
3130
+ meta.className = 'session-card-meta';
3275
3131
  meta.textContent = opts.meta;
3276
3132
  main.appendChild(meta);
3277
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
+ }
3278
3141
 
3279
3142
  button.appendChild(main);
3280
- row.appendChild(button);
3143
+ card.appendChild(button);
3281
3144
  if (opts.overlayNode) {
3282
- row.appendChild(opts.overlayNode);
3145
+ card.appendChild(opts.overlayNode);
3283
3146
  }
3284
-
3285
- const control = {
3286
- root: row,
3287
- button,
3288
- disclosure: prefixControl.disclosure
3289
- };
3290
- setDisclosureExpanded(control, !!opts.expanded);
3291
- return control;
3147
+ return { root: card, button };
3292
3148
  }
3293
3149
 
3294
- function buildSessionTreeNodes(grouped) {
3295
- return (Array.isArray(grouped) ? grouped : []).map(function (directoryGroup) {
3296
- return {
3297
- kind: 'directory',
3298
- title: directoryGroup.path,
3299
- disclosureLabel: directoryGroup.path,
3300
- expanded: isDirectoryExpanded(directoryGroup),
3301
- hasActive: directoryContainsActiveSession(directoryGroup),
3302
- onToggle: function (expanded) {
3303
- setSidebarDirectoryExpanded(directoryGroup.path, expanded);
3304
- },
3305
- children: (Array.isArray(directoryGroup.containers) ? directoryGroup.containers : []).map(function (containerGroup) {
3306
- const status = sessionStatusInfo(containerGroup && containerGroup.status);
3307
- const historyClassName = status.tone === 'history' ? 'tree-node-tone-history' : '';
3308
- const containerName = String(containerGroup && containerGroup.containerName ? containerGroup.containerName : '').trim() || '未命名容器';
3309
- const containerMenu = createTreeNodeMenu([
3310
- {
3311
- label: '新建 AGENT',
3312
- onClick: function () {
3313
- createAgentSession(containerName);
3314
- }
3315
- },
3316
- {
3317
- label: '删除容器',
3318
- danger: true,
3319
- onClick: function () {
3320
- removeContainerByName(containerName);
3321
- }
3322
- }
3323
- ]);
3324
-
3325
- return {
3326
- kind: 'container',
3327
- title: containerName,
3328
- meta: status.label,
3329
- metaClassName: `tree-node-status ${status.tone}`,
3330
- className: historyClassName,
3331
- disclosureLabel: containerName,
3332
- expanded: isContainerExpanded(containerGroup),
3333
- hasActive: containerContainsActiveSession(containerGroup),
3334
- overlayNode: containerMenu,
3335
- onToggle: function (expanded) {
3336
- setSidebarContainerExpanded(containerGroup.containerName, expanded);
3337
- },
3338
- children: (Array.isArray(containerGroup.sessions) ? containerGroup.sessions : []).map(function (session) {
3339
- const agentTitle = session.agentName || session.name;
3340
- return {
3341
- kind: 'agent',
3342
- title: agentTitle,
3343
- meta: formatDateTime(session.updatedAt) || '暂无更新',
3344
- className: historyClassName,
3345
- active: state.active === session.name,
3346
- sessionName: session.name,
3347
- overlayNode: createTreeNodeMenu([
3348
- {
3349
- label: '删除 AGENT',
3350
- danger: true,
3351
- onClick: function () {
3352
- removeAgentSessionByName(session.name, agentTitle);
3353
- }
3354
- }
3355
- ])
3356
- };
3357
- })
3358
- };
3359
- })
3360
- };
3361
- });
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();
3362
3165
  }
3363
3166
 
3364
- function renderSessionTreeNodes(nodes, parentNode, ancestorHasNext) {
3365
- (Array.isArray(nodes) ? nodes : []).forEach(function (node, index) {
3366
- const isLastSibling = index === nodes.length - 1;
3367
- const item = createTreeItem({
3368
- kind: node.kind,
3369
- title: node.title,
3370
- meta: node.meta,
3371
- className: node.className,
3372
- metaClassName: node.metaClassName,
3373
- disclosureLabel: node.disclosureLabel,
3374
- expandable: Array.isArray(node.children) && node.children.length > 0,
3375
- expanded: !!node.expanded,
3376
- active: !!node.active,
3377
- hasActive: !!node.hasActive,
3378
- level: ancestorHasNext.length + 1,
3379
- ancestorHasNext: ancestorHasNext,
3380
- isLastSibling: isLastSibling,
3381
- 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');
3382
3183
  });
3184
+ }
3185
+ sessionBreadcrumb.appendChild(rootCrumb);
3383
3186
 
3384
- if (node.kind === 'agent') {
3385
- item.button.dataset.sessionName = node.sessionName || '';
3386
- state.sessionNodeMap.set(node.sessionName, item.button);
3387
- item.button.addEventListener('click', function () {
3388
- handleSessionItemClick(node.sessionName || '');
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
+ }
3200
+
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 });
3389
3212
  });
3390
- parentNode.appendChild(item.root);
3391
- return;
3213
+ sessionBreadcrumb.appendChild(jumpBtn);
3392
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
+ }
3393
3278
 
3394
- const block = document.createElement('section');
3395
- block.className = `tree-node-block tree-node-block-${node.kind}`;
3396
- block.classList.toggle('has-active', !!node.hasActive);
3397
- 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
+ }
3398
3296
 
3399
- const childrenNode = document.createElement('div');
3400
- childrenNode.className = `tree-node-children tree-node-children-${node.kind}`;
3401
- childrenNode.setAttribute('role', 'group');
3402
- childrenNode.hidden = !node.expanded;
3403
- renderSessionTreeNodes(node.children || [], childrenNode, ancestorHasNext.concat(!isLastSibling));
3404
- block.appendChild(childrenNode);
3405
- 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
+ }
3406
3314
 
3407
- const toggleNode = function () {
3408
- const nextExpanded = childrenNode.hidden;
3409
- if (typeof node.onToggle === 'function') {
3410
- node.onToggle(nextExpanded);
3411
- }
3412
- setDisclosureExpanded(item, nextExpanded);
3413
- childrenNode.hidden = !nextExpanded;
3414
- };
3415
- item.button.addEventListener('click', toggleNode);
3416
- if (item.disclosure) {
3417
- item.disclosure.addEventListener('click', toggleNode);
3418
- }
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);
3419
3357
  });
3420
3358
  }
3421
3359
 
@@ -3434,45 +3372,24 @@
3434
3372
 
3435
3373
  function updateSidebarActiveSelection() {
3436
3374
  state.sessionNodeMap.forEach(function (buttonNode, sessionName) {
3437
- const isActive = sessionName === state.active;
3438
3375
  if (!buttonNode) {
3439
3376
  return;
3440
3377
  }
3378
+ const isActive = sessionName === state.active;
3441
3379
  buttonNode.classList.toggle('active', isActive);
3442
3380
  buttonNode.setAttribute('aria-selected', isActive ? 'true' : 'false');
3443
3381
  });
3444
-
3445
- if (!sessionList) {
3446
- return;
3447
- }
3448
-
3449
- sessionList.querySelectorAll('.tree-node-block.has-active').forEach(function (blockNode) {
3450
- blockNode.classList.remove('has-active');
3451
- });
3452
-
3453
- const activeNode = state.sessionNodeMap.get(state.active);
3454
- let cursor = activeNode ? activeNode.parentElement : null;
3455
- while (cursor && cursor !== sessionList) {
3456
- if (cursor.classList && cursor.classList.contains('tree-node-children')) {
3457
- const blockNode = cursor.parentElement;
3458
- if (blockNode && blockNode.classList && blockNode.classList.contains('tree-node-block')) {
3459
- blockNode.classList.add('has-active');
3460
- }
3461
- }
3462
- cursor = cursor.parentElement;
3463
- }
3382
+ renderBreadcrumb();
3464
3383
  }
3465
3384
 
3466
3385
  function renderSessions() {
3467
- const directoryCount = new Set(state.sessions.map(function (session) {
3468
- return String(session && session.hostPath ? session.hostPath : '').trim() || '未配置目录';
3469
- }).filter(Boolean)).size;
3470
3386
  const containerCount = new Set(state.sessions.map(function (session) {
3471
3387
  return session && session.containerName ? session.containerName : '';
3472
3388
  }).filter(Boolean)).size;
3473
3389
  sessionCount.textContent = state.loadingSessions
3474
3390
  ? '加载中...'
3475
- : `${directoryCount} 个 目录 / ${containerCount} 个容器 / ${state.sessions.length} 个 AGENT`;
3391
+ : `${containerCount} 个容器 / ${state.sessions.length} 个 AGENT`;
3392
+ renderBreadcrumb();
3476
3393
 
3477
3394
  if (state.loadingSessions) {
3478
3395
  renderSessionsLoading();
@@ -3490,18 +3407,153 @@
3490
3407
  return;
3491
3408
  }
3492
3409
 
3493
- sessionList.innerHTML = '';
3494
3410
  state.sessionNodeMap.clear();
3495
- state.sessionRenderMode = 'tree';
3411
+ state.sessionRenderMode = 'cards';
3496
3412
 
3497
- const grouped = groupSessionsByDirectory(state.sessions);
3498
- const treeNodes = buildSessionTreeNodes(grouped);
3499
- renderSessionTreeNodes(treeNodes, sessionList, []);
3500
- 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
+ }
3501
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();
3502
3438
  scrollActiveSessionIntoView();
3503
3439
  }
3504
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
+
3505
3557
  function renderMessagesLoading() {
3506
3558
  messagesNode.innerHTML = '';
3507
3559
  state.messageRenderKeys = [];
@@ -3730,7 +3782,7 @@
3730
3782
  function applySessionsSnapshot(rawSessions, preferredName, preferredContainerName) {
3731
3783
  const previousActive = state.active;
3732
3784
  state.sessions = Array.isArray(rawSessions) ? rawSessions : [];
3733
- pruneSidebarTreeState();
3785
+ pruneSidebarNavState();
3734
3786
 
3735
3787
  if (typeof preferredName === 'string' && preferredName.trim()) {
3736
3788
  state.active = preferredName.trim();
@@ -3745,7 +3797,7 @@
3745
3797
  state.active = findLatestCreatedSessionName(state.sessions, preferredContainerName) || state.sessions[0].name;
3746
3798
  }
3747
3799
  if (state.active && state.active !== previousActive) {
3748
- ensureSessionPathExpanded(state.active);
3800
+ ensureSessionNavPath(state.active);
3749
3801
  state.pendingActiveSessionScroll = true;
3750
3802
  }
3751
3803
  if (state.terminal.sessionName && state.terminal.sessionName !== state.active) {
@@ -4683,6 +4735,37 @@
4683
4735
  });
4684
4736
  }
4685
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
+
4686
4769
  window.addEventListener('keydown', function (event) {
4687
4770
  if (event.key === 'Escape' && state.configModalOpen) {
4688
4771
  closeConfigModal();
@@ -4703,6 +4786,9 @@
4703
4786
  closeExternalLinkModalView();
4704
4787
  syncUi();
4705
4788
  }
4789
+ if (event.key === 'Escape' && state.cloneModal.open && !state.cloneModal.submitting) {
4790
+ closeCloneModal();
4791
+ }
4706
4792
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
4707
4793
  closeMobileSessionPanel();
4708
4794
  }
@@ -4813,7 +4899,7 @@
4813
4899
  disconnectTerminal('', true);
4814
4900
  });
4815
4901
 
4816
- loadSidebarTreeState();
4902
+ loadSidebarNavState();
4817
4903
  renderSessions();
4818
4904
  renderMessages(state.messages);
4819
4905
  setMobileSessionPanel(false);