@xcanwin/manyoyo 6.1.3 → 6.2.1

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,8 +47,8 @@
47
47
  loadingMessages: false,
48
48
  loadingSessionDetail: false,
49
49
  mobileSidebarOpen: false,
50
- mobileActionsOpen: false,
51
50
  workspaceSwitcherOpen: false,
51
+ composerOptionsOpen: false,
52
52
  configModalOpen: false,
53
53
  createModalOpen: false,
54
54
  agentTemplateModalOpen: false,
@@ -123,8 +123,6 @@
123
123
  const sessionList = document.getElementById('sessionList');
124
124
  const sessionCount = document.getElementById('sessionCount');
125
125
  const mobileSessionToggle = document.getElementById('mobileSessionToggle');
126
- const mobileActionsToggle = document.getElementById('mobileActionsToggle');
127
- const headerActions = document.getElementById('headerActions');
128
126
  const viewActivityBtn = document.getElementById('viewActivityBtn');
129
127
  const workspaceSwitcherToggle = document.getElementById('workspaceSwitcherToggle');
130
128
  const workspaceSwitcherPanel = document.getElementById('workspaceSwitcherPanel');
@@ -133,12 +131,10 @@
133
131
  const viewDetailBtn = document.getElementById('viewDetailBtn');
134
132
  const viewConfigBtn = document.getElementById('viewConfigBtn');
135
133
  const viewCheckBtn = document.getElementById('viewCheckBtn');
136
- const addAgentBtn = document.getElementById('addAgentBtn');
137
134
  const mobileSidebarClose = document.getElementById('mobileSidebarClose');
138
135
  const sidebarBackdrop = document.getElementById('sidebarBackdrop');
139
136
  const openConfigBtn = document.getElementById('openConfigBtn');
140
137
  const openCreateBtn = document.getElementById('openCreateBtn');
141
- const openCreateMenuBtn = document.getElementById('openCreateMenuBtn');
142
138
  const configModal = document.getElementById('configModal');
143
139
  const configModalTitle = document.getElementById('configModalTitle');
144
140
  const configPath = document.getElementById('configPath');
@@ -181,12 +177,12 @@
181
177
  const directoryPickerError = document.getElementById('directoryPickerError');
182
178
  const directoryPickerCancelBtn = document.getElementById('directoryPickerCancelBtn');
183
179
  const directoryPickerSelectBtn = document.getElementById('directoryPickerSelectBtn');
184
- const activeTitle = document.getElementById('activeTitle');
185
- const activeMeta = document.getElementById('activeMeta');
186
180
  const activityCommandBtn = document.getElementById('activityCommandBtn');
187
181
  const activityAgentBtn = document.getElementById('activityAgentBtn');
188
182
  const agentTemplateBtn = document.getElementById('agentTemplateBtn');
189
183
  const activityModelChip = document.getElementById('activityModelChip');
184
+ const composerOptionsToggle = document.getElementById('composerOptionsToggle');
185
+ const composerOptionsPanel = document.getElementById('composerOptionsPanel');
190
186
  const messagesNode = document.getElementById('messages');
191
187
  const terminalPanel = document.getElementById('terminalPanel');
192
188
  const filesPanel = document.getElementById('filesPanel');
@@ -199,10 +195,7 @@
199
195
  const terminalScreen = document.getElementById('terminalScreen');
200
196
  const composer = document.getElementById('composer');
201
197
  const commandInput = document.getElementById('commandInput');
202
- const composerHint = document.getElementById('composerHint');
203
- const sendState = document.getElementById('sendState');
204
198
  const sendBtn = document.getElementById('sendBtn');
205
- const stopBtn = document.getElementById('stopBtn');
206
199
  const agentTemplateModal = document.getElementById('agentTemplateModal');
207
200
  const agentTemplatePrimary = document.getElementById('agentTemplatePrimary');
208
201
  const containerCliSelect = document.getElementById('containerCliSelect');
@@ -218,9 +211,6 @@
218
211
  const externalLinkUrl = document.getElementById('externalLinkUrl');
219
212
  const externalLinkCancelBtn = document.getElementById('externalLinkCancelBtn');
220
213
  const externalLinkOpenBtn = document.getElementById('externalLinkOpenBtn');
221
- const refreshBtn = document.getElementById('refreshBtn');
222
- const removeBtn = document.getElementById('removeBtn');
223
- const removeAllBtn = document.getElementById('removeAllBtn');
224
214
  const MOBILE_LAYOUT_MEDIA = window.matchMedia('(max-width: 980px)');
225
215
  const MOBILE_COMPACT_MEDIA = window.matchMedia('(max-width: 640px)');
226
216
  const TERMINAL_FIT_DEBOUNCE_MS = 60;
@@ -328,7 +318,7 @@
328
318
  return Promise.reject(new Error('clipboard unavailable'));
329
319
  }
330
320
 
331
- function appendMessageCopyActions(meta, items) {
321
+ function appendMessageCopyActions(container, items) {
332
322
  const actions = document.createElement('div');
333
323
  actions.className = 'msg-actions';
334
324
  items.forEach(function (item) {
@@ -352,7 +342,7 @@
352
342
  });
353
343
  actions.appendChild(btn);
354
344
  });
355
- meta.appendChild(actions);
345
+ container.appendChild(actions);
356
346
  }
357
347
 
