@xcanwin/manyoyo 5.7.4 → 5.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@
13
13
 
14
14
  --line: #d9c7ad;
15
15
  --line-strong: #b59263;
16
+ --line-soft: rgba(181, 146, 99, 0.26);
16
17
 
17
18
  --text: #1f1a14;
18
19
  --muted: #6a5f52;
@@ -415,7 +416,7 @@ textarea:focus-visible {
415
416
  padding-right: 4px;
416
417
  display: flex;
417
418
  flex-direction: column;
418
- gap: 8px;
419
+ gap: 10px;
419
420
  }
420
421
 
421
422
  #sessionList::-webkit-scrollbar,
@@ -533,14 +534,126 @@ textarea:focus-visible {
533
534
  .workbench-group {
534
535
  display: flex;
535
536
  flex-direction: column;
536
- gap: 8px;
537
+ gap: 10px;
537
538
  }
538
539
 
539
- .workbench-group-head {
540
- padding: 10px 12px;
541
- border: 1px solid rgba(181, 146, 99, 0.38);
542
- border-radius: 12px;
540
+ button.tree-toggle {
541
+ width: 100%;
542
+ text-align: left;
543
+ color: var(--text);
543
544
  background: rgba(255, 250, 242, 0.92);
545
+ border-color: rgba(181, 146, 99, 0.38);
546
+ padding: 10px 12px;
547
+ display: flex;
548
+ align-items: flex-start;
549
+ justify-content: space-between;
550
+ gap: 12px;
551
+ }
552
+
553
+ button.tree-toggle:hover {
554
+ transform: none;
555
+ background: #fff6eb;
556
+ border-color: #d1aa7f;
557
+ }
558
+
559
+ button.tree-toggle:active {
560
+ transform: none;
561
+ }
562
+
563
+ button.tree-toggle:focus-visible {
564
+ outline: none;
565
+ box-shadow: 0 0 0 3px rgba(196, 85, 31, 0.14);
566
+ }
567
+
568
+ .tree-toggle-main {
569
+ min-width: 0;
570
+ display: flex;
571
+ flex-direction: column;
572
+ gap: 4px;
573
+ }
574
+
575
+ .tree-toggle-meta {
576
+ color: var(--muted);
577
+ font-size: 11px;
578
+ line-height: 1.45;
579
+ word-break: break-word;
580
+ }
581
+
582
+ .tree-toggle-caret {
583
+ color: #8c7257;
584
+ font-size: 18px;
585
+ line-height: 1;
586
+ flex-shrink: 0;
587
+ transition: transform 140ms ease;
588
+ }
589
+
590
+ .tree-toggle[aria-expanded="true"] .tree-toggle-caret {
591
+ transform: rotate(90deg);
592
+ }
593
+
594
+ .workbench-group-head {
595
+ padding: 10px 14px;
596
+ border: 1px solid rgba(181, 146, 99, 0.34);
597
+ border-radius: 999px;
598
+ background: linear-gradient(180deg, rgba(255, 250, 242, 0.98) 0%, rgba(252, 242, 230, 0.96) 100%);
599
+ }
600
+
601
+ .workbench-path-bar .tree-toggle-main {
602
+ flex: 1;
603
+ min-width: 0;
604
+ display: grid;
605
+ grid-template-columns: auto minmax(0, 1fr);
606
+ align-items: center;
607
+ column-gap: 10px;
608
+ row-gap: 3px;
609
+ }
610
+
611
+ .workbench-path-bar .workbench-group-kicker {
612
+ grid-column: 1;
613
+ grid-row: 1;
614
+ display: inline-flex;
615
+ align-items: center;
616
+ padding: 3px 8px;
617
+ border-radius: 999px;
618
+ border: 1px solid rgba(181, 146, 99, 0.34);
619
+ background: rgba(255, 255, 255, 0.56);
620
+ }
621
+
622
+ .workbench-path-bar .workbench-group-title {
623
+ grid-column: 2;
624
+ grid-row: 1;
625
+ margin-top: 0;
626
+ min-width: 0;
627
+ overflow: hidden;
628
+ text-overflow: ellipsis;
629
+ white-space: nowrap;
630
+ font-family: var(--font-mono);
631
+ font-size: 12px;
632
+ font-weight: 600;
633
+ }
634
+
635
+ .workbench-path-bar .tree-toggle-meta {
636
+ grid-column: 2;
637
+ grid-row: 2;
638
+ }
639
+
640
+ .workbench-group.has-active .workbench-group-head,
641
+ .container-card.has-active {
642
+ border-color: #c68d5a;
643
+ box-shadow: 0 0 0 2px rgba(196, 85, 31, 0.08);
644
+ }
645
+
646
+ .workbench-group-body {
647
+ display: flex;
648
+ flex-direction: column;
649
+ gap: 10px;
650
+ margin-left: 14px;
651
+ padding-left: 12px;
652
+ border-left: 1px solid var(--line-soft);
653
+ }
654
+
655
+ .workbench-group-body[hidden] {
656
+ display: none;
544
657
  }
545
658
 
546
659
  .workbench-group-kicker,
@@ -564,61 +677,170 @@ textarea:focus-visible {
564
677
  .container-stack {
565
678
  display: flex;
566
679
  flex-direction: column;
567
- gap: 8px;
680
+ gap: 10px;
568
681
  }
569
682
 
570
683
  .container-card {
571
- border: 1px solid var(--line);
572
- border-radius: 12px;
573
- background: rgba(255, 255, 255, 0.9);
574
- padding: 10px;
684
+ border: 1px solid rgba(181, 146, 99, 0.52);
685
+ border-radius: 16px;
686
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(249, 242, 232, 0.98) 100%);
687
+ padding: 12px;
575
688
  display: flex;
576
689
  flex-direction: column;
577
690
  gap: 10px;
691
+ box-shadow: 0 10px 22px rgba(44, 31, 15, 0.08);
578
692
  }
579
693
 
580
694
  .container-card-head {
695
+ display: flex;
696
+ flex-wrap: wrap;
697
+ align-items: flex-start;
698
+ justify-content: space-between;
699
+ gap: 12px;
700
+ }
701
+
702
+ .container-toggle {
703
+ flex: 1;
704
+ min-width: 0;
705
+ padding: 0;
706
+ border: none;
707
+ background: transparent;
708
+ color: var(--text);
581
709
  display: flex;
582
710
  align-items: flex-start;
583
711
  justify-content: space-between;
712
+ gap: 12px;
713
+ }
714
+
715
+ button.container-toggle:hover,
716
+ button.container-toggle:active {
717
+ transform: none;
718
+ background: transparent;
719
+ }
720
+
721
+ button.container-toggle:focus-visible {
722
+ outline: none;
723
+ box-shadow: 0 0 0 3px rgba(196, 85, 31, 0.14);
724
+ }
725
+
726
+ .container-toggle-main {
727
+ min-width: 0;
728
+ flex: 1;
729
+ display: flex;
730
+ flex-direction: column;
731
+ gap: 8px;
732
+ }
733
+
734
+ .container-title-row {
735
+ display: flex;
736
+ align-items: flex-start;
584
737
  gap: 10px;
738
+ min-width: 0;
585
739
  }
586
740
 
587
- .container-card-info {
741
+ .container-title-stack {
588
742
  min-width: 0;
589
743
  display: flex;
590
744
  flex-direction: column;
591
745
  gap: 4px;
592
746
  }
593
747
 
748
+ .sidebar-icon {
749
+ width: 32px;
750
+ height: 32px;
751
+ flex: 0 0 auto;
752
+ display: inline-flex;
753
+ align-items: center;
754
+ justify-content: center;
755
+ border-radius: 11px;
756
+ border: 1px solid rgba(181, 146, 99, 0.34);
757
+ background: linear-gradient(180deg, rgba(255, 244, 232, 0.96) 0%, rgba(255, 252, 247, 0.9) 100%);
758
+ color: var(--accent-strong);
759
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
760
+ }
761
+
762
+ .sidebar-icon svg {
763
+ width: 18px;
764
+ height: 18px;
765
+ display: block;
766
+ }
767
+
594
768
  .container-card-meta {
595
769
  display: flex;
596
770
  flex-wrap: wrap;
597
- gap: 6px;
771
+ gap: 8px;
598
772
  align-items: center;
599
- color: var(--muted);
773
+ }
774
+
775
+ .sidebar-badge {
776
+ display: inline-flex;
777
+ align-items: center;
778
+ gap: 4px;
779
+ min-height: 26px;
780
+ padding: 4px 10px;
781
+ border-radius: 999px;
782
+ border: 1px solid transparent;
783
+ font-size: 11px;
784
+ font-weight: 700;
785
+ line-height: 1.2;
786
+ }
787
+
788
+ .container-status-pill {
789
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55);
790
+ }
791
+
792
+ .container-agent-badge {
793
+ background: rgba(15, 124, 114, 0.1);
794
+ border-color: rgba(15, 124, 114, 0.22);
795
+ color: var(--subaccent-strong);
796
+ }
797
+
798
+ .container-card-info {
799
+ color: #7b6d5d;
600
800
  font-size: 11px;
801
+ line-height: 1.45;
802
+ white-space: nowrap;
803
+ overflow: hidden;
804
+ text-overflow: ellipsis;
601
805
  }
602
806
 
603
807
  .add-agent-btn {
604
- padding: 7px 10px;
808
+ align-self: flex-start;
809
+ padding: 7px 11px;
605
810
  font-size: 12px;
811
+ border-radius: 999px;
812
+ white-space: nowrap;
813
+ margin-bottom: 2px;
606
814
  }
607
815
 
608
816
  .agent-list {
609
817
  display: flex;
610
818
  flex-direction: column;
611
- gap: 6px;
819
+ gap: 7px;
820
+ }
821
+
822
+ .container-card-body {
823
+ margin-top: 2px;
824
+ padding: 10px;
825
+ border: 1px solid rgba(181, 146, 99, 0.28);
826
+ border-top-color: rgba(181, 146, 99, 0.4);
827
+ border-radius: 13px;
828
+ background: linear-gradient(180deg, rgba(244, 238, 228, 0.88) 0%, rgba(255, 255, 255, 0.98) 100%);
829
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88);
830
+ }
831
+
832
+ .agent-list[hidden] {
833
+ display: none;
612
834
  }
613
835
 
614
836
  .agent-item {
615
837
  text-align: left;
616
838
  width: 100%;
617
- border: 1px solid rgba(181, 146, 99, 0.42);
618
- background: var(--panel-strong);
839
+ border: 1px solid rgba(181, 146, 99, 0.38);
840
+ background: rgba(255, 255, 255, 0.94);
619
841
  color: var(--text);
620
- border-radius: 10px;
621
- padding: 10px;
842
+ border-radius: 12px;
843
+ padding: 10px 11px;
622
844
  animation: itemIn 220ms ease both;
623
845
  animation-delay: calc(var(--item-index, 0) * 24ms);
624
846
  }
@@ -1477,6 +1699,28 @@ details.trace-card > .trace-card-summary {
1477
1699
  }
1478
1700
  }
