@xcanwin/manyoyo 6.1.1 → 6.2.0

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.
@@ -47,7 +47,8 @@
47
47
  loadingMessages: false,
48
48
  loadingSessionDetail: false,
49
49
  mobileSidebarOpen: false,
50
- mobileActionsOpen: false,
50
+ workspaceSwitcherOpen: false,
51
+ composerOptionsOpen: false,
51
52
  configModalOpen: false,
52
53
  createModalOpen: false,
53
54
  agentTemplateModalOpen: false,
@@ -122,20 +123,18 @@
122
123
  const sessionList = document.getElementById('sessionList');
123
124
  const sessionCount = document.getElementById('sessionCount');
124
125
  const mobileSessionToggle = document.getElementById('mobileSessionToggle');
125
- const mobileActionsToggle = document.getElementById('mobileActionsToggle');
126
- const headerActions = document.getElementById('headerActions');
127
126
  const viewActivityBtn = document.getElementById('viewActivityBtn');
127
+ const workspaceSwitcherToggle = document.getElementById('workspaceSwitcherToggle');
128
+ const workspaceSwitcherPanel = document.getElementById('workspaceSwitcherPanel');
128
129
  const viewTerminalBtn = document.getElementById('viewTerminalBtn');
129
130
  const viewFilesBtn = document.getElementById('viewFilesBtn');
130
131
  const viewDetailBtn = document.getElementById('viewDetailBtn');
131
132
  const viewConfigBtn = document.getElementById('viewConfigBtn');
132
133
  const viewCheckBtn = document.getElementById('viewCheckBtn');
133
- const addAgentBtn = document.getElementById('addAgentBtn');
134
134
  const mobileSidebarClose = document.getElementById('mobileSidebarClose');
135
135
  const sidebarBackdrop = document.getElementById('sidebarBackdrop');
136
136
  const openConfigBtn = document.getElementById('openConfigBtn');
137
137
  const openCreateBtn = document.getElementById('openCreateBtn');
138
- const openCreateMenuBtn = document.getElementById('openCreateMenuBtn');
139
138
  const configModal = document.getElementById('configModal');
140
139
  const configModalTitle = document.getElementById('configModalTitle');
141
140
  const configPath = document.getElementById('configPath');
@@ -178,12 +177,12 @@
178
177
  const directoryPickerError = document.getElementById('directoryPickerError');
179
178
  const directoryPickerCancelBtn = document.getElementById('directoryPickerCancelBtn');
180
179
  const directoryPickerSelectBtn = document.getElementById('directoryPickerSelectBtn');
181
- const activeTitle = document.getElementById('activeTitle');
182
- const activeMeta = document.getElementById('activeMeta');
183
180
  const activityCommandBtn = document.getElementById('activityCommandBtn');
184
181
  const activityAgentBtn = document.getElementById('activityAgentBtn');
185
182
  const agentTemplateBtn = document.getElementById('agentTemplateBtn');
186
183
  const activityModelChip = document.getElementById('activityModelChip');
184
+ const composerOptionsToggle = document.getElementById('composerOptionsToggle');
185
+ const composerOptionsPanel = document.getElementById('composerOptionsPanel');
187
186
  const messagesNode = document.getElementById('messages');
188
187
  const terminalPanel = document.getElementById('terminalPanel');
189
188
  const filesPanel = document.getElementById('filesPanel');
@@ -196,10 +195,7 @@
196
195
  const terminalScreen = document.getElementById('terminalScreen');
197
196
  const composer = document.getElementById('composer');
198
197
  const commandInput = document.getElementById('commandInput');
199
- const composerHint = document.getElementById('composerHint');
200
- const sendState = document.getElementById('sendState');
201
198
  const sendBtn = document.getElementById('sendBtn');
202
- const stopBtn = document.getElementById('stopBtn');
203
199
  const agentTemplateModal = document.getElementById('agentTemplateModal');
204
200
  const agentTemplatePrimary = document.getElementById('agentTemplatePrimary');
205
201
  const containerCliSelect = document.getElementById('containerCliSelect');
@@ -215,9 +211,6 @@
215
211
  const externalLinkUrl = document.getElementById('externalLinkUrl');
216
212
  const externalLinkCancelBtn = document.getElementById('externalLinkCancelBtn');
217
213
  const externalLinkOpenBtn = document.getElementById('externalLinkOpenBtn');
218
- const refreshBtn = document.getElementById('refreshBtn');
219
- const removeBtn = document.getElementById('removeBtn');
220
- const removeAllBtn = document.getElementById('removeAllBtn');
221
214
  const MOBILE_LAYOUT_MEDIA = window.matchMedia('(max-width: 980px)');
222
215
  const MOBILE_COMPACT_MEDIA = window.matchMedia('(max-width: 640px)');
223
216
  const TERMINAL_FIT_DEBOUNCE_MS = 60;
@@ -318,6 +311,40 @@
318
311
  bubble.appendChild(pre);
319
312
  }
320
313
 