358
348
  function stringifyPrettyJson(value) {
@@ -605,7 +595,7 @@
605
595
  const hasError = traceEvents.some(function (event) {
606
596
  return event && event.kind === 'error';
607
597
  });
608
- const defaultOpen = hasError || traceEvents.length === 0;
598
+ const defaultOpen = hasError;
609
599
  const toggle = document.createElement('details');
610
600
  toggle.className = 'trace-flow-toggle';
611
601
  toggle.open = traceFlowExpandedState.has(messageId) ? traceFlowExpandedState.get(messageId) : defaultOpen;
@@ -632,7 +622,7 @@
632
622
  return 'AGENT 实时回复';
633
623
  }
634
624
  if (message && message.streamTrace) {
635
- return 'AGENT 过程';
625
+ return 'AGENT 回复';
636
626
  }
637
627
  if (message && message.mode === 'agent') {
638
628
  return 'AGENT 回复';
@@ -961,17 +951,6 @@
961
951
  return base.replace(/\/+$/, '') + '/' + child.replace(/^\/+/, '');
962
952
  }
963
953
 
964
- function envMapToText(envMap) {
965
- if (!envMap || typeof envMap !== 'object') {
966
- return '';
967
- }
968
- return Object.entries(envMap)
969
- .map(function (entry) {
970
- return entry[0] + '=' + String(entry[1] == null ? '' : entry[1]);
971
- })
972
- .join('\n');
973
- }
974
-
975
954
  function textToLineArray(text) {
976
955
  return String(text || '')
977
956
  .split('\n')
@@ -1635,17 +1614,6 @@
1635
1614
  return '自动';
1636
1615
  }
1637
1616
 
1638
- function buildActiveMeta(session) {
1639
- if (!session) {
1640
- return '会话不可用';
1641
- }
1642
- const status = sessionStatusInfo(session.status);
1643
- const messageCount = safeMessageCount(session.messageCount);
1644
- const updatedAt = formatDateTime(session.updatedAt) || '暂无更新';
1645
- const containerName = session.containerName || '未绑定容器';
1646
- return `${containerName} · ${status.label} · ${messageCount} 条对话 · ${updatedAt}`;
1647
- }
1648
-
1649
1617
  function buildMessageMetaLines(message) {
1650
1618
  const lines = [];
1651
1619
  const timeText = formatDateTime(message && message.timestamp);
@@ -1980,18 +1948,6 @@
1980
1948
  setMobileSessionPanel(false);
1981
1949
  }
1982
1950
 
1983
- function setMobileActionsMenu(open) {
1984
- state.mobileActionsOpen = Boolean(open);
1985
- document.body.classList.toggle('mobile-actions-open', state.mobileActionsOpen);
1986
- if (mobileActionsToggle) {
1987
- mobileActionsToggle.setAttribute('aria-expanded', state.mobileActionsOpen ? 'true' : 'false');
1988
- }
1989
- }
1990
-
1991
- function closeMobileActionsMenu() {
1992
- setMobileActionsMenu(false);
1993
- }
1994
-
1995
1951
  function setWorkspaceSwitcherMenu(open) {
1996
1952
  state.workspaceSwitcherOpen = Boolean(open);
1997
1953
  document.body.classList.toggle('workspace-switcher-open', state.workspaceSwitcherOpen);
@@ -2007,6 +1963,21 @@
2007
1963
  setWorkspaceSwitcherMenu(false);
2008
1964
  }
2009
1965
 
1966
+ function setComposerOptionsMenu(open) {
1967
+ state.composerOptionsOpen = Boolean(open);
1968
+ document.body.classList.toggle('composer-options-open', state.composerOptionsOpen);
1969
+ if (composerOptionsToggle) {
1970
+ composerOptionsToggle.setAttribute('aria-expanded', state.composerOptionsOpen ? 'true' : 'false');
1971
+ }
1972
+ if (composerOptionsPanel) {
1973
+ composerOptionsPanel.hidden = !state.composerOptionsOpen;
1974
+ }
1975
+ }
1976
+
1977
+ function closeComposerOptionsMenu() {
1978
+ setComposerOptionsMenu(false);
1979
+ }
1980
+
2010
1981
  const VIEW_LABELS = {
2011
1982
  activity: '活动',
2012
1983
  terminal: '终端',
@@ -2274,31 +2245,17 @@
2274
2245
  }
2275
2246
  }
2276
2247
 
