@xcanwin/manyoyo 5.7.4 → 5.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -536,6 +536,60 @@ textarea:focus-visible {
536
536
  gap: 8px;
537
537
  }
538
538
 
539
+ button.tree-toggle {
540
+ width: 100%;
541
+ text-align: left;
542
+ color: var(--text);
543
+ background: rgba(255, 250, 242, 0.92);
544
+ border-color: rgba(181, 146, 99, 0.38);
545
+ padding: 10px 12px;
546
+ display: flex;
547
+ align-items: flex-start;
548
+ justify-content: space-between;
549
+ gap: 12px;
550
+ }
551
+
552
+ button.tree-toggle:hover {
553
+ transform: none;
554
+ background: #fff6eb;
555
+ border-color: #d1aa7f;
556
+ }
557
+
558
+ button.tree-toggle:active {
559
+ transform: none;
560
+ }
561
+
562
+ button.tree-toggle:focus-visible {
563
+ outline: none;
564
+ box-shadow: 0 0 0 3px rgba(196, 85, 31, 0.14);
565
+ }
566
+
567
+ .tree-toggle-main {
568
+ min-width: 0;
569
+ display: flex;
570
+ flex-direction: column;
571
+ gap: 4px;
572
+ }
573
+
574
+ .tree-toggle-meta {
575
+ color: var(--muted);
576
+ font-size: 11px;
577
+ line-height: 1.45;
578
+ word-break: break-word;
579
+ }
580
+
581
+ .tree-toggle-caret {
582
+ color: #8c7257;
583
+ font-size: 18px;
584
+ line-height: 1;
585
+ flex-shrink: 0;
586
+ transition: transform 140ms ease;
587
+ }
588
+
589
+ .tree-toggle[aria-expanded="true"] .tree-toggle-caret {
590
+ transform: rotate(90deg);
591
+ }
592
+
539
593
  .workbench-group-head {
540
594
  padding: 10px 12px;
541
595
  border: 1px solid rgba(181, 146, 99, 0.38);
@@ -543,6 +597,22 @@ textarea:focus-visible {
543
597
  background: rgba(255, 250, 242, 0.92);
544
598
  }
545
599
 
600
+ .workbench-group.has-active .workbench-group-head,
601
+ .container-card.has-active {
602
+ border-color: #c68d5a;
603
+ box-shadow: 0 0 0 2px rgba(196, 85, 31, 0.08);
604
+ }
605
+
606
+ .workbench-group-body {
607
+ display: flex;
608
+ flex-direction: column;
609
+ gap: 8px;
610
+ }
611
+
612
+ .workbench-group-body[hidden] {
613
+ display: none;
614
+ }
615
+
546
616
  .workbench-group-kicker,
547
617
  .container-card-kicker {
548
618
  color: var(--muted);
@@ -584,11 +654,8 @@ textarea:focus-visible {
584
654
  gap: 10px;
585
655
  }
586
656
 
587
- .container-card-info {
588
- min-width: 0;
589
- display: flex;
590
- flex-direction: column;
591
- gap: 4px;
657
+ .container-toggle {
658
+ flex: 1;
592
659
  }
593
660
 
594
661
  .container-card-meta {
@@ -611,6 +678,10 @@ textarea:focus-visible {
611
678
  gap: 6px;
612
679
  }
613
680
 
681
+ .agent-list[hidden] {
682
+ display: none;
683
+ }
684
+
614
685
  .agent-item {
615
686
  text-align: left;
616
687
  width: 100%;
@@ -1477,6 +1548,28 @@ details.trace-card > .trace-card-summary {
1477
1548
  }
1478
1549
  }
1479
1550
 
1551
+ .streaming-reply {
1552
+ border-color: var(--subaccent);
1553
+ box-shadow: 0 8px 16px rgba(15, 124, 114, 0.10);
1554
+ }
1555
+
1556
+ .streaming-cursor {
1557
+ display: inline-block;
1558
+ width: 7px;
1559
+ height: 16px;
1560
+ margin-left: 2px;
1561
+ vertical-align: text-bottom;
1562
+ background: var(--subaccent);
1563
+ border-radius: 2px;
1564
+ animation: blink-cursor 640ms steps(2, start) infinite;
1565
+ }
1566
+
1567
+ @keyframes blink-cursor {
1568
+ to {
1569
+ visibility: hidden;
1570
+ }
1571
+ }
1572
+
1480
1573
  @keyframes shimmer {
1481
1574
  100% {
1482
1575
  transform: translateX(100%);
@@ -58,6 +58,7 @@
58
58
  >更多</button>
59
59
  <div class="header-actions" id="headerActions">
60
60
  <button type="button" id="refreshBtn" class="secondary">刷新</button>
61
+ <button type="button" id="addAgentBtn" class="secondary">新建AGENT</button>
61
62
  <button type="button" id="removeBtn" class="danger-outline">删除容器</button>
62
63
  <button type="button" id="removeAllBtn" class="danger">删除对话</button>
63
64
  </div>
@@ -63,6 +63,12 @@
63
63
  createRuns: {},
64
64
  sessionNodeMap: new Map(),
65
65
  sessionRenderMode: 'empty',
66
+ sidebarTreeLoaded: false,
67
+ sidebarTree: {
68
+ directories: {},
69
+ containers: {}
70
+ },
71
+ pendingActiveSessionScroll: false,
66
72
  directoryPicker: {
67
73
  open: false,
68
74
  loading: false,
@@ -110,6 +116,7 @@
110
116
  const viewDetailBtn = document.getElementById('viewDetailBtn');
111
117
  const viewConfigBtn = document.getElementById('viewConfigBtn');
112
118
  const viewCheckBtn = document.getElementById('viewCheckBtn');
119
+ const addAgentBtn = document.getElementById('addAgentBtn');
113
120
  const mobileSidebarClose = document.getElementById('mobileSidebarClose');
114
121
  const sidebarBackdrop = document.getElementById('sidebarBackdrop');
115
122
  const openConfigBtn = document.getElementById('openConfigBtn');
@@ -205,12 +212,63 @@
205
212
  const GEMINI_YOLO_FLAG = '--yolo';
206
213
  const CODEX_DANGEROUS_FLAG = '--dangerously-bypass-approvals-and-sandbox';
207
214
  const OPENCODE_PERMISSION_KEY = 'OPENCODE_PERMISSION=';
215
+ const SIDEBAR_TREE_STORAGE_KEY = 'manyoyo.web.sidebarTree.v1';
208
216
  const markdownRenderer = window.ManyoyoMarkdown
209
217
  && typeof window.ManyoyoMarkdown.shouldRenderMessage === 'function'
210
218
  && typeof window.ManyoyoMarkdown.render === 'function'
211
219
  ? window.ManyoyoMarkdown
212
220
  : null;
213
221
 
222
+ function normalizeBooleanMap(source) {
223
+ const result = {};
224
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
225
+ return result;
226
+ }
227
+ Object.keys(source).forEach(function (key) {
228
+ if (typeof source[key] === 'boolean') {
229
+ result[String(key)] = source[key];
230
+ }
231
+ });
232
+ return result;
233
+ }
234
+
235
+ function loadSidebarTreeState() {
236
+ if (state.sidebarTreeLoaded) {
237
+ return;
238
+ }
239
+ state.sidebarTreeLoaded = true;
240
+ try {
241
+ if (!window.localStorage) {
242
+ return;
243
+ }
244
+ const raw = window.localStorage.getItem(SIDEBAR_TREE_STORAGE_KEY);
245
+ if (!raw) {
246
+ return;
247
+ }
248
+ const parsed = JSON.parse(raw);
249
+ state.sidebarTree = {
250
+ directories: normalizeBooleanMap(parsed && parsed.directories),
251
+ containers: normalizeBooleanMap(parsed && parsed.containers)
252
+ };
253
+ } catch (e) {
254
+ state.sidebarTree = {
255
+ directories: {},
256
+ containers: {}
257
+ };
258
+ }
259
+ }
260
+
261
+ function persistSidebarTreeState() {
262
+ try {
263
+ if (!window.localStorage) {
264
+ return;
265
+ }
266
+ window.localStorage.setItem(SIDEBAR_TREE_STORAGE_KEY, JSON.stringify(state.sidebarTree));
267
+ } catch (e) {
268
+ // 忽略浏览器存储异常,避免影响主流程
269
+ }
270
+ }
271
+
214
272
  function appendPlainMessageContent(bubble, content) {
215
273
  const pre = document.createElement('pre');
216
274
  pre.textContent = content == null ? '' : String(content);
@@ -412,6 +470,9 @@
412
470
  function roleName(role, message) {
413
471
  if (role === 'user') return '我';
414
472
  if (role === 'assistant') {
473
+ if (message && message.streamingReply) {
474
+ return 'AGENT 实时回复';
475
+ }
415
476
  if (message && message.streamTrace) {
416
477
  return 'AGENT 过程';
417
478
  }
@@ -459,6 +520,132 @@
459
520
  });
460
521
  }
461
522
 
523
+ function getSessionDirectoryPath(session) {
524
+ return String(session && session.hostPath ? session.hostPath : '').trim() || '未配置目录';
525
+ }
526
+
527
+ function getSessionContainerName(session) {
528
+ return String(session && session.containerName ? session.containerName : '').trim();
529
+ }
530
+
531
+ function findSessionByName(sessionName) {
532
+ const target = String(sessionName || '').trim();
533
+ if (!target) {
534
+ return null;
535
+ }
536
+ return state.sessions.find(function (session) {
537
+ return session && session.name === target;
538
+ }) || null;
539
+ }
540
+
541
+ function pruneSidebarTreeState() {
542
+ loadSidebarTreeState();
543
+
544
+ const validDirectories = new Set(state.sessions.map(getSessionDirectoryPath));
545
+ const validContainers = new Set(state.sessions.map(getSessionContainerName).filter(Boolean));
546
+ let changed = false;
547
+
548
+ Object.keys(state.sidebarTree.directories).forEach(function (key) {
549
+ if (!validDirectories.has(key)) {
550
+ delete state.sidebarTree.directories[key];
551
+ changed = true;
552
+ }
553
+ });
554
+
555
+ Object.keys(state.sidebarTree.containers).forEach(function (key) {
556
+ if (!validContainers.has(key)) {
557
+ delete state.sidebarTree.containers[key];
558
+ changed = true;
559
+ }
560
+ });
561
+
562
+ if (changed) {
563
+ persistSidebarTreeState();
564
+ }
565
+ }
566
+
567
+ function setSidebarDirectoryExpanded(directoryPath, expanded, options) {
568
+ const path = String(directoryPath || '').trim();
569
+ if (!path) {
570
+ return false;
571
+ }
572
+ loadSidebarTreeState();
573
+ const opts = options && typeof options === 'object' ? options : {};
574
+ if (state.sidebarTree.directories[path] === expanded) {
575
+ return false;
576
+ }
577
+ state.sidebarTree.directories[path] = expanded;
578
+ if (opts.persist !== false) {
579
+ persistSidebarTreeState();
580
+ }
581
+ return true;
582
+ }
583
+
584
+ function setSidebarContainerExpanded(containerName, expanded, options) {
585
+ const name = String(containerName || '').trim();
586
+ if (!name) {
587
+ return false;
588
+ }
589
+ loadSidebarTreeState();
590
+ const opts = options && typeof options === 'object' ? options : {};
591
+ if (state.sidebarTree.containers[name] === expanded) {
592
+ return false;
593
+ }
594
+ state.sidebarTree.containers[name] = expanded;
595
+ if (opts.persist !== false) {
596
+ persistSidebarTreeState();
597
+ }
598
+ return true;
599
+ }
600
+
601
+ function ensureSessionPathExpanded(sessionName, options) {
602
+ const session = findSessionByName(sessionName);
603
+ if (!session) {
604
+ return false;
605
+ }
606
+ const opts = options && typeof options === 'object' ? options : {};
607
+ const directoryPath = getSessionDirectoryPath(session);
608
+ const containerName = getSessionContainerName(session);
609
+ const changedDirectory = setSidebarDirectoryExpanded(directoryPath, true, { persist: false });
610
+ const changedContainer = setSidebarContainerExpanded(containerName, true, { persist: false });
611
+ if ((changedDirectory || changedContainer) && opts.persist !== false) {
612
+ persistSidebarTreeState();
613
+ }
614
+ return changedDirectory || changedContainer;
615
+ }
616
+
617
+ function directoryContainsActiveSession(directoryGroup) {
618
+ const groups = directoryGroup && Array.isArray(directoryGroup.containers) ? directoryGroup.containers : [];
619
+ return groups.some(function (containerGroup) {
620
+ return containerContainsActiveSession(containerGroup);
621
+ });
622
+ }
623
+
624
+ function containerContainsActiveSession(containerGroup) {
625
+ const sessions = containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions : [];
626
+ return sessions.some(function (session) {
627
+ return session && session.name === state.active;
628
+ });
629
+ }
630
+
631
+ function isDirectoryExpanded(directoryGroup) {
632
+ loadSidebarTreeState();
633
+ const key = String(directoryGroup && directoryGroup.path ? directoryGroup.path : '').trim();
634
+ if (key && typeof state.sidebarTree.directories[key] === 'boolean') {
635
+ return state.sidebarTree.directories[key];
636
+ }
637
+ return false;
638
+ }
639
+
640
+ function isContainerExpanded(containerGroup) {
641
+ loadSidebarTreeState();
642
+ const key = String(containerGroup && containerGroup.containerName ? containerGroup.containerName : '').trim();
643
+ if (key && typeof state.sidebarTree.containers[key] === 'boolean') {
644
+ return state.sidebarTree.containers[key];
645
+ }
646
+ return false;
647
+ }
648
+
462
649
  function normalizeSlashPath(value) {
463
650
  return String(value || '').replace(/\\/g, '/');
464
651
  }
@@ -1524,6 +1711,9 @@
1524
1711
  const activeAgentRunning = isAgentRunActiveForSession(state.active);
1525
1712
  const busy = state.loadingSessions || state.loadingMessages || state.sending;
1526
1713
  refreshBtn.disabled = busy;
1714
+ if (addAgentBtn) {
1715
+ addAgentBtn.disabled = !state.active || busy;
1716
+ }
1527
1717
  removeBtn.disabled = !state.active || busy;
1528
1718
  removeAllBtn.disabled = !state.active || busy;
1529
1719
  sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && !agentEnabled);
@@ -1966,6 +2156,7 @@
1966
2156
  disconnectTerminal('会话切换,终端已断开', true);
1967
2157
  }
1968
2158
  state.active = sessionName;
2159
+ ensureSessionPathExpanded(sessionName);
1969
2160
  state.sessionDetail = null;
1970
2161
  state.sessionDetailError = '';
1971
2162
  if (isMobileLayout()) {
@@ -2044,13 +2235,74 @@
2044
2235
  containerGroup.updatedAt = session.updatedAt;
2045
2236
  }
2046
2237
  });
2047
- return Array.from(groups.values()).sort(function (a, b) {
2238
+ return Array.from(groups.values()).map(function (group) {
2239
+ group.containers = Array.from(group.containers.values()).sort(function (a, b) {
2240
+ const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2241
+ const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
2242
+ return timeB - timeA;
2243
+ });
2244
+ group.containers.forEach(function (containerGroup) {
2245
+ containerGroup.sessions.sort(function (a, b) {
2246
+ const timeA = a && a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2247
+ const timeB = b && b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
2248
+ return timeB - timeA;
2249
+ });
2250
+ });
2251
+ return group;
2252
+ }).sort(function (a, b) {
2048
2253
  const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
2049
2254
  const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
2050
2255
  return timeB - timeA;
2051
2256
  });
2052
2257
  }
2053
2258
 
2259
+ function createTreeMetaText(parts) {
2260
+ return (parts || []).filter(Boolean).join(' · ');
2261
+ }
2262
+
2263
+ function createTreeToggle(options) {
2264
+ const opts = options && typeof options === 'object' ? options : {};
2265
+ const button = document.createElement('button');
2266
+ button.type = 'button';
2267
+ button.className = `tree-toggle ${opts.className || ''}`.trim();
2268
+ button.setAttribute('aria-expanded', opts.expanded ? 'true' : 'false');
2269
+
2270
+ const main = document.createElement('div');
2271
+ main.className = 'tree-toggle-main';
2272
+
2273
+ const kicker = document.createElement('div');
2274
+ kicker.className = opts.kickerClassName || 'workbench-group-kicker';
2275
+ kicker.textContent = opts.kicker || '';
2276
+ main.appendChild(kicker);
2277
+
2278
+ const title = document.createElement('div');
2279
+ title.className = opts.titleClassName || 'workbench-group-title';
2280
+ title.textContent = opts.title || '';
2281
+ main.appendChild(title);
2282
+
2283
+ const metaText = createTreeMetaText(opts.metaParts);
2284
+ if (metaText) {
2285
+ const meta = document.createElement('div');
2286
+ meta.className = 'tree-toggle-meta';
2287
+ meta.textContent = metaText;
2288
+ main.appendChild(meta);
2289
+ }
2290
+
2291
+ const caret = document.createElement('span');
2292
+ caret.className = 'tree-toggle-caret';
2293
+ caret.setAttribute('aria-hidden', 'true');
2294
+ caret.textContent = '›';
2295
+
2296
+ button.appendChild(main);
2297
+ button.appendChild(caret);
2298
+
2299
+ if (typeof opts.onClick === 'function') {
2300
+ button.addEventListener('click', opts.onClick);
2301
+ }
2302
+
2303
+ return button;
2304
+ }
2305
+
2054
2306
  function createAgentRow(session, index) {
2055
2307
  const status = sessionStatusInfo(session.status);
2056
2308
  const btn = document.createElement('button');
@@ -2091,6 +2343,19 @@
2091
2343
  return btn;
2092
2344
  }
2093
2345
 
2346
+ function scrollActiveSessionIntoView() {
2347
+ if (!state.pendingActiveSessionScroll || !state.active) {
2348
+ return;
2349
+ }
2350
+ state.pendingActiveSessionScroll = false;
2351
+ const targetNode = state.sessionNodeMap.get(state.active);
2352
+ if (targetNode && typeof targetNode.scrollIntoView === 'function') {
2353
+ targetNode.scrollIntoView({
2354
+ block: 'nearest'
2355
+ });
2356
+ }
2357
+ }
2358
+
2094
2359
  function renderSessions() {
2095
2360
  const containerCount = new Set(state.sessions.map(function (session) {
2096
2361
  return session && session.containerName ? session.containerName : '';
@@ -2123,38 +2388,65 @@
2123
2388
  let itemIndex = 0;
2124
2389
 
2125
2390
  grouped.forEach(function (directoryGroup) {
2391
+ const directoryExpanded = isDirectoryExpanded(directoryGroup);
2392
+ const directoryHasActive = directoryContainsActiveSession(directoryGroup);
2126
2393
  const group = document.createElement('section');
2127
2394
  group.className = 'workbench-group';
2128
-
2129
- const groupHead = document.createElement('div');
2130
- groupHead.className = 'workbench-group-head';
2131
- groupHead.innerHTML = `
2132
- <div class="workbench-group-kicker">目录</div>
2133
- <div class="workbench-group-title">${escapeHtml(directoryGroup.path)}</div>
2134
- `;
2395
+ group.classList.toggle('has-active', directoryHasActive);
2396
+
2397
+ const groupHead = createTreeToggle({
2398
+ className: 'workbench-group-head',
2399
+ kickerClassName: 'workbench-group-kicker',
2400
+ titleClassName: 'workbench-group-title',
2401
+ kicker: '目录',
2402
+ title: directoryGroup.path,
2403
+ expanded: directoryExpanded,
2404
+ metaParts: [
2405
+ `${directoryGroup.containers.length} 个容器`,
2406
+ `${directoryGroup.containers.reduce(function (count, containerGroup) {
2407
+ return count + containerGroup.sessions.length;
2408
+ }, 0)} 个AGENT`,
2409
+ formatDateTime(directoryGroup.updatedAt) || '暂无更新'
2410
+ ],
2411
+ onClick: function () {
2412
+ setSidebarDirectoryExpanded(directoryGroup.path, !directoryExpanded);
2413
+ renderSessions();
2414
+ }
2415
+ });
2135
2416
  group.appendChild(groupHead);
2136
2417
 
2137
2418
  const containerStack = document.createElement('div');
2138
- containerStack.className = 'container-stack';
2419
+ containerStack.className = 'container-stack workbench-group-body';
2420
+ containerStack.hidden = !directoryExpanded;
2139
2421
 
2140
- Array.from(directoryGroup.containers.values()).forEach(function (containerGroup) {
2422
+ directoryGroup.containers.forEach(function (containerGroup) {
2423
+ const containerExpanded = isContainerExpanded(containerGroup);
2424
+ const containerHasActive = containerContainsActiveSession(containerGroup);
2141
2425
  const status = sessionStatusInfo(containerGroup.status);
2142
2426
  const containerCard = document.createElement('section');
2143
2427
  containerCard.className = 'container-card';
2428
+ containerCard.classList.toggle('has-active', containerHasActive);
2144
2429
 
2145
2430
  const containerHead = document.createElement('div');
2146
2431
  containerHead.className = 'container-card-head';
2147
2432
 
2148
- const containerInfo = document.createElement('div');
2149
- containerInfo.className = 'container-card-info';
2150
- containerInfo.innerHTML = `
2151
- <div class="container-card-kicker">容器</div>
2152
- <div class="container-card-title">${escapeHtml(containerGroup.containerName)}</div>
2153
- <div class="container-card-meta">
2154
- <span class="session-status ${status.tone}">${escapeHtml(status.label)}</span>
2155
- <span>${escapeHtml(formatDateTime(containerGroup.updatedAt) || '暂无更新')}</span>
2156
- </div>
2157
- `;
2433
+ const containerInfo = createTreeToggle({
2434
+ className: 'container-toggle',
2435
+ kickerClassName: 'container-card-kicker',
2436
+ titleClassName: 'container-card-title',
2437
+ kicker: '容器',
2438
+ title: containerGroup.containerName,
2439
+ expanded: containerExpanded,
2440
+ metaParts: [
2441
+ status.label,
2442
+ `${containerGroup.sessions.length} 个AGENT`,
2443
+ formatDateTime(containerGroup.updatedAt) || '暂无更新'
2444
+ ],
2445
+ onClick: function () {
2446
+ setSidebarContainerExpanded(containerGroup.containerName, !containerExpanded);
2447
+ renderSessions();
2448
+ }
2449
+ });
2158
2450
 
2159
2451
  const addAgentBtn = document.createElement('button');
2160
2452
  addAgentBtn.type = 'button';
@@ -2170,6 +2462,7 @@
2170
2462
 
2171
2463
  const agentList = document.createElement('div');
2172
2464
  agentList.className = 'agent-list';
2465
+ agentList.hidden = !containerExpanded;
2173
2466
  containerGroup.sessions.forEach(function (session) {
2174
2467
  const row = createAgentRow(session, itemIndex);
2175
2468
  state.sessionNodeMap.set(session.name, row);
@@ -2183,6 +2476,8 @@
2183
2476
  group.appendChild(containerStack);
2184
2477
  sessionList.appendChild(group);
2185
2478
  });
2479
+
2480
+ scrollActiveSessionIntoView();
2186
2481
  }
2187
2482
 
2188
2483
  function renderMessagesLoading() {
@@ -2216,6 +2511,11 @@
2216
2511
  }
2217
2512
 
2218
2513
  function getMessageRenderKey(msg, index) {
2514
+ if (msg && msg.id && msg.streamingReply) {
2515
+ const content = msg.content ? String(msg.content) : '';
2516
+ const timestamp = msg.timestamp ? String(msg.timestamp) : '';
2517
+ return `id:${msg.id}|streaming|${timestamp}|${content}`;
2518
+ }
2219
2519
  if (msg && msg.id && msg.streamTrace) {
2220
2520
  const content = msg.content ? String(msg.content) : '';
2221
2521
  const timestamp = msg.timestamp ? String(msg.timestamp) : '';
@@ -2267,14 +2567,39 @@
2267
2567
  const bubble = document.createElement('div');
2268
2568
  bubble.className = 'bubble';
2269
2569
 
2570
+ const isStreamingReply = Boolean(msg && msg.streamingReply);
2270
2571
  const shouldRenderStructuredTrace = Boolean(
2271
2572
  msg
2272
2573
  && msg.streamTrace
2273
2574
  && Array.isArray(msg.traceEvents)
2274
2575
  && msg.traceEvents.length
2275
2576
  );
2276
- const shouldRenderMarkdown = Boolean(!msg.streamTrace && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
2277
- if (shouldRenderStructuredTrace) {
2577
+ const shouldRenderMarkdown = Boolean(!msg.streamTrace && !msg.streamingReply && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
2578
+ if (isStreamingReply) {
2579
+ bubble.classList.add('streaming-reply');
2580
+ var replyContent = String(msg.content || '');
2581
+ if (replyContent && markdownRenderer && typeof markdownRenderer.render === 'function') {
2582
+ var mdNode = document.createElement('div');
2583
+ mdNode.className = 'md-content';
2584
+ var rendered = '';
2585
+ try {
2586
+ rendered = String(markdownRenderer.render(replyContent) || '');
2587
+ } catch (e) {
2588
+ rendered = '';
2589
+ }
2590
+ if (rendered) {
2591
+ mdNode.innerHTML = rendered;
2592
+ bubble.appendChild(mdNode);
2593
+ } else {
2594
+ appendPlainMessageContent(bubble, replyContent);
2595
+ }
2596
+ } else {
2597
+ appendPlainMessageContent(bubble, replyContent);
2598
+ }
2599
+ var cursor = document.createElement('span');
2600
+ cursor.className = 'streaming-cursor';
2601
+ bubble.appendChild(cursor);
2602
+ } else if (shouldRenderStructuredTrace) {
2278
2603
  appendStructuredTraceContent(bubble, msg);
2279
2604
  } else if (shouldRenderMarkdown) {
2280
2605
  const markdownNode = document.createElement('div');
@@ -2365,7 +2690,9 @@
2365
2690
  }
2366
2691
 
2367
2692
  function applySessionsSnapshot(rawSessions, preferredName) {
2693
+ const previousActive = state.active;
2368
2694
  state.sessions = Array.isArray(rawSessions) ? rawSessions : [];
2695
+ pruneSidebarTreeState();
2369
2696
 
2370
2697
  if (typeof preferredName === 'string' && preferredName.trim()) {
2371
2698
  state.active = preferredName.trim();
@@ -2379,6 +2706,10 @@
2379
2706
  if (!state.active && state.sessions.length) {
2380
2707
  state.active = state.sessions[0].name;
2381
2708
  }
2709
+ if (state.active && state.active !== previousActive) {
2710
+ ensureSessionPathExpanded(state.active);
2711
+ state.pendingActiveSessionScroll = true;
2712
+ }
2382
2713
  if (state.terminal.sessionName && state.terminal.sessionName !== state.active) {
2383
2714
  disconnectTerminal('会话已变化,终端已断开', true);
2384
2715
  }
@@ -2638,11 +2969,55 @@
2638
2969
  });
2639
2970
  }
2640
2971
 
2972
+ function appendStreamingReplyLocal(sessionName) {
2973
+ var replyMessage = {
2974
+ id: createLocalMessageId('local-streaming-reply'),
2975
+ role: 'assistant',
2976
+ content: '',
2977
+ timestamp: new Date().toISOString(),
2978
+ mode: 'agent',
2979
+ streamingReply: true
2980
+ };
2981
+ if (state.active === sessionName) {
2982
+ state.messages.push(replyMessage);
2983
+ }
2984
+ return replyMessage.id;
2985
+ }
2986
+
2987
+ function updateStreamingReplyLocal(sessionName, replyMessageId, content) {
2988
+ if (state.active !== sessionName) {
2989
+ return;
2990
+ }
2991
+ for (var i = state.messages.length - 1; i >= 0; i -= 1) {
2992
+ var message = state.messages[i];
2993
+ if (!message || String(message.id || '') !== String(replyMessageId || '')) {
2994
+ continue;
2995
+ }
2996
+ message.content = String(content || '');
2997
+ message.timestamp = new Date().toISOString();
2998
+ return;
2999
+ }
3000
+ }
3001
+
3002
+ function removeStreamingReplyLocal(sessionName, replyMessageId) {
3003
+ if (state.active !== sessionName) {
3004
+ return;
3005
+ }
3006
+ for (var i = state.messages.length - 1; i >= 0; i -= 1) {
3007
+ var message = state.messages[i];
3008
+ if (message && String(message.id || '') === String(replyMessageId || '') && message.streamingReply) {
3009
+ state.messages.splice(i, 1);
3010
+ return;
3011
+ }
3012
+ }
3013
+ }
3014
+
2641
3015
  async function sendAgentPromptStream(sessionName, inputText, pendingMessage) {
2642
3016
  const traceMessageId = appendAgentTraceMessageLocal(sessionName);
2643
3017
  const traceLines = ['[执行过程]', '等待 Agent 启动…'];
2644
3018
  let finalResult = null;
2645
3019
  let streamError = null;
3020
+ let streamingReplyId = null;
2646
3021
 
2647
3022
  state.agentRun.active = true;
2648
3023
  state.agentRun.sessionName = sessionName;
@@ -2692,6 +3067,20 @@
2692
3067
  pushTraceLine(event.text || '', event.traceEvent || null);
2693
3068
  return;
2694
3069
  }
3070
+ if (event.type === 'content_delta') {
3071
+ var content = String(event.content || '').trim();
3072
+ if (!content) {
3073
+ return;
3074
+ }
3075
+ if (!streamingReplyId) {
3076
+ streamingReplyId = appendStreamingReplyLocal(sessionName);
3077
+ }
3078
+ updateStreamingReplyLocal(sessionName, streamingReplyId, content);
3079
+ if (state.active === sessionName) {
3080
+ renderMessages(state.messages, { stickToBottom: true });
3081
+ }
3082
+ return;
3083
+ }
2695
3084
  if (event.type === 'result') {
2696
3085
  finalResult = event;
2697
3086
  if (event.interrupted) {
@@ -2711,6 +3100,9 @@
2711
3100
  streamError = e;
2712
3101
  }
2713
3102
  } finally {
3103
+ if (streamingReplyId) {
3104
+ removeStreamingReplyLocal(sessionName, streamingReplyId);
3105
+ }
2714
3106
  const pendingIndex = confirmPendingUserMessage(sessionName, pendingMessage.id);
2715
3107
  if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
2716
3108
  if (pendingIndex < messagesNode.children.length) {
@@ -3099,6 +3491,18 @@
3099
3491
  });
3100
3492
  }
3101
3493
 
3494
+ if (addAgentBtn) {
3495
+ addAgentBtn.addEventListener('click', function () {
3496
+ const activeSession = getActiveSession();
3497
+ const targetContainer = activeSession ? getSessionContainerName(activeSession) : '';
3498
+ if (!targetContainer) {
3499
+ return;
3500
+ }
3501
+ closeMobileActionsMenu();
3502
+ createAgentSession(targetContainer);
3503
+ });
3504
+ }
3505
+
3102
3506
  if (configModal) {
3103
3507
  configModal.addEventListener('click', function (event) {
3104
3508
  if (event.target === configModal && !state.configSaving) {
@@ -3223,6 +3627,7 @@
3223
3627
  disconnectTerminal('', true);
3224
3628
  });
3225
3629
 
3630
+ loadSidebarTreeState();
3226
3631
  renderSessions();
3227
3632
  renderMessages(state.messages);
3228
3633
  setMobileSessionPanel(false);
package/lib/web/server.js CHANGED
@@ -1053,6 +1053,48 @@ function prepareStructuredTraceEvents(agentProgram, payload, state) {
1053
1053
  return [];
1054
1054
  }
1055
1055
 
1056
+ function extractContentDeltaFromPayload(agentProgram, payload) {
1057
+ if (!payload || typeof payload !== 'object') {
1058
+ return null;
1059
+ }
1060
+ if (agentProgram === 'claude') {
1061
+ if (pickFirstString(payload.type) !== 'assistant') {
1062
+ return null;
1063
+ }
1064
+ const message = toPlainObject(payload.message);
1065
+ const content = Array.isArray(message.content) ? message.content : [];
1066
+ const text = content
1067
+ .filter(item => item && typeof item === 'object' && item.type === 'text')
1068
+ .map(item => collectStructuredText(item))
1069
+ .filter(Boolean)
1070
+ .join('\n')
1071
+ .trim();
1072
+ if (!text) {
1073
+ return null;
1074
+ }
1075
+ return { text, reset: true };
1076
+ }
1077
+ if (agentProgram === 'gemini' || agentProgram === 'opencode') {
1078
+ const eventType = pickFirstString(payload.type);
1079
+ if (eventType !== 'message') {
1080
+ return null;
1081
+ }
1082
+ const role = pickFirstString(payload.role);
1083
+ if (role !== 'assistant') {
1084
+ return null;
1085
+ }
1086
+ const text = collectStructuredText(payload.content);
1087
+ if (!text) {
1088
+ return null;
1089
+ }
1090
+ if (payload.delta === true) {
1091
+ return { text, reset: false };
1092
+ }
1093
+ return { text, reset: true };
1094
+ }
1095
+ return null;
1096
+ }
1097
+
1056
1098
  function prepareCodexTraceEvent(payload) {
1057
1099
  if (!payload || typeof payload !== 'object') {
1058
1100
  return null;
@@ -2281,6 +2323,7 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
2281
2323
  const structuredTraceState = {
2282
2324
  toolNamesById: new Map()
2283
2325
  };
2326
+ let contentDeltaAccumulator = '';
2284
2327
  function appendChunk(chunk, target) {
2285
2328
  if (!chunk) return;
2286
2329
  const text = chunk.toString('utf-8');
@@ -2318,6 +2361,18 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
2318
2361
  traceEvent
2319
2362
  });
2320
2363
  });
2364
+ const deltaContent = extractContentDeltaFromPayload(agentProgram, payload, structuredTraceState);
2365
+ if (deltaContent !== null) {
2366
+ if (deltaContent.reset) {
2367
+ contentDeltaAccumulator = deltaContent.text;
2368
+ } else {
2369
+ contentDeltaAccumulator += deltaContent.text;
2370
+ }
2371
+ onEvent({
2372
+ type: 'content_delta',
2373
+ content: contentDeltaAccumulator
2374
+ });
2375
+ }
2321
2376
  return;
2322
2377
  }
2323
2378
  if (agentProgram === 'codex' && (/^OpenAI Codex\b/.test(rawLine) || /^tokens used\b/i.test(rawLine))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.7.4",
3
+ "version": "5.7.6",
4
4
  "imageVersion": "1.9.0-common",
5
5
  "playwrightCliVersion": "0.1.1",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",