314
+ function copyTextToClipboard(text) {
315
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
316
+ return navigator.clipboard.writeText(text);
317
+ }
318
+ return Promise.reject(new Error('clipboard unavailable'));
319
+ }
320
+
321
+ function appendMessageCopyActions(container, items) {
322
+ const actions = document.createElement('div');
323
+ actions.className = 'msg-actions';
324
+ items.forEach(function (item) {
325
+ const btn = document.createElement('button');
326
+ btn.type = 'button';
327
+ btn.className = 'msg-copy-btn';
328
+ btn.textContent = item.label;
329
+ btn.addEventListener('click', function () {
330
+ const text = String(item.getText() || '');
331
+ if (!text) {
332
+ return;
333
+ }
334
+ copyTextToClipboard(text).then(function () {
335
+ btn.textContent = '已复制';
336
+ btn.classList.add('is-copied');
337
+ window.setTimeout(function () {
338
+ btn.textContent = item.label;
339
+ btn.classList.remove('is-copied');
340
+ }, 1200);
341
+ }).catch(function () {});
342
+ });
343
+ actions.appendChild(btn);
344
+ });
345
+ container.appendChild(actions);
346
+ }
347
+
321
348
  function stringifyPrettyJson(value) {
322
349
  if (value === undefined || value === null) {
323
350
  return '';
@@ -545,6 +572,8 @@
545
572
  return card;
546
573
  }
547
574
 
575
+ const traceFlowExpandedState = new Map();
576
+
548
577
  function appendStructuredTraceContent(bubble, message) {
549
578
  bubble.classList.add('trace-bubble');
550
579
  const container = document.createElement('div');
@@ -555,10 +584,33 @@
555
584
  buildStructuredTraceResidualLines(message).forEach(function (line) {
556
585
  flow.appendChild(createResidualTraceCard(line));
557
586
  });
558
- (Array.isArray(message && message.traceEvents) ? message.traceEvents : []).forEach(function (traceEvent) {
587
+ const traceEvents = Array.isArray(message && message.traceEvents) ? message.traceEvents : [];
588
+ traceEvents.forEach(function (traceEvent) {
559
589
  flow.appendChild(createTraceEventCard(traceEvent));
560
590
  });
561
- container.appendChild(flow);
591
+
592
+ const messageId = message && message.id ? message.id : '';
593
+ const pending = Boolean(state.agentRun.active && state.agentRun.traceMessageId === messageId);
594
+ const summaryInfo = window.ManyoyoChatBehavior.summarizeTraceFlow(traceEvents, { pending });
595
+ const hasError = traceEvents.some(function (event) {
596
+ return event && event.kind === 'error';
597
+ });
598
+ const defaultOpen = hasError;
599
+ const toggle = document.createElement('details');
600
+ toggle.className = 'trace-flow-toggle';
601
+ toggle.open = traceFlowExpandedState.has(messageId) ? traceFlowExpandedState.get(messageId) : defaultOpen;
602
+ toggle.addEventListener('toggle', function () {
603
+ if (messageId) {
604
+ traceFlowExpandedState.set(messageId, toggle.open);
605
+ }
606
+ });
607
+
608
+ const summary = document.createElement('summary');
609
+ summary.className = 'trace-flow-toggle-summary';
610
+ summary.textContent = summaryInfo.label;
611
+ toggle.appendChild(summary);
612
+ toggle.appendChild(flow);
613
+ container.appendChild(toggle);
562
614
 
563
615
  bubble.appendChild(container);
564
616
  }
@@ -570,7 +622,7 @@
570
622
  return 'AGENT 实时回复';
571
623
  }
572
624
  if (message && message.streamTrace) {
573
- return 'AGENT 过程';
625
+ return 'AGENT 回复';
574
626
  }
575
627
  if (message && message.mode === 'agent') {
576
628
  return 'AGENT 回复';
@@ -1573,17 +1625,6 @@
1573
1625
  return '自动';
1574
1626
  }
1575
1627
 
1576
- function buildActiveMeta(session) {
1577
- if (!session) {
1578
- return '会话不可用';
1579
- }
1580
- const status = sessionStatusInfo(session.status);
1581
- const messageCount = safeMessageCount(session.messageCount);
1582
- const updatedAt = formatDateTime(session.updatedAt) || '暂无更新';
1583
- const containerName = session.containerName || '未绑定容器';
1584
- return `${containerName} · ${status.label} · ${messageCount} 条对话 · ${updatedAt}`;
1585
- }
1586
-
1587
1628
  function buildMessageMetaLines(message) {
1588
1629
  const lines = [];
1589
1630
  const timeText = formatDateTime(message && message.timestamp);
@@ -1918,16 +1959,34 @@
1918
1959
  setMobileSessionPanel(false);
1919
1960
  }
1920
1961
 
1921
- function setMobileActionsMenu(open) {
1922
- state.mobileActionsOpen = Boolean(open);
1923
- document.body.classList.toggle('mobile-actions-open', state.mobileActionsOpen);
1924
- if (mobileActionsToggle) {
1925
- mobileActionsToggle.setAttribute('aria-expanded', state.mobileActionsOpen ? 'true' : 'false');
1962
+ function setWorkspaceSwitcherMenu(open) {
1963
+ state.workspaceSwitcherOpen = Boolean(open);
1964
+ document.body.classList.toggle('workspace-switcher-open', state.workspaceSwitcherOpen);
1965
+ if (workspaceSwitcherToggle) {
1966
+ workspaceSwitcherToggle.setAttribute('aria-expanded', state.workspaceSwitcherOpen ? 'true' : 'false');
1967
+ }
1968
+ if (workspaceSwitcherPanel) {
1969
+ workspaceSwitcherPanel.hidden = !state.workspaceSwitcherOpen;
1970
+ }
1971
+ }
1972
+
1973
+ function closeWorkspaceSwitcherMenu() {
1974
+ setWorkspaceSwitcherMenu(false);
1975
+ }
1976
+
1977
+ function setComposerOptionsMenu(open) {
1978
+ state.composerOptionsOpen = Boolean(open);
1979
+ document.body.classList.toggle('composer-options-open', state.composerOptionsOpen);
1980
+ if (composerOptionsToggle) {
1981
+ composerOptionsToggle.setAttribute('aria-expanded', state.composerOptionsOpen ? 'true' : 'false');
1982
+ }
1983
+ if (composerOptionsPanel) {
1984
+ composerOptionsPanel.hidden = !state.composerOptionsOpen;
1926
1985
  }
1927
1986
  }
1928
1987
 
1929
- function closeMobileActionsMenu() {
1930
- setMobileActionsMenu(false);
1988
+ function closeComposerOptionsMenu() {
1989
+ setComposerOptionsMenu(false);
1931
1990
  }
1932
1991
 
1933
1992
  const VIEW_LABELS = {
@@ -2199,15 +2258,15 @@
2199
2258
 
2200
2259
  function syncUi() {
2201
2260
  if (!state.active) {
2202
- activeTitle.textContent = '未选择会话';
2203
- activeMeta.textContent = '请选择左侧会话';
2261
+ document.title = window.ManyoyoChatBehavior.buildDocumentTitle('');
2204
2262
  if (isComposerMode()) {
2205
2263
  commandInput.value = '';
2206
2264
  }
2207
2265
  } else {
2208
2266
  const activeSession = getActiveSession();
2209
- activeTitle.textContent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
2210
- activeMeta.textContent = buildActiveMeta(activeSession);
2267
+ document.title = window.ManyoyoChatBehavior.buildDocumentTitle(
2268
+ activeSession && activeSession.agentName ? activeSession.agentName : state.active
2269
+ );
2211
2270
  }
2212
2271
 
2213
2272
  const activityTab = state.activeTab === 'activity';
@@ -2253,6 +2312,7 @@
2253
2312
  if (viewDetailBtn) viewDetailBtn.classList.toggle('is-active', detailTab);
2254
2313
  if (viewConfigBtn) viewConfigBtn.classList.toggle('is-active', configTab);
2255
2314
  if (viewCheckBtn) viewCheckBtn.classList.toggle('is-active', checkTab);
2315
+ if (workspaceSwitcherToggle) workspaceSwitcherToggle.classList.toggle('is-active', !activityTab);
2256
2316
  if (messagesNode) {
2257
2317
  messagesNode.hidden = !activityTab;
2258
2318
  }
@@ -2285,16 +2345,15 @@
2285
2345
 
2286
2346
  const activeAgentRunning = isAgentRunActiveForSession(state.active) || hasPendingAgentMessagesForSession(state.active);
2287
2347
  const busy = state.loadingSessions || state.loadingMessages || state.sending || state.creatingAgent;
2288
- refreshBtn.disabled = busy;
2289
- if (addAgentBtn) {
2290
- addAgentBtn.disabled = !state.active || busy;
2291
- addAgentBtn.textContent = state.creatingAgent ? '新建中...' : '新建 AGENT';
2292
- }
2293
- removeBtn.disabled = !state.active || busy;
2294
- removeAllBtn.disabled = !state.active || busy;
2295
- sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && (!agentEnabled || activeAgentRunning));
2296
- if (stopBtn) {
2297
- stopBtn.disabled = !activityTab || !agentMode || !activeAgentRunning || state.agentRun.stopping;
2348
+ const showStop = agentMode && activeAgentRunning;
2349
+ if (showStop) {
2350
+ sendBtn.disabled = !activityTab || !state.active || state.agentRun.stopping;
2351
+ sendBtn.textContent = state.agentRun.stopping ? '停止中…' : '停止';
2352
+ sendBtn.classList.add('danger-outline');
2353
+ } else {
2354
+ sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && !agentEnabled);
2355
+ sendBtn.textContent = '发送';
2356
+ sendBtn.classList.remove('danger-outline');
2298
2357
  }
2299
2358
  if (agentTemplateBtn) {
2300
2359
  agentTemplateBtn.disabled = !state.active || state.agentTemplateSaving;
@@ -2305,17 +2364,9 @@
2305
2364
  ? '输入提示词,例如:请帮我分析当前项目结构并给出重构建议'
2306
2365
  : '输入容器命令,例如: ls -la';
2307
2366
  }
2308
- if (composerHint) {
2309
- composerHint.textContent = agentMode
2310
- ? 'Enter 发送提示词 · Shift/Alt + Enter 换行 · 执行中可停止'
2311
- : 'Enter 发送 · Shift/Alt + Enter 换行';
2312
- }
2313
2367
  if (openCreateBtn) {
2314
2368
  openCreateBtn.disabled = state.createLoading || state.createSubmitting;
2315
2369
  }
2316
- if (openCreateMenuBtn) {
2317
- openCreateMenuBtn.disabled = state.createLoading || state.createSubmitting;
2318
- }
2319
2370
  if (openConfigBtn) {
2320
2371
  openConfigBtn.disabled = state.configLoading || state.configSaving;
2321
2372
  }
@@ -2342,24 +2393,6 @@
2342
2393
  if (createCancelBtn) {
2343
2394
  createCancelBtn.disabled = state.createSubmitting;
2344
2395
  }
2345
- if (sendState) {
2346
- if (!state.active) {
2347
- sendState.textContent = '未选择会话';
2348
- sendState.classList.remove('is-active');
2349
- } else if (state.creatingAgent) {
2350
- sendState.textContent = '正在新建 AGENT…';
2351
- sendState.classList.add('is-active');
2352
- } else if (activeAgentRunning && agentMode) {
2353
- sendState.textContent = state.agentRun.stopping ? '正在停止 Agent…' : 'Agent 执行中';
2354
- sendState.classList.add('is-active');
2355
- } else if (busy) {
2356
- sendState.textContent = '处理中';
2357
- sendState.classList.add('is-active');
2358
- } else {
2359
- sendState.textContent = agentMode ? 'Agent 就绪' : '命令就绪';
2360
- sendState.classList.remove('is-active');
2361
- }
2362
- }
2363
2396
  if (configModal) {
2364
2397
  configModal.hidden = !state.configModalOpen;
2365
2398
  }
@@ -2400,25 +2433,10 @@
2400
2433
  'modal-open',
2401
2434
  state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen
2402
2435
  );
2403
- if (!state.active) {
2404
- sendState.textContent = '未选择会话';
2405
- } else if (state.creatingAgent) {
2406
- sendState.textContent = '正在新建 AGENT…';
2407
- } else if (agentMode && !agentEnabled) {
2408
- sendState.textContent = '当前会话未配置 AGENT 模板';
2409
- } else if (state.sending) {
2410
- sendState.textContent = '发送中...';
2411
- } else if (state.loadingSessions || state.loadingMessages) {
2412
- sendState.textContent = '加载中...';
2413
- } else {
2414
- sendState.textContent = '就绪';
2415
- }
2416
- sendState.classList.toggle('is-active', state.sending || state.creatingAgent);
2417
2436
  if (composer) {
2418
2437
  composer.hidden = !activityTab;
2419
2438
  }
2420
2439
  setMobileSessionPanel(state.mobileSidebarOpen);
2421
- setMobileActionsMenu(state.mobileActionsOpen);
2422
2440
  renderSessionDetailPanels();
2423
2441
  }
2424
2442
 
@@ -3041,6 +3059,122 @@
3041
3059
  return findLatestCreatedSessionName(remaining, removedRef.containerName);
3042
3060
  }
3043
3061
 
3062
+ let openTreeNodeMenu = null;
3063
+
3064
+ function closeOpenTreeNodeMenu() {
3065
+ if (openTreeNodeMenu) {
3066
+ const menu = openTreeNodeMenu;
3067
+ openTreeNodeMenu = null;
3068
+ menu.close();
3069
+ }
3070
+ }
3071
+
3072
+ function createTreeNodeMenu(items) {
3073
+ const wrap = document.createElement('div');
3074
+ wrap.className = 'tree-node-menu';
3075
+
3076
+ const trigger = document.createElement('button');
3077
+ trigger.type = 'button';
3078
+ trigger.className = 'secondary tree-node-menu-trigger';
3079
+ trigger.setAttribute('aria-haspopup', 'true');
3080
+ trigger.setAttribute('aria-expanded', 'false');
3081
+ trigger.setAttribute('aria-label', '更多操作');
3082
+ trigger.innerHTML = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><circle cx="3" cy="8" r="1.4"></circle><circle cx="8" cy="8" r="1.4"></circle><circle cx="13" cy="8" r="1.4"></circle></svg>';
3083
+
3084
+ const panel = document.createElement('div');
3085
+ panel.className = 'tree-node-menu-panel';
3086
+ panel.hidden = true;
3087
+
3088
+ function close() {
3089
+ panel.hidden = true;
3090
+ trigger.setAttribute('aria-expanded', 'false');
3091
+ wrap.classList.remove('is-open');
3092
+ }
3093
+
3094
+ trigger.addEventListener('click', function (event) {
3095
+ event.stopPropagation();
3096
+ if (panel.hidden) {
3097
+ closeOpenTreeNodeMenu();
3098
+ panel.hidden = false;
3099
+ trigger.setAttribute('aria-expanded', 'true');
3100
+ wrap.classList.add('is-open');
3101
+ openTreeNodeMenu = { wrap, close };
3102
+ } else {
3103
+ close();
3104
+ openTreeNodeMenu = null;
3105
+ }
3106
+ });
3107
+
3108
+ (Array.isArray(items) ? items : []).forEach(function (item) {
3109
+ const itemBtn = document.createElement('button');
3110
+ itemBtn.type = 'button';
3111
+ itemBtn.className = 'tree-node-menu-panel-item' + (item.danger ? ' is-danger' : '');
3112
+ itemBtn.textContent = item.label;
3113
+ itemBtn.addEventListener('click', function (event) {
3114
+ event.stopPropagation();
3115
+ close();
3116
+ openTreeNodeMenu = null;
3117
+ if (typeof item.onClick === 'function') {
3118
+ item.onClick();
3119
+ }
3120
+ });
3121
+ panel.appendChild(itemBtn);
3122
+ });
3123
+
3124
+ wrap.appendChild(trigger);
3125
+ wrap.appendChild(panel);
3126
+ return wrap;
3127
+ }
3128
+
3129
+ async function removeContainerByName(containerName) {
3130
+ const target = String(containerName || '').trim();
3131
+ if (!target) return;
3132
+ const yes = confirm('确认删除容器 ' + target + ' ?');
3133
+ if (!yes) return;
3134
+ try {
3135
+ const wasActiveContainer = parseSessionKey(state.active).containerName === target;
3136
+ if (parseSessionKey(state.terminal.sessionName).containerName === target
3137
+ && (state.terminal.connected || state.terminal.connecting)) {
3138
+ disconnectTerminal('容器删除,终端已断开', true);
3139
+ }
3140
+ await api('/api/sessions/' + encodeURIComponent(target) + '/remove', {
3141
+ method: 'POST'
3142
+ });
3143
+ await refreshSessions({
3144
+ preferredName: wasActiveContainer ? target : (state.active || ''),
3145
+ withLoading: true,
3146
+ reloadMessages: wasActiveContainer
3147
+ });
3148
+ } catch (e) {
3149
+ alert(e.message);
3150
+ }
3151
+ }
3152
+
3153
+ async function removeAgentSessionByName(sessionName, agentLabel) {
3154
+ const target = String(sessionName || '').trim();
3155
+ if (!target) return;
3156
+ const yes = confirm('确认删除 AGENT ' + (agentLabel || target) + ' ?');
3157
+ if (!yes) return;
3158
+ const isActive = target === state.active;
3159
+ try {
3160
+ const containerName = parseSessionKey(target).containerName;
3161
+ const fallbackSessionName = isActive
3162
+ ? findPreferredSessionNameAfterRemoval(state.sessions, target)
3163
+ : '';
3164
+ await api('/api/sessions/' + encodeURIComponent(target) + '/remove-with-history', {
3165
+ method: 'POST'
3166
+ });
3167
+ await refreshSessions({
3168
+ preferredName: isActive ? (fallbackSessionName || '') : (state.active || ''),
3169
+ preferredContainerName: containerName,
3170
+ withLoading: true,
3171
+ reloadMessages: isActive
3172
+ });
3173
+ } catch (e) {
3174
+ alert(e.message);
3175
+ }
3176
+ }
3177
+
3044
3178
  function createDisclosureButton(expanded, label) {
3045
3179
  const button = document.createElement('button');
3046
3180
  button.type = 'button';
@@ -3179,18 +3313,21 @@
3179
3313
  const status = sessionStatusInfo(containerGroup && containerGroup.status);
3180
3314
  const historyClassName = status.tone === 'history' ? 'tree-node-tone-history' : '';
3181
3315
  const containerName = String(containerGroup && containerGroup.containerName ? containerGroup.containerName : '').trim() || '未命名容器';
3182
- const hoverMenu = document.createElement('div');
3183
- hoverMenu.className = 'tree-node-hover-menu';
3184
-
3185
- const addAgentBtn = document.createElement('button');
3186
- addAgentBtn.type = 'button';
3187
- addAgentBtn.className = 'secondary tree-node-menu-item';
3188
- addAgentBtn.textContent = '新建 AGENT';
3189
- addAgentBtn.addEventListener('click', function (event) {
3190
- event.stopPropagation();
3191
- createAgentSession(containerName);
3192
- });
3193
- hoverMenu.appendChild(addAgentBtn);
3316
+ const containerMenu = createTreeNodeMenu([
3317
+ {
3318
+ label: '新建 AGENT',
3319
+ onClick: function () {
3320
+ createAgentSession(containerName);
3321
+ }
3322
+ },
3323
+ {
3324
+ label: '删除容器',
3325
+ danger: true,
3326
+ onClick: function () {
3327
+ removeContainerByName(containerName);
3328
+ }
3329
+ }
3330
+ ]);
3194
3331
 
3195
3332
  return {
3196
3333
  kind: 'container',
@@ -3201,18 +3338,28 @@
3201
3338
  disclosureLabel: containerName,
3202
3339
  expanded: isContainerExpanded(containerGroup),
3203
3340
  hasActive: containerContainsActiveSession(containerGroup),
3204
- overlayNode: hoverMenu,
3341
+ overlayNode: containerMenu,
3205
3342
  onToggle: function (expanded) {
3206
3343
  setSidebarContainerExpanded(containerGroup.containerName, expanded);
3207
3344
  },
3208
3345
  children: (Array.isArray(containerGroup.sessions) ? containerGroup.sessions : []).map(function (session) {
3346
+ const agentTitle = session.agentName || session.name;
3209
3347
  return {
3210
3348
  kind: 'agent',
3211
- title: session.agentName || session.name,
3349
+ title: agentTitle,
3212
3350
  meta: formatDateTime(session.updatedAt) || '暂无更新',
3213
3351
  className: historyClassName,
3214
3352
  active: state.active === session.name,
3215
- sessionName: session.name
3353
+ sessionName: session.name,
3354
+ overlayNode: createTreeNodeMenu([
3355
+ {
3356
+ label: '删除 AGENT',
3357
+ danger: true,
3358
+ onClick: function () {
3359
+ removeAgentSessionByName(session.name, agentTitle);
3360
+ }
3361
+ }
3362
+ ])
3216
3363
  };
3217
3364
  })
3218
3365
  };
@@ -3376,9 +3523,13 @@
3376
3523
  }
3377
3524
 
3378
3525
  function isMessagesNearBottom(thresholdPx) {
3379
- const threshold = Number.isFinite(thresholdPx) ? thresholdPx : 40;
3380
3526
  if (!messagesNode) return true;
3381
- return (messagesNode.scrollHeight - (messagesNode.scrollTop + messagesNode.clientHeight)) <= threshold;
3527
+ return window.ManyoyoChatBehavior.isNearBottom(
3528
+ messagesNode.scrollTop,
3529
+ messagesNode.scrollHeight,
3530
+ messagesNode.clientHeight,
3531
+ thresholdPx
3532
+ );
3382
3533
  }
3383
3534
 
3384
3535
  function scrollMessagesToBottomImmediate() {
@@ -3396,18 +3547,21 @@
3396
3547
  }
3397
3548
 
3398
3549
  function getMessageRenderKey(msg, index) {
3550
+ const pairedTrace = msg && msg.pairedTrace ? msg.pairedTrace : null;
3551
+ const tracePart = pairedTrace ? `|paired:${String(pairedTrace.content || '')}` : '';
3399
3552
  if (msg && msg.id && msg.streamingReply) {
3400
3553
  const content = msg.content ? String(msg.content) : '';
3401
3554
  const timestamp = msg.timestamp ? String(msg.timestamp) : '';
3402
- return `id:${msg.id}|streaming|${timestamp}|${content}`;
3555
+ return `id:${msg.id}|streaming|${timestamp}|${content}${tracePart}`;
3403
3556
  }
3404
3557
  if (msg && msg.id && msg.streamTrace) {
3405
3558
  const content = msg.content ? String(msg.content) : '';
3406
3559
  const timestamp = msg.timestamp ? String(msg.timestamp) : '';
3407
- return `id:${msg.id}|trace|${timestamp}|${content}`;
3560
+ const running = (state.agentRun.active && state.agentRun.traceMessageId === msg.id) ? '1' : '0';
3561
+ return `id:${msg.id}|trace|${timestamp}|${content}|${running}`;
3408
3562
  }
3409
3563
  if (msg && msg.id) {
3410
- return `id:${msg.id}`;
3564
+ return `id:${msg.id}${tracePart}`;
3411
3565
  }
3412
3566
  const role = msg && msg.role ? String(msg.role) : '';
3413
3567
  const mode = msg && msg.mode ? String(msg.mode) : '';
@@ -3415,7 +3569,7 @@
3415
3569
  const exitCode = msg && typeof msg.exitCode === 'number' ? String(msg.exitCode) : '';
3416
3570
  const pending = msg && msg.pending ? '1' : '0';
3417
3571
  const content = msg && msg.content ? String(msg.content) : '';
3418
- return `idx:${index}|${role}|${mode}|${timestamp}|${exitCode}|${pending}|${content}`;
3572
+ return `idx:${index}|${role}|${mode}|${timestamp}|${exitCode}|${pending}|${content}${tracePart}`;
3419
3573
  }
3420
3574
 
3421
3575
  function resolveMessageOrigin(msg) {
@@ -3452,13 +3606,12 @@
3452
3606
  const bubble = document.createElement('div');
3453
3607
  bubble.className = 'bubble';
3454
3608
 
3609
+ if (msg && msg.pairedTrace) {
3610
+ appendStructuredTraceContent(bubble, msg.pairedTrace);
3611
+ }
3612
+
3455
3613
  const isStreamingReply = Boolean(msg && msg.streamingReply);
3456
- const shouldRenderStructuredTrace = Boolean(
3457
- msg
3458
- && msg.streamTrace
3459
- && Array.isArray(msg.traceEvents)
3460
- && msg.traceEvents.length
3461
- );
3614
+ const shouldRenderStructuredTrace = Boolean(msg && msg.streamTrace);
3462
3615
  const shouldRenderMarkdown = Boolean(!msg.streamTrace && !msg.streamingReply && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
3463
3616
  if (isStreamingReply) {
3464
3617
  bubble.classList.add('streaming-reply');
@@ -3498,11 +3651,20 @@
3498
3651
  if (renderedMarkdown) {
3499
3652
  markdownNode.innerHTML = renderedMarkdown;
3500
3653
  bubble.appendChild(markdownNode);
3654
+ appendMessageCopyActions(bubble, [
3655
+ { label: '复制文本', getText: function () { return markdownNode.innerText || markdownNode.textContent || ''; } },
3656
+ { label: '复制 Markdown', getText: function () { return msg.content; } }
3657
+ ]);
3501
3658
  } else {
3502
3659
  appendPlainMessageContent(bubble, msg.content);
3503
3660
  }
3504
3661
  } else {
3505
3662
  appendPlainMessageContent(bubble, msg.content);
3663
+ if (msg.role === 'user') {
3664
+ appendMessageCopyActions(bubble, [
3665
+ { label: '复制', getText: function () { return msg.content; } }
3666
+ ]);
3667
+ }
3506
3668
  }
3507
3669
 
3508
3670
  row.appendChild(meta);
@@ -3510,7 +3672,8 @@
3510
3672
  return row;
3511
3673
  }
3512
3674
 
3513
- function renderMessages(messages, options) {
3675
+ function renderMessages(rawMessages, options) {
3676
+ const messages = window.ManyoyoChatBehavior.mergeTraceIntoReply(rawMessages);
3514
3677
  const renderOptions = options && typeof options === 'object' ? options : {};
3515
3678
  const stickToBottom = renderOptions.stickToBottom === true || isMessagesNearBottom(40);
3516
3679
 
@@ -3932,7 +4095,7 @@
3932
4095
  traceLines.push(line);
3933
4096
  updateAgentTraceMessageLocal(sessionName, traceMessageId, traceLines.join('\n'), traceEvent);
3934
4097
  if (state.active === sessionName) {
3935
- renderMessages(state.messages, { stickToBottom: true });
4098
+ renderMessages(state.messages);
3936
4099
  }
3937
4100
  }
3938
4101
 
@@ -3971,7 +4134,7 @@
3971
4134
  }
3972
4135
  updateStreamingReplyLocal(sessionName, streamingReplyId, content);
3973
4136
  if (state.active === sessionName) {
3974
- renderMessages(state.messages, { stickToBottom: true });
4137
+ renderMessages(state.messages);
3975
4138
  }
3976
4139
  return;
3977
4140
  }
@@ -4007,6 +4170,9 @@
4007
4170
  }
4008
4171
  }
4009
4172
  finalizeAgentRunState();
4173
+ if (state.active === sessionName) {
4174
+ renderMessages(state.messages);
4175
+ }
4010
4176
  syncUi();
4011
4177
  }