2277
- function syncComposerExpanded() {
2278
- if (!composer || !commandInput) {
2279
- return;
2280
- }
2281
- // 判定条件必须是"焦点是否还在 composer 内部"而不是"是否等于 commandInput":
2282
- // 点击 Agent/命令 切换或 CLI/模型 按钮时,commandInput 会先 blur,
2283
- // 若立刻按 commandInput 是否聚焦来收起,会在 mousedown 与 click 之间把工具栏收起,
2284
- // 导致按钮的点击事件因布局变化而落空(点了没反应)。
2285
- const focused = composer.contains(document.activeElement);
2286
- const hasDraft = Boolean(commandInput.value && commandInput.value.trim());
2287
- const expanded = window.ManyoyoChatBehavior.shouldExpandComposer({ focused, hasDraft });
2288
- composer.classList.toggle('is-expanded', expanded);
2289
- }
2290
-
2291
2248
  function syncUi() {
2292
2249
  if (!state.active) {
2293
- activeTitle.textContent = '未选择会话';
2294
- activeMeta.textContent = '请选择左侧会话';
2250
+ document.title = window.ManyoyoChatBehavior.buildDocumentTitle('');
2295
2251
  if (isComposerMode()) {
2296
2252
  commandInput.value = '';
2297
2253
  }
2298
2254
  } else {
2299
2255
  const activeSession = getActiveSession();
2300
- activeTitle.textContent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
2301
- activeMeta.textContent = buildActiveMeta(activeSession);
2256
+ document.title = window.ManyoyoChatBehavior.buildDocumentTitle(
2257
+ activeSession && activeSession.agentName ? activeSession.agentName : state.active
2258
+ );
2302
2259
  }
2303
2260
 
2304
2261
  const activityTab = state.activeTab === 'activity';
@@ -2377,16 +2334,15 @@
2377
2334
 
2378
2335
  const activeAgentRunning = isAgentRunActiveForSession(state.active) || hasPendingAgentMessagesForSession(state.active);
2379
2336
  const busy = state.loadingSessions || state.loadingMessages || state.sending || state.creatingAgent;
2380
- refreshBtn.disabled = busy;
2381
- if (addAgentBtn) {
2382
- addAgentBtn.disabled = !state.active || busy;
2383
- addAgentBtn.textContent = state.creatingAgent ? '新建中...' : '新建 AGENT';
2384
- }
2385
- removeBtn.disabled = !state.active || busy;
2386
- removeAllBtn.disabled = !state.active || busy;
2387
- sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && (!agentEnabled || activeAgentRunning));
2388
- if (stopBtn) {
2389
- stopBtn.disabled = !activityTab || !agentMode || !activeAgentRunning || state.agentRun.stopping;
2337
+ const showStop = agentMode && activeAgentRunning;
2338
+ if (showStop) {
2339
+ sendBtn.disabled = !activityTab || !state.active || state.agentRun.stopping;
2340
+ sendBtn.textContent = state.agentRun.stopping ? '停止中…' : '停止';
2341
+ sendBtn.classList.add('danger-outline');
2342
+ } else {
2343
+ sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && !agentEnabled);
2344
+ sendBtn.textContent = '发送';
2345
+ sendBtn.classList.remove('danger-outline');
2390
2346
  }
2391
2347
  if (agentTemplateBtn) {
2392
2348
  agentTemplateBtn.disabled = !state.active || state.agentTemplateSaving;
@@ -2397,18 +2353,9 @@
2397
2353
  ? '输入提示词,例如:请帮我分析当前项目结构并给出重构建议'
2398
2354
  : '输入容器命令,例如: ls -la';
2399
2355
  }
2400
- if (composerHint) {
2401
- composerHint.textContent = agentMode
2402
- ? 'Enter 发送提示词 · Shift/Alt + Enter 换行 · 执行中可停止'
2403
- : 'Enter 发送 · Shift/Alt + Enter 换行';
2404
- }
2405
- syncComposerExpanded();
2406
2356
  if (openCreateBtn) {
2407
2357
  openCreateBtn.disabled = state.createLoading || state.createSubmitting;
2408
2358
  }
2409
- if (openCreateMenuBtn) {
2410
- openCreateMenuBtn.disabled = state.createLoading || state.createSubmitting;
2411
- }
2412
2359
  if (openConfigBtn) {
2413
2360
  openConfigBtn.disabled = state.configLoading || state.configSaving;
2414
2361
  }
@@ -2435,24 +2382,6 @@
2435
2382
  if (createCancelBtn) {
2436
2383
  createCancelBtn.disabled = state.createSubmitting;
2437
2384
  }
2438
- if (sendState) {
2439
- if (!state.active) {
2440
- sendState.textContent = '未选择会话';
2441
- sendState.classList.remove('is-active');
2442
- } else if (state.creatingAgent) {
2443
- sendState.textContent = '正在新建 AGENT…';
2444
- sendState.classList.add('is-active');
2445
- } else if (activeAgentRunning && agentMode) {
2446
- sendState.textContent = state.agentRun.stopping ? '正在停止 Agent…' : 'Agent 执行中';
2447
- sendState.classList.add('is-active');
2448
- } else if (busy) {
2449
- sendState.textContent = '处理中';
2450
- sendState.classList.add('is-active');
2451
- } else {
2452
- sendState.textContent = agentMode ? 'Agent 就绪' : '命令就绪';
2453
- sendState.classList.remove('is-active');
2454
- }
2455
- }
2456
2385
  if (configModal) {
2457
2386
  configModal.hidden = !state.configModalOpen;
2458
2387
  }
@@ -2493,25 +2422,10 @@
2493
2422
  'modal-open',
2494
2423
  state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen
2495
2424
  );
2496
- if (!state.active) {
2497
- sendState.textContent = '未选择会话';
2498
- } else if (state.creatingAgent) {
2499
- sendState.textContent = '正在新建 AGENT…';
2500
- } else if (agentMode && !agentEnabled) {
2501
- sendState.textContent = '当前会话未配置 AGENT 模板';
2502
- } else if (state.sending) {
2503
- sendState.textContent = '发送中...';
2504
- } else if (state.loadingSessions || state.loadingMessages) {
2505
- sendState.textContent = '加载中...';
2506
- } else {
2507
- sendState.textContent = '就绪';
2508
- }
2509
- sendState.classList.toggle('is-active', state.sending || state.creatingAgent);
2510
2425
  if (composer) {
2511
2426
  composer.hidden = !activityTab;
2512
2427
  }