1479
1701
 
1702
+ .streaming-reply {
1703
+ border-color: var(--subaccent);
1704
+ box-shadow: 0 8px 16px rgba(15, 124, 114, 0.10);
1705
+ }
1706
+
1707
+ .streaming-cursor {
1708
+ display: inline-block;
1709
+ width: 7px;
1710
+ height: 16px;
1711
+ margin-left: 2px;
1712
+ vertical-align: text-bottom;
1713
+ background: var(--subaccent);
1714
+ border-radius: 2px;
1715
+ animation: blink-cursor 640ms steps(2, start) infinite;
1716
+ }
1717
+
1718
+ @keyframes blink-cursor {
1719
+ to {
1720
+ visibility: hidden;
1721
+ }
1722
+ }
1723
+
1480
1724
  @keyframes shimmer {
1481
1725
  100% {
1482
1726
  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,160 @@
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
+ title.title = opts.title || '';
2282
+ main.appendChild(title);
2283
+
2284
+ const metaText = createTreeMetaText(opts.metaParts);
2285
+ if (metaText) {
2286
+ const meta = document.createElement('div');
2287
+ meta.className = 'tree-toggle-meta';
2288
+ meta.textContent = metaText;
2289
+ main.appendChild(meta);
2290
+ }
2291
+
2292
+ const caret = document.createElement('span');
2293
+ caret.className = 'tree-toggle-caret';
2294
+ caret.setAttribute('aria-hidden', 'true');
2295
+ caret.textContent = '›';
2296
+
2297
+ button.appendChild(main);
2298
+ button.appendChild(caret);
2299
+
2300
+ if (typeof opts.onClick === 'function') {
2301
+ button.addEventListener('click', opts.onClick);
2302
+ }
2303
+
2304
+ return button;
2305
+ }
2306
+
2307
+ function createSidebarIcon(name) {
2308
+ const icon = document.createElement('span');
2309
+ icon.className = `sidebar-icon ${name ? `sidebar-icon-${name}` : ''}`.trim();
2310
+ icon.setAttribute('aria-hidden', 'true');
2311
+
2312
+ if (name === 'cube') {
2313
+ icon.innerHTML = [
2314
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">',
2315
+ '<path d="M12 3.5 4.75 7.5 12 11.5l7.25-4-7.25-4Z"></path>',
2316
+ '<path d="M4.75 7.5v9L12 20.5l7.25-4v-9"></path>',
2317
+ '<path d="M12 11.5v9"></path>',
2318
+ '</svg>'
2319
+ ].join('');
2320
+ }
2321
+
2322
+ return icon;
2323
+ }
2324
+
2325
+ function createSidebarBadge(text, className) {
2326
+ const badge = document.createElement('span');
2327
+ badge.className = `sidebar-badge ${className || ''}`.trim();
2328
+ badge.textContent = text || '';
2329
+ return badge;
2330
+ }
2331
+
2332
+ function createContainerToggle(containerGroup, expanded) {
2333
+ const status = sessionStatusInfo(containerGroup && containerGroup.status);
2334
+ const button = document.createElement('button');
2335
+ button.type = 'button';
2336
+ button.className = 'tree-toggle container-toggle';
2337
+ button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
2338
+
2339
+ const main = document.createElement('div');
2340
+ main.className = 'container-toggle-main';
2341
+
2342
+ const titleRow = document.createElement('div');
2343
+ titleRow.className = 'container-title-row';
2344
+ titleRow.appendChild(createSidebarIcon('cube'));
2345
+
2346
+ const titleStack = document.createElement('div');
2347
+ titleStack.className = 'container-title-stack';
2348
+
2349
+ const kicker = document.createElement('div');
2350
+ kicker.className = 'container-card-kicker';
2351
+ kicker.textContent = '容器';
2352
+
2353
+ const title = document.createElement('div');
2354
+ title.className = 'container-card-title';
2355
+ title.textContent = containerGroup && containerGroup.containerName ? containerGroup.containerName : '';
2356
+ title.title = title.textContent;
2357
+
2358
+ titleStack.appendChild(kicker);
2359
+ titleStack.appendChild(title);
2360
+ titleRow.appendChild(titleStack);
2361
+ main.appendChild(titleRow);
2362
+
2363
+ const meta = document.createElement('div');
2364
+ meta.className = 'container-card-meta';
2365
+ meta.appendChild(createSidebarBadge(status.label, `session-status ${status.tone} container-status-pill`));
2366
+ meta.appendChild(createSidebarBadge(`${containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions.length : 0} 个 AGENT`, 'container-agent-badge'));
2367
+ main.appendChild(meta);
2368
+
2369
+ const infoText = createTreeMetaText([
2370
+ containerGroup && containerGroup.image ? containerGroup.image : '',
2371
+ formatDateTime(containerGroup && containerGroup.updatedAt) || '暂无更新'
2372
+ ]);
2373
+ if (infoText) {
2374
+ const info = document.createElement('div');
2375
+ info.className = 'container-card-info';
2376
+ info.textContent = infoText;
2377
+ info.title = infoText;
2378
+ main.appendChild(info);
2379
+ }
2380
+
2381
+ const caret = document.createElement('span');
2382
+ caret.className = 'tree-toggle-caret';
2383
+ caret.setAttribute('aria-hidden', 'true');
2384
+ caret.textContent = '›';
2385
+
2386
+ button.appendChild(main);
2387
+ button.appendChild(caret);
2388
+
2389
+ return button;
2390
+ }
2391
+
2054
2392
  function createAgentRow(session, index) {
2055
2393
  const status = sessionStatusInfo(session.status);
2056
2394
  const btn = document.createElement('button');
@@ -2091,6 +2429,19 @@
2091
2429
  return btn;
2092
2430
  }
2093
2431
 
2432
+ function scrollActiveSessionIntoView() {
2433
+ if (!state.pendingActiveSessionScroll || !state.active) {
2434
+ return;
2435
+ }
2436
+ state.pendingActiveSessionScroll = false;
2437
+ const targetNode = state.sessionNodeMap.get(state.active);
2438
+ if (targetNode && typeof targetNode.scrollIntoView === 'function') {
2439
+ targetNode.scrollIntoView({
2440
+ block: 'nearest'
2441
+ });
2442
+ }
2443
+ }
2444
+
2094
2445
  function renderSessions() {
2095
2446
  const containerCount = new Set(state.sessions.map(function (session) {
2096
2447
  return session && session.containerName ? session.containerName : '';
@@ -2123,38 +2474,52 @@
2123
2474
  let itemIndex = 0;
2124
2475
 
2125
2476
  grouped.forEach(function (directoryGroup) {
2477
+ const directoryExpanded = isDirectoryExpanded(directoryGroup);
2478
+ const directoryHasActive = directoryContainsActiveSession(directoryGroup);
2126
2479
  const group = document.createElement('section');
2127
2480
  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
- `;
2481
+ group.classList.toggle('has-active', directoryHasActive);
2482
+
2483
+ const groupHead = createTreeToggle({
2484
+ className: 'workbench-group-head workbench-path-bar',
2485
+ kickerClassName: 'workbench-group-kicker',
2486
+ titleClassName: 'workbench-group-title',
2487
+ kicker: '路径分组',
2488
+ title: directoryGroup.path,
2489
+ expanded: directoryExpanded,
2490
+ metaParts: [
2491
+ `${directoryGroup.containers.length} 容器`,
2492
+ `${directoryGroup.containers.reduce(function (count, containerGroup) {
2493
+ return count + containerGroup.sessions.length;
2494
+ }, 0)} AGENT`,
2495
+ formatDateTime(directoryGroup.updatedAt) || '暂无更新'
2496
+ ],
2497
+ onClick: function () {
2498
+ setSidebarDirectoryExpanded(directoryGroup.path, !directoryExpanded);
2499
+ renderSessions();
2500
+ }
2501
+ });
2135
2502
  group.appendChild(groupHead);
2136
2503
 
2137
2504
  const containerStack = document.createElement('div');
2138
- containerStack.className = 'container-stack';
2505
+ containerStack.className = 'container-stack workbench-group-body';
2506
+ containerStack.hidden = !directoryExpanded;
2139
2507
 
2140
- Array.from(directoryGroup.containers.values()).forEach(function (containerGroup) {
2141
- const status = sessionStatusInfo(containerGroup.status);
2508
+ directoryGroup.containers.forEach(function (containerGroup) {
2509
+ const containerExpanded = isContainerExpanded(containerGroup);
2510
+ const containerHasActive = containerContainsActiveSession(containerGroup);
2142
2511
  const containerCard = document.createElement('section');
2143
2512
  containerCard.className = 'container-card';
2513
+ containerCard.classList.toggle('has-active', containerHasActive);
2144
2514
 
2145
2515
  const containerHead = document.createElement('div');
2146
2516
  containerHead.className = 'container-card-head';
2147
2517
 
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
- `;
2518
+ const containerToggle = createContainerToggle(containerGroup, containerExpanded);
2519
+ containerToggle.addEventListener('click', function () {
2520
+ setSidebarContainerExpanded(containerGroup.containerName, !containerExpanded);
2521
+ renderSessions();
2522
+ });
2158
2523
 
2159
2524
  const addAgentBtn = document.createElement('button');
2160
2525
  addAgentBtn.type = 'button';
@@ -2164,12 +2529,13 @@
2164
2529
  createAgentSession(containerGroup.containerName);
2165
2530
  });
2166
2531
 
2167
- containerHead.appendChild(containerInfo);
2168
- containerHead.appendChild(addAgentBtn);
2532
+ containerHead.appendChild(containerToggle);
2169
2533
  containerCard.appendChild(containerHead);
2170
2534
 
2171
2535
  const agentList = document.createElement('div');
2172
- agentList.className = 'agent-list';
2536
+ agentList.className = 'agent-list container-card-body';
2537
+ agentList.hidden = !containerExpanded;
2538
+ agentList.appendChild(addAgentBtn);
2173
2539
  containerGroup.sessions.forEach(function (session) {
2174
2540
  const row = createAgentRow(session, itemIndex);
2175
2541
  state.sessionNodeMap.set(session.name, row);
@@ -2183,6 +2549,8 @@
2183
2549
  group.appendChild(containerStack);
2184
2550
  sessionList.appendChild(group);
2185
2551
  });
2552
+
2553
+ scrollActiveSessionIntoView();
2186
2554
  }
2187
2555
 
2188
2556
  function renderMessagesLoading() {
@@ -2216,6 +2584,11 @@
2216
2584
  }
2217
2585
 
2218
2586
  function getMessageRenderKey(msg, index) {
2587
+ if (msg && msg.id && msg.streamingReply) {
2588
+ const content = msg.content ? String(msg.content) : '';
2589
+ const timestamp = msg.timestamp ? String(msg.timestamp) : '';
2590
+ return `id:${msg.id}|streaming|${timestamp}|${content}`;
2591
+ }
2219
2592
  if (msg && msg.id && msg.streamTrace) {
2220
2593
  const content = msg.content ? String(msg.content) : '';
2221
2594
  const timestamp = msg.timestamp ? String(msg.timestamp) : '';
@@ -2267,14 +2640,39 @@
2267
2640
  const bubble = document.createElement('div');
2268
2641
  bubble.className = 'bubble';
2269
2642
 
2643
+ const isStreamingReply = Boolean(msg && msg.streamingReply);
2270
2644
  const shouldRenderStructuredTrace = Boolean(
2271
2645
  msg
2272
2646
  && msg.streamTrace
2273
2647
  && Array.isArray(msg.traceEvents)
2274
2648
  && msg.traceEvents.length
2275
2649
  );
2276
- const shouldRenderMarkdown = Boolean(!msg.streamTrace && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
2277
- if (shouldRenderStructuredTrace) {
2650
+ const shouldRenderMarkdown = Boolean(!msg.streamTrace && !msg.streamingReply && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
2651
+ if (isStreamingReply) {
2652
+ bubble.classList.add('streaming-reply');
2653
+ var replyContent = String(msg.content || '');
2654
+ if (replyContent && markdownRenderer && typeof markdownRenderer.render === 'function') {
2655
+ var mdNode = document.createElement('div');
2656
+ mdNode.className = 'md-content';
2657
+ var rendered = '';
2658
+ try {
2659
+ rendered = String(markdownRenderer.render(replyContent) || '');
2660
+ } catch (e) {
2661
+ rendered = '';
2662
+ }
2663
+ if (rendered) {
2664
+ mdNode.innerHTML = rendered;
2665
+ bubble.appendChild(mdNode);
2666
+ } else {
2667
+ appendPlainMessageContent(bubble, replyContent);
2668
+ }
2669
+ } else {
2670
+ appendPlainMessageContent(bubble, replyContent);
2671
+ }
2672
+ var cursor = document.createElement('span');
2673
+ cursor.className = 'streaming-cursor';
2674
+ bubble.appendChild(cursor);
2675
+ } else if (shouldRenderStructuredTrace) {
2278
2676
  appendStructuredTraceContent(bubble, msg);
2279
2677
  } else if (shouldRenderMarkdown) {
2280
2678
  const markdownNode = document.createElement('div');
@@ -2365,7 +2763,9 @@
2365
2763
  }
2366
2764
 
2367
2765
  function applySessionsSnapshot(rawSessions, preferredName) {
2766
+ const previousActive = state.active;
2368
2767
  state.sessions = Array.isArray(rawSessions) ? rawSessions : [];
2768
+ pruneSidebarTreeState();
2369
2769
 
2370
2770
  if (typeof preferredName === 'string' && preferredName.trim()) {
2371
2771
  state.active = preferredName.trim();
@@ -2379,6 +2779,10 @@
2379
2779
  if (!state.active && state.sessions.length) {
2380
2780
  state.active = state.sessions[0].name;
2381
2781
  }
2782
+ if (state.active && state.active !== previousActive) {
2783
+ ensureSessionPathExpanded(state.active);
2784
+ state.pendingActiveSessionScroll = true;
2785
+ }
2382
2786
  if (state.terminal.sessionName && state.terminal.sessionName !== state.active) {
2383
2787
  disconnectTerminal('会话已变化,终端已断开', true);
2384
2788
  }
@@ -2638,11 +3042,55 @@
2638
3042
  });
2639
3043
  }
2640
3044
 
3045
+ function appendStreamingReplyLocal(sessionName) {
3046
+ var replyMessage = {
3047
+ id: createLocalMessageId('local-streaming-reply'),
3048
+ role: 'assistant',
3049
+ content: '',
3050
+ timestamp: new Date().toISOString(),
3051
+ mode: 'agent',
3052
+ streamingReply: true
3053
+ };
3054
+ if (state.active === sessionName) {
3055
+ state.messages.push(replyMessage);
3056
+ }
3057
+ return replyMessage.id;
3058
+ }
3059
+
3060
+ function updateStreamingReplyLocal(sessionName, replyMessageId, content) {
3061
+ if (state.active !== sessionName) {
3062
+ return;
3063
+ }
3064
+ for (var i = state.messages.length - 1; i >= 0; i -= 1) {
3065
+ var message = state.messages[i];
3066
+ if (!message || String(message.id || '') !== String(replyMessageId || '')) {
3067
+ continue;
3068
+ }
3069
+ message.content = String(content || '');
3070
+ message.timestamp = new Date().toISOString();
3071
+ return;
3072
+ }
3073
+ }
3074
+
3075
+ function removeStreamingReplyLocal(sessionName, replyMessageId) {
3076
+ if (state.active !== sessionName) {
3077
+ return;
3078
+ }
3079
+ for (var i = state.messages.length - 1; i >= 0; i -= 1) {
3080
+ var message = state.messages[i];
3081
+ if (message && String(message.id || '') === String(replyMessageId || '') && message.streamingReply) {
3082
+ state.messages.splice(i, 1);
3083
+ return;
3084
+ }
3085
+ }
3086
+ }
3087
+
2641
3088
  async function sendAgentPromptStream(sessionName, inputText, pendingMessage) {
2642
3089
  const traceMessageId = appendAgentTraceMessageLocal(sessionName);
2643
3090
  const traceLines = ['[执行过程]', '等待 Agent 启动…'];
2644
3091
  let finalResult = null;
2645
3092
  let streamError = null;
3093
+ let streamingReplyId = null;
2646
3094
 
2647
3095
  state.agentRun.active = true;
2648
3096
  state.agentRun.sessionName = sessionName;
@@ -2692,6 +3140,20 @@
2692
3140
  pushTraceLine(event.text || '', event.traceEvent || null);
2693
3141
  return;
2694
3142
  }
3143
+ if (event.type === 'content_delta') {
3144
+ var content = String(event.content || '').trim();
3145
+ if (!content) {
3146
+ return;
3147
+ }
3148
+ if (!streamingReplyId) {
3149
+ streamingReplyId = appendStreamingReplyLocal(sessionName);
3150
+ }
3151
+ updateStreamingReplyLocal(sessionName, streamingReplyId, content);
3152
+ if (state.active === sessionName) {
3153
+ renderMessages(state.messages, { stickToBottom: true });
3154
+ }
3155
+ return;
3156
+ }
2695
3157
  if (event.type === 'result') {
2696
3158
  finalResult = event;
2697
3159
  if (event.interrupted) {
@@ -2711,6 +3173,9 @@
2711
3173
  streamError = e;
2712
3174
  }
2713
3175
  } finally {
3176
+ if (streamingReplyId) {
3177
+ removeStreamingReplyLocal(sessionName, streamingReplyId);
3178
+ }
2714
3179
  const pendingIndex = confirmPendingUserMessage(sessionName, pendingMessage.id);
2715
3180
  if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
2716
3181
  if (pendingIndex < messagesNode.children.length) {
@@ -3099,6 +3564,18 @@
3099
3564
  });
3100
3565
  }
3101
3566
 
3567
+ if (addAgentBtn) {
3568
+ addAgentBtn.addEventListener('click', function () {
3569
+ const activeSession = getActiveSession();
3570
+ const targetContainer = activeSession ? getSessionContainerName(activeSession) : '';
3571
+ if (!targetContainer) {
3572
+ return;
3573
+ }
3574
+ closeMobileActionsMenu();
3575
+ createAgentSession(targetContainer);
3576
+ });
3577
+ }
3578
+
3102
3579
  if (configModal) {
3103
3580
  configModal.addEventListener('click', function (event) {
3104
3581
  if (event.target === configModal && !state.configSaving) {
@@ -3223,6 +3700,7 @@
3223
3700
  disconnectTerminal('', true);
3224
3701
  });
3225
3702
 
3703
+ loadSidebarTreeState();
3226
3704
  renderSessions();
3227
3705
  renderMessages(state.messages);
3228
3706
  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.7",
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",