4012
4178
 
@@ -4019,7 +4185,7 @@
4019
4185
 
4020
4186
  appendAssistantMessageLocal(sessionName, finalResult, 'agent');
4021
4187
  if (state.active === sessionName) {
4022
- renderMessages(state.messages, { stickToBottom: true });
4188
+ renderMessages(state.messages);
4023
4189
  }
4024
4190
  bumpSessionMetaAfterSend(sessionName);
4025
4191
  refreshSessionsSilent({ preferredName: sessionName }).catch(function () {
@@ -4039,15 +4205,9 @@
4039
4205
  });
4040
4206
  }
4041
4207
 
4042
- if (openCreateMenuBtn) {
4043
- openCreateMenuBtn.addEventListener('click', function () {
4044
- closeMobileActionsMenu();
4045
- openCreateModal();
4046
- });
4047
- }
4048
-
4049
4208
  if (agentTemplateBtn) {
4050
4209
  agentTemplateBtn.addEventListener('click', function () {
4210
+ closeComposerOptionsMenu();
4051
4211
  openAgentTemplateModal().catch(function (e) {
4052
4212
  alert(e && e.message ? e.message : '加载 Agent 模板失败');
4053
4213
  });
@@ -4239,10 +4399,29 @@
4239
4399
  composer.addEventListener('submit', async function (event) {
4240
4400
  event.preventDefault();
4241
4401
  if (!state.active) return;
4242
- if (state.sending) return;
4243
- if (state.loadingSessions || state.loadingMessages) return;
4244
4402
  if (state.activeTab !== 'activity') return;
4403
+
4245
4404
  const mode = state.mode === 'agent' ? 'agent' : 'command';
4405
+ const activeAgentRunning = isAgentRunActiveForSession(state.active) || hasPendingAgentMessagesForSession(state.active);
4406
+ if (mode === 'agent' && activeAgentRunning) {
4407
+ if (state.agentRun.stopping) return;
4408
+ state.agentRun.stopping = true;
4409
+ syncUi();
4410
+ try {
4411
+ await api('/api/sessions/' + encodeURIComponent(state.active) + '/agent/stop', {
4412
+ method: 'POST',
4413
+ body: JSON.stringify({})
4414
+ });
4415
+ } catch (e) {
4416
+ alert(e.message);
4417
+ state.agentRun.stopping = false;
4418
+ syncUi();
4419
+ }
4420
+ return;
4421
+ }
4422
+
4423
+ if (state.sending) return;
4424
+ if (state.loadingSessions || state.loadingMessages) return;
4246
4425
  if (mode === 'agent' && !isActiveAgentEnabled()) {
4247
4426
  syncUi();
4248
4427
  return;
@@ -4285,7 +4464,7 @@
4285
4464
  }
4286
4465
  appendAssistantMessageLocal(submitSession, runResult, mode);
4287
4466
  if (state.active === submitSession) {
4288
- renderMessages(state.messages, { stickToBottom: true });
4467
+ renderMessages(state.messages);
4289
4468
  }
4290
4469
  bumpSessionMetaAfterSend(submitSession);
4291
4470
  refreshSessionsSilent({ preferredName: submitSession }).catch(function () {
@@ -4303,37 +4482,16 @@
4303
4482
  state.messages = state.messages.filter(function (message) {
4304
4483
  return !(message && message.id === pendingMessage.id);
4305
4484
  });
4306
- renderMessages(state.messages, { stickToBottom: true });
4485
+ renderMessages(state.messages);
4307
4486
  }
4308
4487
  alert(e.message);
4309
4488
  }
4310
4489
  } finally {
4311
4490
  state.sending = false;
4312
4491
  syncUi();
4313
- commandInput.focus();
4314
4492
  }
4315
4493
  });
4316
4494
 
4317
- if (stopBtn) {
4318
- stopBtn.addEventListener('click', async function () {
4319
- if (!state.active || !isAgentRunActiveForSession(state.active) || state.agentRun.stopping) {
4320
- return;
4321
- }
4322
- state.agentRun.stopping = true;
4323
- syncUi();
4324
- try {
4325
- await api('/api/sessions/' + encodeURIComponent(state.active) + '/agent/stop', {
4326
- method: 'POST',
4327
- body: JSON.stringify({})
4328
- });
4329
- } catch (e) {
4330
- alert(e.message);
4331
- state.agentRun.stopping = false;
4332
- syncUi();
4333
- }
4334
- });
4335
- }
4336
-
4337
4495
  commandInput.addEventListener('keydown', function (event) {
4338
4496
  if (event.key !== 'Enter' || event.isComposing) {
4339
4497
  return;
@@ -4356,6 +4514,7 @@
4356
4514
  activityCommandBtn.addEventListener('click', function () {
4357
4515
  state.mode = 'command';
4358
4516
  renderMessages(state.messages, { forceFullRender: true });
4517
+ closeComposerOptionsMenu();
4359
4518
  syncUi();
4360
4519
  commandInput.focus();
4361
4520
  });
@@ -4365,13 +4524,21 @@
4365
4524
  activityAgentBtn.addEventListener('click', function () {
4366
4525
  state.mode = 'agent';
4367
4526
  renderMessages(state.messages, { forceFullRender: true });
4527
+ closeComposerOptionsMenu();
4368
4528
  syncUi();
4369
4529
  commandInput.focus();
4370
4530
  });
4371
4531
  }
4372
4532
 
4533
+ if (composerOptionsToggle) {
4534
+ composerOptionsToggle.addEventListener('click', function () {
4535
+ setComposerOptionsMenu(!state.composerOptionsOpen);
4536
+ });
4537
+ }
4538
+
4373
4539
  if (viewActivityBtn) {
4374
4540
  viewActivityBtn.addEventListener('click', function () {
4541
+ closeWorkspaceSwitcherMenu();
4375
4542
  setActiveTab('activity');
4376
4543
  commandInput.focus();
4377
4544
  });
@@ -4379,39 +4546,39 @@
4379
4546
 
4380
4547
  if (viewTerminalBtn) {
4381
4548
  viewTerminalBtn.addEventListener('click', function () {
4549
+ closeWorkspaceSwitcherMenu();
4382
4550
  setActiveTab('terminal');
4383
4551
  });
4384
4552
  }
4385
4553
 
4386
4554
  if (viewFilesBtn) {
4387
4555
  viewFilesBtn.addEventListener('click', function () {
4556
+ closeWorkspaceSwitcherMenu();
4388
4557
  setActiveTab('files');
4389
4558
  });
4390
4559
  }
4391
4560
 
4392
4561
  if (viewDetailBtn) {
4393
4562
  viewDetailBtn.addEventListener('click', function () {
4563
+ closeWorkspaceSwitcherMenu();
4394
4564
  setActiveTab('detail');
4395
4565
  });
4396
4566
  }
4397
4567
 
4398
4568
  if (viewConfigBtn) {
4399
4569
  viewConfigBtn.addEventListener('click', function () {
4570
+ closeWorkspaceSwitcherMenu();
4400
4571
  setActiveTab('config');
4401
4572
  });
4402
4573
  }
4403
4574
 
4404
4575
  if (viewCheckBtn) {
4405
4576
  viewCheckBtn.addEventListener('click', function () {
4577
+ closeWorkspaceSwitcherMenu();
4406
4578
  setActiveTab('check');
4407
4579
  });
4408
4580
  }
4409
4581
 
4410
- refreshBtn.addEventListener('click', function () {
4411
- closeMobileActionsMenu();
4412
- loadSessions(state.active).catch(function (e) { alert(e.message); });
4413
- });
4414
-
4415
4582
  const TERM_KEY_SEQUENCES = {
4416
4583
  esc: '\x1b',
4417
4584
  tab: '\x09',
@@ -4453,9 +4620,9 @@
4453
4620
  });
4454
4621
  }
4455
4622
 
4456
- if (mobileActionsToggle) {
4457
- mobileActionsToggle.addEventListener('click', function () {
4458
- setMobileActionsMenu(!state.mobileActionsOpen);
4623
+ if (workspaceSwitcherToggle) {
4624
+ workspaceSwitcherToggle.addEventListener('click', function () {
4625
+ setWorkspaceSwitcherMenu(!state.workspaceSwitcherOpen);
4459
4626
  });
4460
4627
  }
4461
4628
 
@@ -4471,18 +4638,6 @@
4471
4638
  });
4472
4639
  }
4473
4640
 
4474
- if (addAgentBtn) {
4475
- addAgentBtn.addEventListener('click', function () {
4476
- const activeSession = getActiveSession();
4477
- const targetContainer = activeSession ? getSessionContainerName(activeSession) : '';
4478
- if (!targetContainer) {
4479
- return;
4480
- }
4481
- closeMobileActionsMenu();
4482
- createAgentSession(targetContainer);
4483
- });
4484
- }
4485
-
4486
4641
  if (configModal) {
4487
4642
  configModal.addEventListener('click', function (event) {
4488
4643
  if (event.target === configModal && !state.configSaving) {
@@ -4550,14 +4705,19 @@
4550
4705
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
4551
4706
  closeMobileSessionPanel();
4552
4707
  }
4553
- if (event.key === 'Escape' && state.mobileActionsOpen) {
4554
- closeMobileActionsMenu();
4708
+ if (event.key === 'Escape' && state.workspaceSwitcherOpen) {
4709
+ closeWorkspaceSwitcherMenu();
4710
+ }
4711
+ if (event.key === 'Escape' && state.composerOptionsOpen) {
4712
+ closeComposerOptionsMenu();
4713
+ }
4714
+ if (event.key === 'Escape' && openTreeNodeMenu) {
4715
+ closeOpenTreeNodeMenu();
4555
4716
  }
4556
4717
  });
4557
4718
 
4558
4719
  function onLayoutMediaChange() {
4559
4720
  setMobileSessionPanel(state.mobileSidebarOpen);
4560
- setMobileActionsMenu(state.mobileActionsOpen);
4561
4721
  if (state.activeTab === 'terminal' && state.terminal.terminalReady) {
4562
4722
  scheduleTerminalFit(false);
4563
4723
  }
@@ -4601,59 +4761,22 @@
4601
4761
 
4602
4762
  document.addEventListener('click', function (event) {
4603
4763
  const target = event.target;
4604
- if (state.mobileActionsOpen) {
4605
- if (mobileActionsToggle && mobileActionsToggle.contains(target)) return;
4606
- if (headerActions && headerActions.contains(target)) return;
4607
- closeMobileActionsMenu();
4764
+ if (state.workspaceSwitcherOpen) {
4765
+ if (workspaceSwitcherToggle && workspaceSwitcherToggle.contains(target)) return;
4766
+ if (workspaceSwitcherPanel && workspaceSwitcherPanel.contains(target)) return;
4767
+ closeWorkspaceSwitcherMenu();
4608
4768
  }
4609
- });
4610
-
4611
- removeBtn.addEventListener('click', async function () {
4612
- if (!state.active) return;
4613
- closeMobileActionsMenu();
4614
- const activeSession = getActiveSession();
4615
- const targetContainer = activeSession && activeSession.containerName ? activeSession.containerName : state.active;
4616
- const yes = confirm('确认删除容器 ' + targetContainer + ' ?');
4617
- if (!yes) return;
4618
- try {
4619
- const current = state.active;
4620
- if (state.terminal.sessionName === current && (state.terminal.connected || state.terminal.connecting)) {
4621
- disconnectTerminal('容器删除,终端已断开', true);
4622
- }
4623
- await api('/api/sessions/' + encodeURIComponent(current) + '/remove', {
4624
- method: 'POST'
4625
- });
4626
- await loadSessions(current);
4627
- } catch (e) {
4628
- alert(e.message);
4769
+ if (state.composerOptionsOpen) {
4770
+ if (composerOptionsToggle && composerOptionsToggle.contains(target)) return;
4771
+ if (composerOptionsPanel && composerOptionsPanel.contains(target)) return;
4772
+ closeComposerOptionsMenu();
4629
4773
  }
4630
- });
4631
-
4632
- removeAllBtn.addEventListener('click', async function () {
4633
- if (!state.active) return;
4634
- closeMobileActionsMenu();
4635
- const activeSession = getActiveSession();
4636
- const targetAgent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
4637
- const yes = confirm('确认删除 AGENT ' + targetAgent + ' ?');
4638
- if (!yes) return;
4639
- try {
4640
- const current = state.active;
4641
- const targetContainerName = activeSession && activeSession.containerName ? activeSession.containerName : '';
4642
- const fallbackSessionName = findPreferredSessionNameAfterRemoval(state.sessions, current);
4643
- await api('/api/sessions/' + encodeURIComponent(state.active) + '/remove-with-history', {
4644
- method: 'POST'
4645
- });
4646
- await refreshSessions({
4647
- preferredName: fallbackSessionName || '',
4648
- preferredContainerName: targetContainerName,
4649
- withLoading: true,
4650
- reloadMessages: true
4651
- });
4652
- } catch (e) {
4653
- alert(e.message);
4774
+ if (openTreeNodeMenu && !openTreeNodeMenu.wrap.contains(target)) {
4775
+ closeOpenTreeNodeMenu();
4654
4776
  }
4655
4777
  });
4656
4778
 
4779
+
4657
4780
  if (externalLinkCancelBtn) {
4658
4781
  externalLinkCancelBtn.addEventListener('click', function () {
4659
4782
  closeExternalLinkModalView();