2513
2428
  setMobileSessionPanel(state.mobileSidebarOpen);
2514
- setMobileActionsMenu(state.mobileActionsOpen);
2515
2429
  renderSessionDetailPanels();
2516
2430
  }
2517
2431
 
@@ -3028,10 +2942,6 @@
3028
2942
  });
3029
2943
  }
3030
2944
 
3031
- function createTreeMetaText(parts) {
3032
- return (parts || []).filter(Boolean).join(' · ');
3033
- }
3034
-
3035
2945
  function getSessionCreatedTime(session) {
3036
2946
  if (session && session.createdAt) {
3037
2947
  const time = new Date(session.createdAt).getTime();
@@ -3134,6 +3044,122 @@
3134
3044
  return findLatestCreatedSessionName(remaining, removedRef.containerName);
3135
3045
  }
3136
3046
 
3047
+ let openTreeNodeMenu = null;
3048
+
3049
+ function closeOpenTreeNodeMenu() {
3050
+ if (openTreeNodeMenu) {
3051
+ const menu = openTreeNodeMenu;
3052
+ openTreeNodeMenu = null;
3053
+ menu.close();
3054
+ }
3055
+ }
3056
+
3057
+ function createTreeNodeMenu(items) {
3058
+ const wrap = document.createElement('div');
3059
+ wrap.className = 'tree-node-menu';
3060
+
3061
+ const trigger = document.createElement('button');
3062
+ trigger.type = 'button';
3063
+ trigger.className = 'secondary tree-node-menu-trigger';
3064
+ trigger.setAttribute('aria-haspopup', 'true');
3065
+ trigger.setAttribute('aria-expanded', 'false');
3066
+ trigger.setAttribute('aria-label', '更多操作');
3067
+ 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>';
3068
+
3069
+ const panel = document.createElement('div');
3070
+ panel.className = 'tree-node-menu-panel';
3071
+ panel.hidden = true;
3072
+
3073
+ function close() {
3074
+ panel.hidden = true;
3075
+ trigger.setAttribute('aria-expanded', 'false');
3076
+ wrap.classList.remove('is-open');
3077
+ }
3078
+
3079
+ trigger.addEventListener('click', function (event) {
3080
+ event.stopPropagation();
3081
+ if (panel.hidden) {
3082
+ closeOpenTreeNodeMenu();
3083
+ panel.hidden = false;
3084
+ trigger.setAttribute('aria-expanded', 'true');
3085
+ wrap.classList.add('is-open');
3086
+ openTreeNodeMenu = { wrap, close };
3087
+ } else {
3088
+ close();
3089
+ openTreeNodeMenu = null;
3090
+ }
3091
+ });
3092
+
3093
+ (Array.isArray(items) ? items : []).forEach(function (item) {
3094
+ const itemBtn = document.createElement('button');
3095
+ itemBtn.type = 'button';
3096
+ itemBtn.className = 'tree-node-menu-panel-item' + (item.danger ? ' is-danger' : '');
3097
+ itemBtn.textContent = item.label;
3098
+ itemBtn.addEventListener('click', function (event) {
3099
+ event.stopPropagation();
3100
+ close();
3101
+ openTreeNodeMenu = null;
3102
+ if (typeof item.onClick === 'function') {
3103
+ item.onClick();
3104
+ }
3105
+ });
3106
+ panel.appendChild(itemBtn);
3107
+ });
3108
+
3109
+ wrap.appendChild(trigger);
3110
+ wrap.appendChild(panel);
3111
+ return wrap;
3112
+ }
3113
+
3114
+ async function removeContainerByName(containerName) {
3115
+ const target = String(containerName || '').trim();
3116
+ if (!target) return;
3117
+ const yes = confirm('确认删除容器 ' + target + ' ?');
3118
+ if (!yes) return;
3119
+ try {
3120
+ const wasActiveContainer = parseSessionKey(state.active).containerName === target;
3121
+ if (parseSessionKey(state.terminal.sessionName).containerName === target
3122
+ && (state.terminal.connected || state.terminal.connecting)) {
3123
+ disconnectTerminal('容器删除,终端已断开', true);
3124
+ }
3125
+ await api('/api/sessions/' + encodeURIComponent(target) + '/remove', {
3126
+ method: 'POST'
3127
+ });
3128
+ await refreshSessions({
3129
+ preferredName: wasActiveContainer ? target : (state.active || ''),
3130
+ withLoading: true,
3131
+ reloadMessages: wasActiveContainer
3132
+ });
3133
+ } catch (e) {
3134
+ alert(e.message);
3135
+ }
3136
+ }
3137
+
3138
+ async function removeAgentSessionByName(sessionName, agentLabel) {
3139
+ const target = String(sessionName || '').trim();
3140
+ if (!target) return;
3141
+ const yes = confirm('确认删除 AGENT ' + (agentLabel || target) + ' ?');
3142
+ if (!yes) return;
3143
+ const isActive = target === state.active;
3144
+ try {
3145
+ const containerName = parseSessionKey(target).containerName;
3146
+ const fallbackSessionName = isActive
3147
+ ? findPreferredSessionNameAfterRemoval(state.sessions, target)
3148
+ : '';
3149
+ await api('/api/sessions/' + encodeURIComponent(target) + '/remove-with-history', {
3150
+ method: 'POST'
3151
+ });
3152
+ await refreshSessions({
3153
+ preferredName: isActive ? (fallbackSessionName || '') : (state.active || ''),
3154
+ preferredContainerName: containerName,
3155
+ withLoading: true,
3156
+ reloadMessages: isActive
3157
+ });
3158
+ } catch (e) {
3159
+ alert(e.message);
3160
+ }
3161
+ }
3162
+
3137
3163
  function createDisclosureButton(expanded, label) {
3138
3164
  const button = document.createElement('button');
3139
3165
  button.type = 'button';
@@ -3272,18 +3298,21 @@
3272
3298
  const status = sessionStatusInfo(containerGroup && containerGroup.status);
3273
3299
  const historyClassName = status.tone === 'history' ? 'tree-node-tone-history' : '';
3274
3300
  const containerName = String(containerGroup && containerGroup.containerName ? containerGroup.containerName : '').trim() || '未命名容器';
3275
- const hoverMenu = document.createElement('div');
3276
- hoverMenu.className = 'tree-node-hover-menu';
3277
-
3278
- const addAgentBtn = document.createElement('button');
3279
- addAgentBtn.type = 'button';
3280
- addAgentBtn.className = 'secondary tree-node-menu-item';
3281
- addAgentBtn.textContent = '新建 AGENT';
3282
- addAgentBtn.addEventListener('click', function (event) {
3283
- event.stopPropagation();
3284
- createAgentSession(containerName);
3285
- });
3286
- hoverMenu.appendChild(addAgentBtn);
3301
+ const containerMenu = createTreeNodeMenu([
3302
+ {
3303
+ label: '新建 AGENT',
3304
+ onClick: function () {
3305
+ createAgentSession(containerName);
3306
+ }
3307
+ },
3308
+ {
3309
+ label: '删除容器',
3310
+ danger: true,
3311
+ onClick: function () {
3312
+ removeContainerByName(containerName);
3313
+ }
3314
+ }
3315
+ ]);
3287
3316
 
3288
3317
  return {
3289
3318
  kind: 'container',
@@ -3294,18 +3323,28 @@
3294
3323
  disclosureLabel: containerName,
3295
3324
  expanded: isContainerExpanded(containerGroup),
3296
3325
  hasActive: containerContainsActiveSession(containerGroup),
3297
- overlayNode: hoverMenu,
3326
+ overlayNode: containerMenu,
3298
3327
  onToggle: function (expanded) {
3299
3328
  setSidebarContainerExpanded(containerGroup.containerName, expanded);
3300
3329
  },
3301
3330
  children: (Array.isArray(containerGroup.sessions) ? containerGroup.sessions : []).map(function (session) {
3331
+ const agentTitle = session.agentName || session.name;
3302
3332
  return {
3303
3333
  kind: 'agent',
3304
- title: session.agentName || session.name,
3334
+ title: agentTitle,
3305
3335
  meta: formatDateTime(session.updatedAt) || '暂无更新',
3306
3336
  className: historyClassName,
3307
3337
  active: state.active === session.name,
3308
- sessionName: session.name
3338
+ sessionName: session.name,
3339
+ overlayNode: createTreeNodeMenu([
3340
+ {
3341
+ label: '删除 AGENT',
3342
+ danger: true,
3343
+ onClick: function () {
3344
+ removeAgentSessionByName(session.name, agentTitle);
3345
+ }
3346
+ }
3347
+ ])
3309
3348
  };
3310
3349
  })
3311
3350
  };
@@ -3314,7 +3353,7 @@
3314
3353
  });
3315
3354
  }
3316
3355
 
3317
- function renderSessionTreeNodes(nodes, parentNode, ancestorHasNext, itemCounter) {
3356
+ function renderSessionTreeNodes(nodes, parentNode, ancestorHasNext) {
3318
3357
  (Array.isArray(nodes) ? nodes : []).forEach(function (node, index) {
3319
3358
  const isLastSibling = index === nodes.length - 1;
3320
3359
  const item = createTreeItem({
@@ -3336,8 +3375,6 @@
3336
3375
 
3337
3376
  if (node.kind === 'agent') {
3338
3377
  item.button.dataset.sessionName = node.sessionName || '';
3339
- item.button.style.setProperty('--item-index', String(itemCounter.value));
3340
- itemCounter.value += 1;
3341
3378
  state.sessionNodeMap.set(node.sessionName, item.button);
3342
3379
  item.button.addEventListener('click', function () {
3343
3380
  handleSessionItemClick(node.sessionName || '');
@@ -3355,7 +3392,7 @@
3355
3392
  childrenNode.className = `tree-node-children tree-node-children-${node.kind}`;
3356
3393
  childrenNode.setAttribute('role', 'group');
3357
3394
  childrenNode.hidden = !node.expanded;
3358
- renderSessionTreeNodes(node.children || [], childrenNode, ancestorHasNext.concat(!isLastSibling), itemCounter);
3395
+ renderSessionTreeNodes(node.children || [], childrenNode, ancestorHasNext.concat(!isLastSibling));
3359
3396
  block.appendChild(childrenNode);
3360
3397
  parentNode.appendChild(block);
3361
3398
 
@@ -3451,8 +3488,7 @@
3451
3488
 
3452
3489
  const grouped = groupSessionsByDirectory(state.sessions);
3453
3490
  const treeNodes = buildSessionTreeNodes(grouped);
3454
- const itemCounter = { value: 0 };
3455
- renderSessionTreeNodes(treeNodes, sessionList, [], itemCounter);
3491
+ renderSessionTreeNodes(treeNodes, sessionList, []);
3456
3492
  updateSidebarActiveSelection();
3457
3493
 
3458
3494
  scrollActiveSessionIntoView();
@@ -3493,10 +3529,12 @@
3493
3529
  }
3494
3530
 
3495
3531
  function getMessageRenderKey(msg, index) {
3532
+ const pairedTrace = msg && msg.pairedTrace ? msg.pairedTrace : null;
3533
+ const tracePart = pairedTrace ? `|paired:${String(pairedTrace.content || '')}` : '';
3496
3534
  if (msg && msg.id && msg.streamingReply) {
3497
3535
  const content = msg.content ? String(msg.content) : '';
3498
3536
  const timestamp = msg.timestamp ? String(msg.timestamp) : '';
3499
- return `id:${msg.id}|streaming|${timestamp}|${content}`;
3537
+ return `id:${msg.id}|streaming|${timestamp}|${content}${tracePart}`;
3500
3538
  }
3501
3539
  if (msg && msg.id && msg.streamTrace) {
3502
3540
  const content = msg.content ? String(msg.content) : '';
@@ -3505,7 +3543,7 @@
3505
3543
  return `id:${msg.id}|trace|${timestamp}|${content}|${running}`;
3506
3544
  }
3507
3545
  if (msg && msg.id) {
3508
- return `id:${msg.id}`;
3546
+ return `id:${msg.id}${tracePart}`;
3509
3547
  }
3510
3548
  const role = msg && msg.role ? String(msg.role) : '';
3511
3549
  const mode = msg && msg.mode ? String(msg.mode) : '';
@@ -3513,7 +3551,7 @@
3513
3551
  const exitCode = msg && typeof msg.exitCode === 'number' ? String(msg.exitCode) : '';
3514
3552
  const pending = msg && msg.pending ? '1' : '0';
3515
3553
  const content = msg && msg.content ? String(msg.content) : '';
3516
- return `idx:${index}|${role}|${mode}|${timestamp}|${exitCode}|${pending}|${content}`;
3554
+ return `idx:${index}|${role}|${mode}|${timestamp}|${exitCode}|${pending}|${content}${tracePart}`;
3517
3555
  }
3518
3556
 
3519
3557
  function resolveMessageOrigin(msg) {
@@ -3550,13 +3588,12 @@
3550
3588
  const bubble = document.createElement('div');
3551
3589
  bubble.className = 'bubble';
3552
3590
 
3591
+ if (msg && msg.pairedTrace) {
3592
+ appendStructuredTraceContent(bubble, msg.pairedTrace);
3593
+ }
3594
+
3553
3595
  const isStreamingReply = Boolean(msg && msg.streamingReply);
3554
- const shouldRenderStructuredTrace = Boolean(
3555
- msg
3556
- && msg.streamTrace
3557
- && Array.isArray(msg.traceEvents)
3558
- && msg.traceEvents.length
3559
- );
3596
+ const shouldRenderStructuredTrace = Boolean(msg && msg.streamTrace);
3560
3597
  const shouldRenderMarkdown = Boolean(!msg.streamTrace && !msg.streamingReply && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
3561
3598
  if (isStreamingReply) {
3562
3599
  bubble.classList.add('streaming-reply');
@@ -3596,7 +3633,7 @@
3596
3633
  if (renderedMarkdown) {
3597
3634
  markdownNode.innerHTML = renderedMarkdown;
3598
3635
  bubble.appendChild(markdownNode);
3599
- appendMessageCopyActions(meta, [
3636
+ appendMessageCopyActions(bubble, [
3600
3637
  { label: '复制文本', getText: function () { return markdownNode.innerText || markdownNode.textContent || ''; } },
3601
3638
  { label: '复制 Markdown', getText: function () { return msg.content; } }
3602
3639
  ]);
@@ -3606,7 +3643,7 @@
3606
3643
  } else {
3607
3644
  appendPlainMessageContent(bubble, msg.content);
3608
3645
  if (msg.role === 'user') {
3609
- appendMessageCopyActions(meta, [
3646
+ appendMessageCopyActions(bubble, [
3610
3647
  { label: '复制', getText: function () { return msg.content; } }
3611
3648
  ]);
3612
3649
  }
@@ -3617,7 +3654,8 @@
3617
3654
  return row;
3618
3655
  }
3619
3656
 
3620
- function renderMessages(messages, options) {
3657
+ function renderMessages(rawMessages, options) {
3658
+ const messages = window.ManyoyoChatBehavior.mergeTraceIntoReply(rawMessages);
3621
3659
  const renderOptions = options && typeof options === 'object' ? options : {};
3622
3660
  const stickToBottom = renderOptions.stickToBottom === true || isMessagesNearBottom(40);
3623
3661
 
@@ -4149,15 +4187,9 @@
4149
4187
  });
4150
4188
  }
4151
4189
 
4152
- if (openCreateMenuBtn) {
4153
- openCreateMenuBtn.addEventListener('click', function () {
4154
- closeMobileActionsMenu();
4155
- openCreateModal();
4156
- });
4157
- }
4158
-
4159
4190
  if (agentTemplateBtn) {
4160
4191
  agentTemplateBtn.addEventListener('click', function () {
4192
+ closeComposerOptionsMenu();
4161
4193
  openAgentTemplateModal().catch(function (e) {
4162
4194
  alert(e && e.message ? e.message : '加载 Agent 模板失败');
4163
4195
  });
@@ -4349,10 +4381,29 @@
4349
4381
  composer.addEventListener('submit', async function (event) {
4350
4382
  event.preventDefault();
4351
4383
  if (!state.active) return;
4352
- if (state.sending) return;
4353
- if (state.loadingSessions || state.loadingMessages) return;
4354
4384
  if (state.activeTab !== 'activity') return;
4385
+
4355
4386
  const mode = state.mode === 'agent' ? 'agent' : 'command';
4387
+ const activeAgentRunning = isAgentRunActiveForSession(state.active) || hasPendingAgentMessagesForSession(state.active);
4388
+ if (mode === 'agent' && activeAgentRunning) {
4389
+ if (state.agentRun.stopping) return;
4390
+ state.agentRun.stopping = true;
4391
+ syncUi();
4392
+ try {
4393
+ await api('/api/sessions/' + encodeURIComponent(state.active) + '/agent/stop', {
4394
+ method: 'POST',
4395
+ body: JSON.stringify({})
4396
+ });
4397
+ } catch (e) {
4398
+ alert(e.message);
4399
+ state.agentRun.stopping = false;
4400
+ syncUi();
4401
+ }
4402
+ return;
4403
+ }
4404
+
4405
+ if (state.sending) return;
4406
+ if (state.loadingSessions || state.loadingMessages) return;
4356
4407
  if (mode === 'agent' && !isActiveAgentEnabled()) {
4357
4408
  syncUi();
4358
4409
  return;
@@ -4420,40 +4471,9 @@
4420
4471
  } finally {
4421
4472
  state.sending = false;
4422
4473
  syncUi();
4423
- commandInput.focus();
4424
4474
  }
4425
4475
  });
4426
4476
 
4427
- if (stopBtn) {
4428
- stopBtn.addEventListener('click', async function () {
4429
- if (!state.active || !isAgentRunActiveForSession(state.active) || state.agentRun.stopping) {
4430
- return;
4431
- }
4432
- state.agentRun.stopping = true;
4433
- syncUi();
4434
- try {
4435
- await api('/api/sessions/' + encodeURIComponent(state.active) + '/agent/stop', {
4436
- method: 'POST',
4437
- body: JSON.stringify({})
4438
- });
4439
- } catch (e) {
4440
- alert(e.message);
4441
- state.agentRun.stopping = false;
4442
- syncUi();
4443
- }
4444
- });
4445
- }
4446
-
4447
- if (composer) {
4448
- composer.addEventListener('focusin', syncComposerExpanded);
4449
- composer.addEventListener('focusout', function () {
4450
- // 延后到下一个宏任务再判断,让浏览器先把焦点落到新的目标元素上
4451
- // (比如 composer 内的 CLI/Agent 按钮),避免过早收起导致点击落空。
4452
- window.setTimeout(syncComposerExpanded, 0);
4453
- });
4454
- }
4455
- commandInput.addEventListener('input', syncComposerExpanded);
4456
-
4457
4477
  commandInput.addEventListener('keydown', function (event) {
4458
4478
  if (event.key !== 'Enter' || event.isComposing) {
4459
4479
  return;
@@ -4476,6 +4496,7 @@
4476
4496
  activityCommandBtn.addEventListener('click', function () {
4477
4497
  state.mode = 'command';
4478
4498
  renderMessages(state.messages, { forceFullRender: true });
4499
+ closeComposerOptionsMenu();
4479
4500
  syncUi();
4480
4501
  commandInput.focus();
4481
4502
  });
@@ -4485,11 +4506,18 @@
4485
4506
  activityAgentBtn.addEventListener('click', function () {
4486
4507
  state.mode = 'agent';
4487
4508
  renderMessages(state.messages, { forceFullRender: true });
4509
+ closeComposerOptionsMenu();
4488
4510
  syncUi();
4489
4511
  commandInput.focus();
4490
4512
  });
4491
4513
  }
4492
4514
 
4515
+ if (composerOptionsToggle) {
4516
+ composerOptionsToggle.addEventListener('click', function () {
4517
+ setComposerOptionsMenu(!state.composerOptionsOpen);
4518
+ });
4519
+ }
4520
+
4493
4521
  if (viewActivityBtn) {
4494
4522
  viewActivityBtn.addEventListener('click', function () {
4495
4523
  closeWorkspaceSwitcherMenu();
@@ -4533,11 +4561,6 @@
4533
4561
  });
4534
4562
  }
4535
4563
 
4536
- refreshBtn.addEventListener('click', function () {
4537
- closeMobileActionsMenu();
4538
- loadSessions(state.active).catch(function (e) { alert(e.message); });
4539
- });
4540
-
4541
4564
  const TERM_KEY_SEQUENCES = {
4542
4565
  esc: '\x1b',
4543
4566
  tab: '\x09',
@@ -4579,12 +4602,6 @@
4579
4602
  });
4580
4603
  }
4581
4604
 
4582
- if (mobileActionsToggle) {
4583
- mobileActionsToggle.addEventListener('click', function () {
4584
- setMobileActionsMenu(!state.mobileActionsOpen);
4585
- });
4586
- }
4587
-
4588
4605
  if (workspaceSwitcherToggle) {
4589
4606
  workspaceSwitcherToggle.addEventListener('click', function () {
4590
4607
  setWorkspaceSwitcherMenu(!state.workspaceSwitcherOpen);
@@ -4603,18 +4620,6 @@
4603
4620
  });
4604
4621
  }
4605
4622
 
4606
- if (addAgentBtn) {
4607
- addAgentBtn.addEventListener('click', function () {
4608
- const activeSession = getActiveSession();
4609
- const targetContainer = activeSession ? getSessionContainerName(activeSession) : '';
4610
- if (!targetContainer) {
4611
- return;
4612
- }
4613
- closeMobileActionsMenu();
4614
- createAgentSession(targetContainer);
4615
- });
4616
- }
4617
-
4618
4623
  if (configModal) {
4619
4624
  configModal.addEventListener('click', function (event) {
4620
4625
  if (event.target === configModal && !state.configSaving) {
@@ -4682,17 +4687,19 @@
4682
4687
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
4683
4688
  closeMobileSessionPanel();
4684
4689
  }
4685
- if (event.key === 'Escape' && state.mobileActionsOpen) {
4686
- closeMobileActionsMenu();
4687
- }
4688
4690
  if (event.key === 'Escape' && state.workspaceSwitcherOpen) {
4689
4691
  closeWorkspaceSwitcherMenu();
4690
4692
  }
4693
+ if (event.key === 'Escape' && state.composerOptionsOpen) {
4694
+ closeComposerOptionsMenu();
4695
+ }
4696
+ if (event.key === 'Escape' && openTreeNodeMenu) {
4697
+ closeOpenTreeNodeMenu();
4698
+ }
4691
4699
  });
4692
4700
 
4693
4701
  function onLayoutMediaChange() {
4694
4702
  setMobileSessionPanel(state.mobileSidebarOpen);
4695
- setMobileActionsMenu(state.mobileActionsOpen);
4696
4703
  if (state.activeTab === 'terminal' && state.terminal.terminalReady) {
4697
4704
  scheduleTerminalFit(false);
4698
4705
  }
@@ -4736,64 +4743,22 @@
4736
4743
 
4737
4744
  document.addEventListener('click', function (event) {
4738
4745
  const target = event.target;
4739
- if (state.mobileActionsOpen) {
4740
- if (mobileActionsToggle && mobileActionsToggle.contains(target)) return;
4741
- if (headerActions && headerActions.contains(target)) return;
4742
- closeMobileActionsMenu();
4743
- }
4744
4746
  if (state.workspaceSwitcherOpen) {
4745
4747
  if (workspaceSwitcherToggle && workspaceSwitcherToggle.contains(target)) return;
4746
4748
  if (workspaceSwitcherPanel && workspaceSwitcherPanel.contains(target)) return;
4747
4749
  closeWorkspaceSwitcherMenu();
4748
4750
  }
4749
- });
4750
-
4751
- removeBtn.addEventListener('click', async function () {
4752
- if (!state.active) return;
4753
- closeMobileActionsMenu();
4754
- const activeSession = getActiveSession();
4755
- const targetContainer = activeSession && activeSession.containerName ? activeSession.containerName : state.active;
4756
- const yes = confirm('确认删除容器 ' + targetContainer + ' ?');
4757
- if (!yes) return;
4758
- try {
4759
- const current = state.active;
4760
- if (state.terminal.sessionName === current && (state.terminal.connected || state.terminal.connecting)) {
4761
- disconnectTerminal('容器删除,终端已断开', true);
4762
- }
4763
- await api('/api/sessions/' + encodeURIComponent(current) + '/remove', {
4764
- method: 'POST'
4765
- });
4766
- await loadSessions(current);
4767
- } catch (e) {
4768
- alert(e.message);
4751
+ if (state.composerOptionsOpen) {
4752
+ if (composerOptionsToggle && composerOptionsToggle.contains(target)) return;
4753
+ if (composerOptionsPanel && composerOptionsPanel.contains(target)) return;
4754
+ closeComposerOptionsMenu();
4769
4755
  }
4770
- });
4771
-
4772
- removeAllBtn.addEventListener('click', async function () {
4773
- if (!state.active) return;
4774
- closeMobileActionsMenu();
4775
- const activeSession = getActiveSession();
4776
- const targetAgent = activeSession && activeSession.agentName ? activeSession.agentName : state.active;
4777
- const yes = confirm('确认删除 AGENT ' + targetAgent + ' ?');
4778
- if (!yes) return;
4779
- try {
4780
- const current = state.active;
4781
- const targetContainerName = activeSession && activeSession.containerName ? activeSession.containerName : '';
4782
- const fallbackSessionName = findPreferredSessionNameAfterRemoval(state.sessions, current);
4783
- await api('/api/sessions/' + encodeURIComponent(state.active) + '/remove-with-history', {
4784
- method: 'POST'
4785
- });
4786
- await refreshSessions({
4787
- preferredName: fallbackSessionName || '',
4788
- preferredContainerName: targetContainerName,
4789
- withLoading: true,
4790
- reloadMessages: true
4791
- });
4792
- } catch (e) {
4793
- alert(e.message);
4756
+ if (openTreeNodeMenu && !openTreeNodeMenu.wrap.contains(target)) {
4757
+ closeOpenTreeNodeMenu();
4794
4758
  }
4795
4759
  });
4796
4760
 
4761
+
4797
4762
  if (externalLinkCancelBtn) {
4798
4763
  externalLinkCancelBtn.addEventListener('click', function () {
4799
4764
  closeExternalLinkModalView();