myagent-ai 1.7.2 → 1.7.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -65,4 +65,4 @@
65
65
  "departments/",
66
66
  "web/"
67
67
  ]
68
- }
68
+ }
package/web/api_server.py CHANGED
@@ -746,29 +746,41 @@ class ApiServer:
746
746
  return ""
747
747
 
748
748
  base_instruction = (
749
- "你当前处于【执行模式】(Execution Mode)。\n"
750
- "1. **复杂度分析**:首先评估任务复杂度。对于简单问候或常见问题,直接回答;对于多步骤任务,【必须】先制定计划。\n"
751
- "2. **强制规则 - 任务列表**:每次回复【必须】包含 ```tasklist``` 代码块,输出 JSON 格式的任务进度列表。先写纯文本分析,再写 tasklist,最后写 action(如有)。\n"
752
- "3. **强制规则 - 单步执行**:每次回复【只能执行一个操作】(一个工具调用或一个代码块)。执行完后等待结果反馈。\n"
749
+ "你当前处于【执行模式】(Execution Mode)。\n\n"
750
+ "## 核心规则\n"
751
+ "1. **任务列表(强制)**:每次回复【必须】包含一个 ```tasklist``` 代码块,内含 JSON 数组格式的任务进度。\n"
752
+ " - 格式:```tasklist\\n[{\"text\": \"步骤描述\", \"status\": \"pending\"}]\\n```\n"
753
+ " - status 可选值:pending(待执行)、running(进行中)、done(已完成)、blocked(受阻)\n"
754
+ " - 首次收到任务时,拆分为多个步骤,全部标记为 pending\n"
755
+ " - 每次执行完一个步骤后,更新对应步骤状态为 done,下一个为 running\n"
756
+ "2. **单步执行(强制)**:每次回复【只能执行一个操作】(一个工具调用、一个代码块或一个技能调用)。\n"
757
+ " - 执行完一个操作后停下来,等待结果反馈后再决定下一步\n"
758
+ " - 不要一次性执行多个操作\n"
759
+ "3. **回复格式**:先写纯文本分析/总结 → 再写 ```tasklist``` 更新进度 → 最后写 ```action``` 执行操作(如有)\n"
753
760
  )
754
761
 
755
762
  # 从内存读取当前任务列表
756
763
  tasks = self._task_list_store.get(agent_path, [])
757
764
  if not tasks:
758
- return base_instruction + "请分析需求后制定计划。"
765
+ return base_instruction + "\n## 当前状态\n暂无任务计划。请先分析用户需求,拆分为具体步骤,然后用 ```tasklist``` 输出计划。"
759
766
 
760
767
  pending = [f" - ⏳ {t['text']}" for t in tasks if t.get("status") in ("pending", "running", "blocked")]
761
768
  done = [f" - ✅ {t['text']}" for t in tasks if t.get("status") == "done"]
762
769
  running = [f" - 🔄 {t['text']}" for t in tasks if t.get("status") == "running"]
763
770
 
764
- context = base_instruction + "\n当前任务进度:\n"
771
+ context = base_instruction + "\n## 当前任务进度\n"
765
772
  if done:
766
773
  context += "已完成:\n" + "\n".join(done) + "\n"
767
774
  if running:
768
775
  context += "进行中:\n" + "\n".join(running) + "\n"
769
776
  if pending:
770
777
  context += "待执行:\n" + "\n".join(pending) + "\n"
771
- context += "\n请在回复中用 ```tasklist``` 更新任务进度(先写文本分析,再写 tasklist,最后写 action)。记住:【每次只能执行一个操作】。"
778
+ context += (
779
+ "\n## 下一步\n"
780
+ "1. 用纯文本简要分析当前进展\n"
781
+ "2. 用 ```tasklist``` 更新任务进度(标记已完成的步骤为 done,标记当前步骤为 running)\n"
782
+ "3. 用 ```action``` 执行下一个待执行步骤(每次只执行一个操作)\n"
783
+ )
772
784
  return context
773
785
 
774
786
  async def handle_chat_page(self, request):
@@ -2613,7 +2625,9 @@ class ApiServer:
2613
2625
  iteration = 0
2614
2626
  # 追踪连续无 action 迭代次数,防止无限重新提示
2615
2627
  _consecutive_no_action = 0
2616
- _MAX_NO_ACTION_RETRIES = 3
2628
+ _MAX_NO_ACTION_RETRIES = 5 # 提高重试次数,给 LLM 更多机会完成剩余任务
2629
+ # ── 追踪所有流式推送的纯文本(用于刷新后恢复) ──
2630
+ _all_streamed_text_parts = [] # 每轮迭代推送的纯文本片段
2617
2631
 
2618
2632
  while iteration < max_iter:
2619
2633
  iteration += 1
@@ -2682,6 +2696,7 @@ class ApiServer:
2682
2696
  text_before = remaining[:marker_pos]
2683
2697
  if text_before.strip():
2684
2698
  await _write_sse({"type": "text_delta", "content": text_before})
2699
+ _all_streamed_text_parts.append(text_before)
2685
2700
  # 跳过整个开始标记(```action 或 ```tasklist),不要只跳到 ```
2686
2701
  st["processed_pos"] += marker_pos + len(f"```{block_type}")
2687
2702
  if block_type == "tasklist":
@@ -2718,7 +2733,9 @@ class ApiServer:
2718
2733
  # 没有找到标记,流式推送(保留末尾可能的部分标记)
2719
2734
  safe_end = len(remaining) - _MAX_HOLD
2720
2735
  if safe_end > 0:
2721
- await _write_sse({"type": "text_delta", "content": remaining[:safe_end]})
2736
+ chunk = remaining[:safe_end]
2737
+ await _write_sse({"type": "text_delta", "content": chunk})
2738
+ _all_streamed_text_parts.append(chunk)
2722
2739
  st["processed_pos"] += safe_end
2723
2740
  remaining = full_text_so_far[st["processed_pos"]:]
2724
2741
  else:
@@ -2790,6 +2807,7 @@ class ApiServer:
2790
2807
  await _stream_text_chunked(remaining, _write_sse, chunk_size=3, delay=0.01)
2791
2808
  else:
2792
2809
  await _write_sse({"type": "text_delta", "content": remaining})
2810
+ _all_streamed_text_parts.append(remaining)
2793
2811
  st["processed_pos"] = len(full_text)
2794
2812
 
2795
2813
  # Call LLM with streaming — tokens are filtered through _text_delta_callback
@@ -3054,10 +3072,19 @@ class ApiServer:
3054
3072
  break
3055
3073
 
3056
3074
  # Save assistant response to memory
3057
- if agent.memory and final_response:
3058
- agent.memory.add_short_term(session_id=session_id, role="assistant", content=final_response)
3059
-
3060
- return final_response
3075
+ # ── 优先使用流式累积文本(包含所有迭代的纯文本),回退到 final_response ──
3076
+ saved_response = final_response
3077
+ if not saved_response and _all_streamed_text_parts:
3078
+ saved_response = "\n\n".join(p for p in _all_streamed_text_parts if p.strip())
3079
+ if not saved_response and content:
3080
+ saved_response = content # 兜底:使用最后一轮的完整输出
3081
+ if agent.memory and saved_response:
3082
+ agent.memory.add_short_term(session_id=session_id, role="assistant", content=saved_response)
3083
+ elif agent.memory:
3084
+ # 即使为空也保存一条,防止刷新后消息丢失
3085
+ agent.memory.add_short_term(session_id=session_id, role="assistant", content="(执行完成,无文本回复)")
3086
+
3087
+ return saved_response or final_response or content or ""
3061
3088
 
3062
3089
  async def _execute_actions_streaming(
3063
3090
  self, agent, action_data: dict, context, write_sse
@@ -588,6 +588,13 @@ input,textarea,select{font:inherit}
588
588
  }
589
589
  .rp-section-toggle:hover{background:var(--bg4);color:var(--text)}
590
590
  .rp-section-toggle.expanded{transform:rotate(90deg)}
591
+ .rp-section-action-btn{
592
+ width:20px;height:20px;border-radius:3px;border:none;
593
+ background:transparent;color:var(--text3);
594
+ display:inline-grid;place-items:center;flex-shrink:0;
595
+ cursor:pointer;transition:var(--transition);
596
+ }
597
+ .rp-section-action-btn:hover{background:var(--bg4);color:var(--accent)}
591
598
 
592
599
  /* Master Agent Card (全权Agent) */
593
600
  .rp-master-card{
@@ -1204,8 +1211,9 @@ input,textarea,select{font:inherit}
1204
1211
  opacity:0;position:absolute;right:8px;top:50%;transform:translateY(-50%);
1205
1212
  width:24px;height:24px;border-radius:4px;display:grid;place-items:center;
1206
1213
  background:var(--bg2);transition:var(--transition);font-size:12px;color:var(--text3);
1214
+ pointer-events:none;
1207
1215
  }
1208
- .group-item:hover .group-delete{opacity:1}
1216
+ .group-item:hover .group-delete{opacity:1;pointer-events:auto}
1209
1217
  .group-delete:hover{background:var(--danger);color:#fff}
1210
1218
 
1211
1219
  /* Group Chat Messages */
@@ -1569,6 +1577,20 @@ input,textarea,select{font:inherit}
1569
1577
  .exec-event-result-btn:hover{background:var(--accent-light);color:var(--accent-dark)}
1570
1578
  .exec-event-result-btn svg{width:12px;height:12px}
1571
1579
 
1580
+ /* ── Inline Exec Events (Timeline Interleaved) ── */
1581
+ .msg-timeline{display:flex;flex-direction:column;gap:6px}
1582
+ .inline-exec-event{margin:2px 0;padding:8px 12px;background:var(--bg2);border-left:3px solid var(--border);border-radius:6px;font-size:13px;animation:execEventSlide .3s ease-out}
1583
+ .inline-exec-header{display:flex;align-items:center;gap:6px;margin-bottom:4px}
1584
+ .inline-exec-icon{font-size:14px}
1585
+ .inline-exec-title{font-weight:500;color:var(--text);font-size:12px}
1586
+ .inline-exec-meta{color:var(--text3);font-size:11px;margin-left:auto}
1587
+ .inline-exec-code{background:var(--bg);padding:6px 8px;border-radius:4px;font-family:'SF Mono','Fira Code','Cascadia Code',monospace;font-size:12px;color:var(--text2);margin:4px 0;max-height:100px;overflow:hidden;cursor:pointer;transition:var(--transition);white-space:pre-wrap;word-break:break-all}
1588
+ .inline-exec-code:hover{background:var(--bg3)}
1589
+ .inline-exec-code.expanded{max-height:none}
1590
+ .inline-exec-summary{color:var(--text2);font-size:12px;margin:4px 0}
1591
+ .inline-exec-result-btn{background:none;border:1px solid var(--border);color:var(--text2);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;margin-top:4px;transition:var(--transition)}
1592
+ .inline-exec-result-btn:hover{background:var(--bg2);border-color:var(--accent);color:var(--accent)}
1593
+
1572
1594
  /* ── Execution Result Modal ── */
1573
1595
  .exec-result-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:center;justify-content:center;animation:fadeIn .15s ease}
1574
1596
  .exec-result-modal{background:var(--bg);border:1px solid var(--border);border-radius:12px;width:min(680px,90vw);max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.25);animation:slideUp .2s ease}
@@ -1605,6 +1627,9 @@ input,textarea,select{font:inherit}
1605
1627
  [data-theme="dark"] .exec-result-modal{background:var(--bg2);border-color:var(--border)}
1606
1628
  [data-theme="dark"] .exec-result-modal-body pre{background:#0a0c10;color:#cdd6f4}
1607
1629
  [data-theme="dark"] .exec-result-info-item{background:var(--bg3)}
1630
+ [data-theme="dark"] .inline-exec-event{background:var(--bg3);border-left-color:var(--border)}
1631
+ [data-theme="dark"] .inline-exec-code{background:var(--bg)}
1632
+ [data-theme="dark"] .inline-exec-result-btn:hover{background:var(--bg4)}
1608
1633
 
1609
1634
  .thought-block {
1610
1635
  background: rgba(0, 0, 0, 0.03);
@@ -1626,3 +1651,143 @@ input,textarea,select{font:inherit}
1626
1651
  font-family: inherit;
1627
1652
  white-space: pre-wrap;
1628
1653
  }
1654
+
1655
+ /* ══════════════════════════════════════════════════════
1656
+ ── Mobile Responsive (≤768px) ──
1657
+ ══════════════════════════════════════════════════════ */
1658
+
1659
+ /* ── Mobile Overlay Backdrop (placed outside media query so JS can always use it) ── */
1660
+ .mobile-overlay{
1661
+ position:fixed;inset:0;
1662
+ background:rgba(0,0,0,.4);
1663
+ z-index:45;
1664
+ opacity:0;visibility:hidden;
1665
+ transition:opacity .3s ease,visibility .3s ease;
1666
+ pointer-events:none;
1667
+ }
1668
+ .mobile-overlay.active{
1669
+ opacity:1;visibility:visible;
1670
+ pointer-events:auto;
1671
+ }
1672
+
1673
+ @media(max-width:768px){
1674
+
1675
+ /* ── Sidebar: Fixed overlay sliding from left ── */
1676
+ .sidebar{
1677
+ position:fixed;
1678
+ left:0;top:0;
1679
+ width:85vw;max-width:320px;
1680
+ height:100vh;
1681
+ z-index:50;
1682
+ transform:translateX(-100%);
1683
+ transition:transform .3s cubic-bezier(.4,0,.2,1);
1684
+ box-shadow:none;
1685
+ }
1686
+ .sidebar.mobile-open{
1687
+ transform:translateX(0);
1688
+ box-shadow:4px 0 24px rgba(0,0,0,.15);
1689
+ }
1690
+
1691
+ /* ── Agent Panel: Fixed overlay sliding from right ── */
1692
+ .agent-panel{
1693
+ position:fixed;
1694
+ right:0;top:0;
1695
+ width:85vw;max-width:340px;
1696
+ height:100vh;
1697
+ z-index:50;
1698
+ transform:translateX(100%);
1699
+ transition:transform .3s cubic-bezier(.4,0,.2,1);
1700
+ box-shadow:none;
1701
+ }
1702
+ .agent-panel.mobile-open{
1703
+ transform:translateX(0);
1704
+ box-shadow:-4px 0 24px rgba(0,0,0,.15);
1705
+ }
1706
+
1707
+ /* ── Hide desktop toggle buttons on mobile ── */
1708
+ .sidebar-toggle,
1709
+ .agent-toggle{
1710
+ display:none!important;
1711
+ }
1712
+
1713
+ /* ── Header ── */
1714
+ .main-header{
1715
+ height:50px;min-height:50px;
1716
+ padding:0 12px;
1717
+ }
1718
+ .main-title{
1719
+ font-size:14px;
1720
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
1721
+ min-width:0;
1722
+ }
1723
+
1724
+ /* ── Messages ── */
1725
+ .messages-container{
1726
+ padding:12px;
1727
+ }
1728
+ .messages-inner{
1729
+ max-width:100%;
1730
+ }
1731
+ .message-bubble{
1732
+ max-width:88%;
1733
+ }
1734
+ .message-bubble pre{
1735
+ font-size:12px;
1736
+ }
1737
+
1738
+ /* ── Input Area ── */
1739
+ .input-area{
1740
+ padding:10px 12px;
1741
+ }
1742
+ .input-wrapper{
1743
+ max-width:100%;
1744
+ }
1745
+ .input-box textarea{
1746
+ min-height:60px;
1747
+ }
1748
+
1749
+ /* ── Modals ── */
1750
+ .modal,
1751
+ .agent-modal,
1752
+ .platform-modal{
1753
+ width:95%;max-height:90vh;padding:20px;
1754
+ }
1755
+
1756
+ /* ── Toast ── */
1757
+ .toast-container{
1758
+ top:12px;right:12px;left:12px;
1759
+ }
1760
+ .toast{
1761
+ max-width:100%;
1762
+ }
1763
+
1764
+ /* ── Welcome Card Capabilities ── */
1765
+ .capabilities{
1766
+ grid-template-columns:1fr 1fr;
1767
+ }
1768
+
1769
+ /* ── Execution Timer ── */
1770
+ .exec-timer{
1771
+ max-width:100%;
1772
+ }
1773
+
1774
+ /* ── Empty State ── */
1775
+ .empty-icon{
1776
+ font-size:40px;
1777
+ }
1778
+ .empty-title{
1779
+ font-size:16px;
1780
+ }
1781
+ }
1782
+
1783
+ /* ══════════════════════════════════════════════════════
1784
+ ── Very Small Phones (≤480px) ──
1785
+ ══════════════════════════════════════════════════════ */
1786
+ @media(max-width:480px){
1787
+ .message-bubble{
1788
+ max-width:92%;
1789
+ }
1790
+ .main-header{
1791
+ padding:0 8px;
1792
+ }
1793
+ }
@@ -18,6 +18,9 @@
18
18
  <div id="rightAgentsContainer"></div>
19
19
  </div>
20
20
 
21
+ <!-- Mobile Overlay -->
22
+ <div class="mobile-overlay" id="chatMobileOverlay"></div>
23
+
21
24
  <!-- Toast Container -->
22
25
  <div class="toast-container" id="toastContainer"></div>
23
26
 
@@ -28,18 +28,119 @@ initTheme();
28
28
  document.getElementById('themeToggle')?.addEventListener('click', toggleTheme);
29
29
 
30
30
  // ── Sidebar Collapse ──
31
+ function isMobile() { return window.innerWidth <= 768; }
32
+
31
33
  function toggleSidebar() {
32
34
  const sidebar = document.getElementById('sidebar');
33
35
  const toggleBtn = document.getElementById('sidebarToggle');
34
36
  if (!sidebar) return;
35
- sidebar.classList.toggle('collapsed');
36
- const isCollapsed = sidebar.classList.contains('collapsed');
37
- if (toggleBtn) toggleBtn.textContent = isCollapsed ? '' : '◀';
38
- localStorage.setItem('myagent-sidebar-collapsed', isCollapsed);
37
+ if (isMobile()) {
38
+ // Mobile: toggle as overlay
39
+ const overlay = document.getElementById('chatMobileOverlay');
40
+ const isOpen = sidebar.classList.contains('mobile-open');
41
+ if (isOpen) {
42
+ closeMobileSidebar();
43
+ } else {
44
+ sidebar.classList.add('mobile-open');
45
+ if (overlay) overlay.classList.add('active');
46
+ // Close agent panel if open
47
+ closeMobileAgentPanel();
48
+ }
49
+ } else {
50
+ // Desktop: collapse/expand
51
+ sidebar.classList.toggle('collapsed');
52
+ const isCollapsed = sidebar.classList.contains('collapsed');
53
+ if (toggleBtn) toggleBtn.textContent = isCollapsed ? '▶' : '◀';
54
+ localStorage.setItem('myagent-sidebar-collapsed', isCollapsed);
55
+ }
56
+ }
57
+
58
+ function closeMobileSidebar() {
59
+ const sidebar = document.getElementById('sidebar');
60
+ const overlay = document.getElementById('chatMobileOverlay');
61
+ if (sidebar) sidebar.classList.remove('mobile-open');
62
+ if (overlay) overlay.classList.remove('active');
63
+ }
64
+
65
+ function toggleMobileAgentPanel() {
66
+ const panel = document.getElementById('agentPanel');
67
+ const overlay = document.getElementById('chatMobileOverlay');
68
+ if (!panel) return;
69
+ const isOpen = panel.classList.contains('mobile-open');
70
+ if (isOpen) {
71
+ closeMobileAgentPanel();
72
+ } else {
73
+ panel.classList.add('mobile-open');
74
+ if (overlay) overlay.classList.add('active');
75
+ // Close sidebar if open
76
+ closeMobileSidebar();
77
+ }
78
+ }
79
+
80
+ function closeMobileAgentPanel() {
81
+ const panel = document.getElementById('agentPanel');
82
+ const overlay = document.getElementById('chatMobileOverlay');
83
+ if (panel) panel.classList.remove('mobile-open');
84
+ if (overlay) overlay.classList.remove('active');
85
+ }
86
+
87
+ // Mobile overlay click closes both panels
88
+ document.addEventListener('click', function(e) {
89
+ if (e.target && e.target.id === 'chatMobileOverlay') {
90
+ closeMobileSidebar();
91
+ closeMobileAgentPanel();
92
+ }
93
+ });
94
+
95
+ // Show/hide mobile agents button based on screen size
96
+ function checkChatMobile() {
97
+ const btn = document.getElementById('mobileAgentsBtn');
98
+ if (btn) btn.style.display = isMobile() ? 'grid' : 'none';
99
+ }
100
+ window.addEventListener('resize', checkChatMobile);
101
+ // Run after DOM ready
102
+ if (document.readyState === 'loading') {
103
+ document.addEventListener('DOMContentLoaded', checkChatMobile);
104
+ } else {
105
+ checkChatMobile();
39
106
  }
40
- // Restore sidebar state
107
+
108
+ // Override toggleAgentPanel for mobile (deferred since function is defined later)
109
+ var _origToggleAgentPanel = null;
110
+ (function() {
111
+ var origDef = toggleAgentPanel;
112
+ if (typeof origDef === 'function') {
113
+ _origToggleAgentPanel = origDef;
114
+ toggleAgentPanel = function() {
115
+ if (isMobile()) {
116
+ toggleMobileAgentPanel();
117
+ } else {
118
+ _origToggleAgentPanel();
119
+ }
120
+ };
121
+ }
122
+ })();
123
+ // Also override after definition via setTimeout (fallback)
124
+ setTimeout(function() {
125
+ if (!_origToggleAgentPanel && typeof toggleAgentPanel === 'function') {
126
+ // Check if it's already the mobile version
127
+ var testFn = toggleAgentPanel.toString();
128
+ if (testFn.indexOf('toggleMobileAgentPanel') === -1) {
129
+ _origToggleAgentPanel = toggleAgentPanel;
130
+ toggleAgentPanel = function() {
131
+ if (isMobile()) {
132
+ toggleMobileAgentPanel();
133
+ } else {
134
+ _origToggleAgentPanel();
135
+ }
136
+ };
137
+ }
138
+ }
139
+ }, 0);
140
+
141
+ // Restore sidebar state (desktop only)
41
142
  (function() {
42
- if (localStorage.getItem('myagent-sidebar-collapsed') === 'true') {
143
+ if (localStorage.getItem('myagent-sidebar-collapsed') === 'true' && !isMobile()) {
43
144
  const sidebar = document.getElementById('sidebar');
44
145
  const toggleBtn = document.getElementById('sidebarToggle');
45
146
  if (sidebar) sidebar.classList.add('collapsed');
@@ -966,6 +1067,7 @@ async function selectAgent(agentPath) {
966
1067
  // 如果 loadSessions 已经 auto-selected 了 session,UI 已由 selectSession 设置好,不再覆盖
967
1068
 
968
1069
  document.getElementById('userInput').focus();
1070
+ if (isMobile()) closeMobileAgentPanel();
969
1071
  // Reload task plan if in exec mode
970
1072
  if (state.chatMode === 'exec') loadTaskPlan();
971
1073
  // Reset escalation and update exec mode UI
@@ -1434,9 +1536,13 @@ function newChat() {
1434
1536
  async function selectSession(id) {
1435
1537
  if (id === '__new__') {
1436
1538
  newChat();
1539
+ if (isMobile()) closeMobileSidebar();
1540
+ return;
1541
+ }
1542
+ if (state.activeSessionId === id && state.messages.length > 0) {
1543
+ if (isMobile()) closeMobileSidebar();
1437
1544
  return;
1438
1545
  }
1439
- if (state.activeSessionId === id && state.messages.length > 0) return;
1440
1546
 
1441
1547
  // 重置生成状态,防止残留的 isGenerating=true 导致输入框锁定
1442
1548
  state.isGenerating = false;
@@ -1470,6 +1576,7 @@ async function selectSession(id) {
1470
1576
  }
1471
1577
  renderMessages();
1472
1578
  document.getElementById('userInput').focus();
1579
+ if (isMobile()) closeMobileSidebar();
1473
1580
  }
1474
1581
 
1475
1582
  async function deleteSession(id) {
@@ -1680,9 +1787,12 @@ function renderMessages() {
1680
1787
  </div>` : '';
1681
1788
  const ttsIndicator = ttsManager && ttsManager.isPlaying && ttsManager.currentMsgIndex === i ?
1682
1789
  ' <span class="tts-playing-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg></span>' : '';
1683
- const execEventsHtml = (!isUser && msg.exec_events && msg.exec_events.length > 0)
1684
- ? renderExecEvents(msg.exec_events, i) : '';
1685
- const streamingIndicator = msg.streaming && !msg.content && !msg.thought ? `
1790
+
1791
+ // ── Determine rendering mode and streaming indicator ──
1792
+ const hasParts = Array.isArray(msg.parts) && msg.parts.length > 0;
1793
+ const hasStreamingText = msg._streamingText && msg._streamingText.trim();
1794
+ const anyContent = msg.content || msg._streamingText || hasParts;
1795
+ const streamingIndicator = msg.streaming && !anyContent && !msg.thought ? `
1686
1796
  <div class="streaming-indicator">
1687
1797
  <div class="spinner"></div>
1688
1798
  <div class="streaming-dots">
@@ -1690,13 +1800,42 @@ function renderMessages() {
1690
1800
  </div>
1691
1801
  <span style="font-weight:500">Agent 正在思考...</span>
1692
1802
  </div>` : '';
1803
+
1804
+ // ── Timeline rendering for interleaved text + exec events ──
1805
+ let timelineHtml = '';
1806
+ if (hasParts || hasStreamingText) {
1807
+ let partsHtml = '';
1808
+ for (const part of (msg.parts || [])) {
1809
+ if (part.type === 'text' && part.content.trim()) {
1810
+ partsHtml += '<div class="message-bubble">' + renderMarkdown(part.content) + '</div>';
1811
+ } else if (part.type === 'exec') {
1812
+ partsHtml += renderInlineExecEvent(part.data, i);
1813
+ }
1814
+ }
1815
+ if (hasStreamingText) {
1816
+ partsHtml += '<div class="message-bubble">' + renderMarkdown(msg._streamingText) + '</div>';
1817
+ }
1818
+ if (partsHtml) {
1819
+ timelineHtml = '<div class="msg-timeline">' + partsHtml + '</div>';
1820
+ }
1821
+ }
1822
+
1823
+ // Backward compat: single bubble for messages without parts
1824
+ const singleBubbleHtml = (!hasParts && !hasStreamingText)
1825
+ ? ((content || streamingIndicator) ? `<div class="message-bubble">${content}${ttsIndicator}</div>` : '')
1826
+ : '';
1827
+
1828
+ // Exec events panel: only for backward compat (messages without parts loaded from DB)
1829
+ const execEventsHtml = (!isUser && !hasParts && msg.exec_events && msg.exec_events.length > 0)
1830
+ ? renderExecEvents(msg.exec_events, i) : '';
1693
1831
  html += `
1694
1832
  <div class="message-row ${msg.role}">
1695
1833
  <div class="message-avatar">${avatar}</div>
1696
1834
  <div style="flex:1;min-width:0">
1697
1835
  ${reasoningHtml}
1698
1836
  ${thoughtHtml}
1699
- ${content || streamingIndicator ? `<div class="message-bubble">${content}${ttsIndicator}</div>` : ''}
1837
+ ${timelineHtml}
1838
+ ${singleBubbleHtml}
1700
1839
  ${streamingIndicator}
1701
1840
  ${execEventsHtml}
1702
1841
  ${msg.time ? `<div class="message-time">${formatTime(msg.time)}</div>` : ''}
@@ -1,4 +1,3 @@
1
- <string>:27: SyntaxWarning: invalid escape sequence '\s'
2
1
  // ══════════════════════════════════════════════════════
3
2
  // ── Flow Engine: 文本处理流引擎 ──
4
3
  // ── 负责消息发送、SSE 流式处理、大文本检测与分段、
@@ -351,22 +350,87 @@ function updateStreamingMessage(msgIdx) {
351
350
  }
352
351
  }
353
352
 
354
- // Update content bubble
355
- let bubble = contentArea.querySelector('.message-bubble');
356
- const content = renderMarkdown(msg.content);
357
- if (content && !bubble) {
358
- // Create bubble
359
- bubble = document.createElement('div');
360
- bubble.className = 'message-bubble';
361
- contentArea.appendChild(bubble);
362
- }
363
- if (bubble && content) {
364
- bubble.innerHTML = content;
353
+ // Update content - timeline (interleaved text + exec events) or single bubble (backward compat)
354
+ const hasParts = Array.isArray(msg.parts);
355
+ if (hasParts) {
356
+ // ── Timeline rendering for interleaved text + exec events ──
357
+ let timeline = contentArea.querySelector('.msg-timeline');
358
+ if (!timeline) {
359
+ // Remove old single bubble if exists
360
+ const oldBubble = contentArea.querySelector(':scope > .message-bubble');
361
+ if (oldBubble) oldBubble.remove();
362
+ // Create timeline container
363
+ timeline = document.createElement('div');
364
+ timeline.className = 'msg-timeline';
365
+ // Insert after thought blocks or at beginning
366
+ const allThoughts = contentArea.querySelectorAll(':scope > .thought-block');
367
+ if (allThoughts.length > 0) {
368
+ allThoughts[allThoughts.length - 1].insertAdjacentElement('afterend', timeline);
369
+ } else {
370
+ contentArea.appendChild(timeline);
371
+ }
372
+ }
373
+
374
+ // Cache completed parts rendering (only re-render when parts count changes)
375
+ const partsCount = msg.parts.length;
376
+ if (!msg._renderedPartsHtml || msg._lastPartsCount !== partsCount) {
377
+ let html = '';
378
+ for (const part of msg.parts) {
379
+ if (part.type === 'text' && part.content.trim()) {
380
+ html += '<div class="message-bubble">' + renderMarkdown(part.content) + '</div>';
381
+ } else if (part.type === 'exec') {
382
+ html += renderInlineExecEvent(part.data, msgIdx);
383
+ }
384
+ }
385
+ msg._renderedPartsHtml = html;
386
+ msg._lastPartsCount = partsCount;
387
+ }
388
+
389
+ // Build streaming bubble for current in-progress text
390
+ const streamingText = msg._streamingText || '';
391
+ const streamingBubbleHtml = streamingText.trim()
392
+ ? '<div class="message-bubble">' + renderMarkdown(streamingText) + '</div>'
393
+ : '';
394
+
395
+ timeline.innerHTML = msg._renderedPartsHtml + streamingBubbleHtml;
396
+
397
+ // Remove exec events panel if present (events are now inline in timeline)
398
+ const execPanel = contentArea.querySelector('.exec-events-panel');
399
+ if (execPanel) execPanel.remove();
400
+ } else {
401
+ // ── Backward compat: single content bubble + exec events panel ──
402
+ let bubble = contentArea.querySelector('.message-bubble');
403
+ const content = renderMarkdown(msg.content);
404
+ if (content && !bubble) {
405
+ bubble = document.createElement('div');
406
+ bubble.className = 'message-bubble';
407
+ contentArea.appendChild(bubble);
408
+ }
409
+ if (bubble && content) {
410
+ bubble.innerHTML = content;
411
+ }
412
+
413
+ // Exec events panel (only for backward compat messages without parts)
414
+ if (msg.exec_events && msg.exec_events.length > 0) {
415
+ let execPanel = contentArea.querySelector('.exec-events-panel');
416
+ const newExecHtml = renderExecEvents(msg.exec_events, msgIdx);
417
+ if (execPanel) {
418
+ execPanel.outerHTML = newExecHtml;
419
+ } else {
420
+ const timeEl = contentArea.querySelector('.message-time');
421
+ if (timeEl) {
422
+ timeEl.insertAdjacentHTML('beforebegin', newExecHtml);
423
+ } else {
424
+ contentArea.insertAdjacentHTML('beforeend', newExecHtml);
425
+ }
426
+ }
427
+ }
365
428
  }
366
429
 
367
430
  // Update streaming indicator
368
431
  let indicator = contentArea.querySelector('.streaming-indicator');
369
- const streamingIndicator = msg.streaming && !msg.content && !msg.thought ? `
432
+ const anyContent = msg.content || msg._streamingText || (msg.parts && msg.parts.length > 0);
433
+ const streamingIndicator = msg.streaming && !anyContent && !msg.thought ? `
370
434
  <div class="streaming-indicator">
371
435
  <div class="streaming-dots">
372
436
  <span class="dot"></span><span class="dot"></span><span class="dot"></span>
@@ -381,23 +445,6 @@ function updateStreamingMessage(msgIdx) {
381
445
  indicator.remove();
382
446
  }
383
447
 
384
- // Update exec events panel
385
- if (msg.exec_events && msg.exec_events.length > 0) {
386
- let execPanel = contentArea.querySelector('.exec-events-panel');
387
- const newExecHtml = renderExecEvents(msg.exec_events, msgIdx);
388
- if (execPanel) {
389
- execPanel.outerHTML = newExecHtml;
390
- } else {
391
- // Insert before time element or at end
392
- const timeEl = contentArea.querySelector('.message-time');
393
- if (timeEl) {
394
- timeEl.insertAdjacentHTML('beforebegin', newExecHtml);
395
- } else {
396
- contentArea.insertAdjacentHTML('beforeend', newExecHtml);
397
- }
398
- }
399
- }
400
-
401
448
  // Auto-scroll
402
449
  scrollToBottom();
403
450
  }
@@ -508,6 +555,52 @@ function toggleExecEventsPanel(header) {
508
555
  body.classList.toggle('expanded');
509
556
  }
510
557
 
558
+ // ══════════════════════════════════════════════════════
559
+ // ── Inline Exec Event (Timeline Card) ──
560
+ // ══════════════════════════════════════════════════════
561
+
562
+ function renderInlineExecEvent(data, msgIdx) {
563
+ const iconEmoji = getEventIconEmoji(data);
564
+ const title = data.title || (data.tool_name || data.skill_name || '执行事件');
565
+
566
+ // Build meta text
567
+ let metaParts = [];
568
+ if (data.execution_time !== undefined) metaParts.push('耗时 ' + data.execution_time + 's');
569
+ if (data.language) metaParts.push(escapeHtml(data.language));
570
+ if (data.tool_name || data.skill_name) metaParts.push(escapeHtml(data.tool_name || data.skill_name));
571
+ if (data.timed_out) metaParts.push('超时');
572
+ if (data.exit_code !== undefined) metaParts.push('exit: ' + data.exit_code);
573
+ const metaText = metaParts.join(' · ');
574
+
575
+ // Build body content
576
+ let bodyHtml = '';
577
+ // Code preview for code_exec/code_result
578
+ if (data.code_preview && (data.type === 'code_exec' || data.type === 'code_result')) {
579
+ bodyHtml += '<div class="inline-exec-code" onclick="showExecResultModal(' + msgIdx + ', ' + data.id + ')" title="点击查看完整结果">' + escapeHtml(data.code_preview) + '</div>';
580
+ }
581
+ // Summary for tool_result/skill_result
582
+ if (data.summary && (data.type === 'tool_result' || data.type === 'skill_result')) {
583
+ bodyHtml += '<div class="inline-exec-summary">' + escapeHtml(data.summary) + '</div>';
584
+ }
585
+ // Result button for code_result
586
+ if (data.type === 'code_result' && (data.stdout || data.stderr || data.error)) {
587
+ bodyHtml += '<button class="inline-exec-result-btn" onclick="showExecResultModal(' + msgIdx + ', ' + data.id + ')">查看详情</button>';
588
+ }
589
+ // Result button for tool_result/skill_result
590
+ if ((data.type === 'tool_result' || data.type === 'skill_result') && data.result) {
591
+ bodyHtml += '<button class="inline-exec-result-btn" onclick="showToolResultModal(' + msgIdx + ', ' + data.id + ')">查看详情</button>';
592
+ }
593
+
594
+ return '<div class="inline-exec-event">' +
595
+ '<div class="inline-exec-header">' +
596
+ '<span class="inline-exec-icon">' + iconEmoji + '</span>' +
597
+ '<span class="inline-exec-title">' + escapeHtml(title) + '</span>' +
598
+ (metaText ? '<span class="inline-exec-meta">' + metaText + '</span>' : '') +
599
+ '</div>' +
600
+ bodyHtml +
601
+ '</div>';
602
+ }
603
+
511
604
  // ══════════════════════════════════════════════════════
512
605
  // ── Execution Result Modal (执行结果弹窗) ──
513
606
  // ══════════════════════════════════════════════════════
@@ -703,14 +796,22 @@ async function sendMessage() {
703
796
  const reader = resp.body.getReader();
704
797
  const decoder = new TextDecoder();
705
798
  let buffer = '';
706
- let fullResponse = '';
799
+ let msgParts = []; // Timeline: [{type:'text', content:'...'}, {type:'exec', data:{...}}]
800
+ let currentText = ''; // Accumulator for current streaming text segment
801
+ let allExecEvents = []; // All exec events (for summary panel at bottom)
707
802
  let msgIdx = state.messages.length;
708
803
  let sessionIdReceived = sessionId;
709
- let execEventsReceived = [];
710
804
  let fullThought = '';
711
-
805
+
806
+ function flushCurrentText() {
807
+ if (currentText.trim()) {
808
+ msgParts.push({type: 'text', content: currentText});
809
+ }
810
+ currentText = '';
811
+ }
812
+
712
813
  // Add placeholder for streaming response
713
- state.messages.push({ role: 'assistant', content: '', thought: '', time: new Date().toISOString(), streaming: true });
814
+ state.messages.push({ role: 'assistant', content: '', thought: '', parts: [], time: new Date().toISOString(), streaming: true });
714
815
  renderMessages();
715
816
 
716
817
  while (true) {
@@ -731,13 +832,22 @@ async function sendMessage() {
731
832
  // Sync the actual session ID (backend may prefix with agent_path)
732
833
  state.activeSessionId = evt.session_id;
733
834
  } else if (evt.type === 'text') {
734
- fullResponse = evt.content;
835
+ // Full text event (non-streaming replacement)
836
+ flushCurrentText();
837
+ msgParts.push({type: 'text', content: evt.content});
838
+ state.messages[msgIdx].parts = [...msgParts];
839
+ state.messages[msgIdx]._streamingText = '';
735
840
  state.messages[msgIdx].content = evt.content;
736
841
  renderMessages();
737
842
  } else if (evt.type === 'text_delta') {
738
843
  // Incremental streaming token
739
- fullResponse += evt.content;
740
- state.messages[msgIdx].content = fullResponse;
844
+ currentText += evt.content;
845
+ // Build backward-compat content from all parts + streaming text
846
+ const allText = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n')
847
+ + (currentText.trim() ? '\n\n' + currentText : '');
848
+ state.messages[msgIdx].parts = [...msgParts];
849
+ state.messages[msgIdx]._streamingText = currentText;
850
+ state.messages[msgIdx].content = allText;
741
851
  throttledStreamUpdate(msgIdx);
742
852
  // ── 分段流式 TTS:推送增量文本 ──
743
853
  if (ttsManager.enabled && !ttsManager._streamActive) {
@@ -759,28 +869,39 @@ async function sendMessage() {
759
869
  state.messages[msgIdx].thought = fullThought;
760
870
  throttledStreamUpdate(msgIdx);
761
871
  } else if (evt.type === 'queue_start') {
762
- // New message starting from queue
872
+ // Finalize previous message
873
+ flushCurrentText();
763
874
  if (state.messages[msgIdx]) {
764
875
  state.messages[msgIdx].streaming = false;
765
- if (execEventsReceived.length > 0) state.messages[msgIdx].exec_events = [...execEventsReceived];
876
+ state.messages[msgIdx].parts = [...msgParts];
877
+ state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '(无回复)';
878
+ state.messages[msgIdx]._streamingText = '';
879
+ if (allExecEvents.length > 0) state.messages[msgIdx].exec_events = [...allExecEvents];
766
880
  }
881
+ // Start new message
767
882
  state.messages.push({ role: 'user', content: evt.message, time: new Date().toISOString() });
768
883
  msgIdx = state.messages.length;
769
- fullResponse = '';
884
+ msgParts = [];
885
+ currentText = '';
886
+ allExecEvents = [];
770
887
  fullThought = '';
771
- execEventsReceived = [];
772
- state.messages.push({ role: 'assistant', content: '', thought: '', time: new Date().toISOString(), streaming: true });
888
+ state.messages.push({ role: 'assistant', content: '', thought: '', parts: [], time: new Date().toISOString(), streaming: true });
773
889
  renderMessages();
774
890
  } else if (evt.type === 'clear_text') {
775
891
  // Clear intermediate text from previous agent loop iterations
776
- fullResponse = '';
777
- state.messages[msgIdx].content = '';
892
+ flushCurrentText();
893
+ state.messages[msgIdx].parts = [...msgParts];
894
+ state.messages[msgIdx]._streamingText = '';
895
+ state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '';
778
896
  throttledStreamUpdate(msgIdx);
779
897
  } else if (evt.type === 'exec_event') {
780
898
  // Real-time execution event (tool call, code exec, skill result, etc.)
781
- execEventsReceived.push(evt.data);
782
- // 立即更新消息的 exec_events 并渲染
783
- state.messages[msgIdx].exec_events = [...execEventsReceived];
899
+ flushCurrentText();
900
+ msgParts.push({type: 'exec', data: evt.data});
901
+ allExecEvents.push(evt.data);
902
+ state.messages[msgIdx].parts = [...msgParts];
903
+ state.messages[msgIdx]._streamingText = '';
904
+ state.messages[msgIdx].exec_events = [...allExecEvents];
784
905
  throttledStreamUpdate(msgIdx);
785
906
  } else if (evt.type === 'task_list_update') {
786
907
  // 任务列表 JSON 直推更新(exec 模式)
@@ -803,11 +924,15 @@ async function sendMessage() {
803
924
  }
804
925
  }
805
926
  } else if (evt.type === 'done') {
927
+ flushCurrentText();
806
928
  // done 事件提供最终事件列表(可能有去重/合并)
807
929
  if (evt.exec_events && evt.exec_events.length > 0) {
808
- execEventsReceived = evt.exec_events;
809
- state.messages[msgIdx].exec_events = [...execEventsReceived];
930
+ allExecEvents = evt.exec_events;
810
931
  }
932
+ state.messages[msgIdx].parts = [...msgParts];
933
+ state.messages[msgIdx]._streamingText = '';
934
+ state.messages[msgIdx].exec_events = [...allExecEvents];
935
+ state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '(无回复)';
811
936
  } else if (evt.type === 'reasoning_delta') {
812
937
  // 模型推理过程增量文本(OpenAI o1/o3/DeepSeek-R1 等推理模型)
813
938
  if (!state.messages[msgIdx].reasoning) state.messages[msgIdx].reasoning = '';
@@ -818,22 +943,25 @@ async function sendMessage() {
818
943
  state.messages[msgIdx].reasoning = evt.content;
819
944
  throttledStreamUpdate(msgIdx);
820
945
  } else if (evt.type === 'error') {
821
- fullResponse = '❌ ' + evt.error;
822
- state.messages[msgIdx].content = fullResponse;
946
+ flushCurrentText();
947
+ currentText = '❌ ' + evt.error;
948
+ msgParts.push({type: 'text', content: currentText});
949
+ state.messages[msgIdx].parts = [...msgParts];
950
+ state.messages[msgIdx]._streamingText = '';
951
+ state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n');
823
952
  }
824
953
  } catch (e) { /* skip malformed */ }
825
954
  }
826
955
  }
827
956
 
828
957
  // Finalize message
958
+ flushCurrentText();
829
959
  if (state.messages[msgIdx]) {
830
960
  state.messages[msgIdx].streaming = false;
831
- if (execEventsReceived.length > 0) {
832
- state.messages[msgIdx].exec_events = execEventsReceived;
833
- }
834
- if (!state.messages[msgIdx].content) {
835
- state.messages[msgIdx].content = '(无回复)';
836
- }
961
+ state.messages[msgIdx].parts = [...msgParts];
962
+ state.messages[msgIdx]._streamingText = '';
963
+ state.messages[msgIdx].exec_events = allExecEvents;
964
+ state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '(无回复)';
837
965
  }
838
966
 
839
967
  // Task list 已通过 SSE task_list_update 事件实时推送,无需再轮询
@@ -37,7 +37,7 @@ async function fetchGroups() {
37
37
  async function createGroupApi(name, description, emoji, color, members) {
38
38
  return await api('/api/groups', {
39
39
  method: 'POST',
40
- body: JSON.stringify({ name, description, emoji, color, members }),
40
+ body: JSON.stringify({ name, description, avatar_emoji: emoji, avatar_color: color, members }),
41
41
  });
42
42
  }
43
43
 
@@ -109,13 +109,12 @@ function renderGroups() {
109
109
  var memberCount = (g.members || []).length;
110
110
  var isActive = g.id === currentGroupId;
111
111
  html += '<div class="group-item ' + (isActive ? 'active' : '') + '" onclick="selectGroup(\'' + escapeHtml(g.id) + '\')" title="' + escapeHtml(g.name) + '">';
112
- html += '<div class="group-icon">' + escapeHtml(g.emoji || '👥') + '</div>';
112
+ html += '<div class="group-icon"' + (g.avatar_color ? ' style="background:' + escapeHtml(g.avatar_color) + ';color:#fff"' : '') + '>' + escapeHtml(g.avatar_emoji || g.emoji || '👥') + '</div>';
113
113
  html += '<div class="group-info">';
114
114
  html += '<div class="group-name">' + escapeHtml(g.name) + '</div>';
115
115
  html += '<div class="group-preview">' + escapeHtml(g.description || memberCount + ' 位成员') + '</div>';
116
116
  html += '</div>';
117
117
  html += '<span class="group-badge">' + memberCount + '</span>';
118
- html += '<button class="group-delete" onclick="event.stopPropagation();deleteGroupConfirm(\'' + escapeHtml(g.id) + '\')" title="删除">✕</button>';
119
118
  html += '</div>';
120
119
  }
121
120
  listEl.innerHTML = html;
@@ -145,7 +144,7 @@ async function selectGroup(gid) {
145
144
  var groupData = await getGroup(gid);
146
145
 
147
146
  // Update header
148
- document.getElementById('headerTitle').textContent = (groupData.emoji || '👥') + ' ' + groupData.name;
147
+ document.getElementById('headerTitle').textContent = (groupData.avatar_emoji || groupData.emoji || '👥') + ' ' + groupData.name;
149
148
  document.getElementById('activeAgentBadge').style.display = 'none';
150
149
  document.getElementById('groupBackBtn').style.display = '';
151
150
  document.getElementById('groupSettingsBtn').style.display = '';
@@ -217,7 +216,7 @@ function renderGroupMessages() {
217
216
  if (groupMessages.length === 0) {
218
217
  var group = groups.find(function(g) { return g.id === currentGroupId; });
219
218
  html = '<div class="welcome-card">'
220
- + '<h2><span class="emoji">' + escapeHtml((group && group.emoji) || '👥') + '</span>'
219
+ + '<h2><span class="emoji">' + escapeHtml((group && (group.avatar_emoji || group.emoji)) || '👥') + '</span>'
221
220
  + ' <span>群聊: ' + escapeHtml((group && group.name) || '') + '</span></h2>'
222
221
  + '<p class="subtitle">向所有成员发送消息,每个 Agent 会分别回复</p>'
223
222
  + '</div>';
@@ -426,8 +425,8 @@ async function showGroupSettingsModal() {
426
425
  + '</div>'
427
426
  + '<div class="agent-form-group"><label>头像</label>'
428
427
  + '<div class="agent-avatar-picker" id="groupSettingsEmojiPicker">'
429
- + emojis.map(function(e) { return '<div class="agent-avatar-option ' + ((groupData.emoji || '👥') === e ? 'selected' : '') + '" onclick="pickGroupSettingsEmoji(this,\'' + e + '\')" data-emoji="' + e + '">' + e + '</div>'; }).join('')
430
- + '</div><input type="hidden" id="groupSettingsEmoji" value="' + escapeHtml(groupData.emoji || '👥') + '"></div>'
428
+ + emojis.map(function(e) { return '<div class="agent-avatar-option ' + ((groupData.avatar_emoji || groupData.emoji || '👥') === e ? 'selected' : '') + '" onclick="pickGroupSettingsEmoji(this,\'' + e + '\')" data-emoji="' + e + '">' + e + '</div>'; }).join('')
429
+ + '</div><input type="hidden" id="groupSettingsEmoji" value="' + escapeHtml(groupData.avatar_emoji || groupData.emoji || '👥') + '"></div>'
431
430
  + '<div class="agent-form-group"><label>成员 (' + members.length + ')</label>'
432
431
  + '<div style="margin-bottom:8px">'
433
432
  + '<button class="config-action-btn" onclick="showAddMemberToGroup()" style="font-size:12px;padding:6px 12px">'
@@ -463,14 +462,14 @@ async function saveGroupSettings() {
463
462
  await updateGroup(currentGroupId, {
464
463
  name: name,
465
464
  description: document.getElementById('groupSettingsDesc').value.trim(),
466
- emoji: document.getElementById('groupSettingsEmoji').value
465
+ avatar_emoji: document.getElementById('groupSettingsEmoji').value
467
466
  });
468
467
  toast('群聊设置已保存', 'success');
469
468
  closeGroupModal();
470
469
  await fetchGroups();
471
470
  var group = groups.find(function(g) { return g.id === currentGroupId; });
472
471
  if (group) {
473
- document.getElementById('headerTitle').textContent = (group.emoji || '👥') + ' ' + group.name;
472
+ document.getElementById('headerTitle').textContent = (group.avatar_emoji || group.emoji || '👥') + ' ' + group.name;
474
473
  }
475
474
  } catch (e) {
476
475
  toast('保存失败: ' + e.message, 'error');
@@ -12,6 +12,9 @@
12
12
  </span>
13
13
  </div>
14
14
  <div class="header-actions">
15
+ <button class="header-btn" id="mobileAgentsBtn" onclick="toggleMobileAgentPanel()" title="Agents" style="display:none">
16
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
17
+ </button>
15
18
  <button class="header-btn" id="groupBackBtn" onclick="exitGroupChat()" title="返回对话" style="display:none">
16
19
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
17
20
  </button>
@@ -39,7 +39,12 @@
39
39
  <div id="rpGroupSection">
40
40
  <div class="rp-section-header" onclick="toggleRpSection('group')">
41
41
  <span class="rp-section-title">👥 群聊</span>
42
- <span class="rp-section-toggle expanded" id="rpGroupToggle">▶</span>
42
+ <div style="display:flex;align-items:center;gap:4px">
43
+ <button class="rp-section-action-btn" onclick="event.stopPropagation();showCreateGroupModal()" title="创建群聊">
44
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
45
+ </button>
46
+ <span class="rp-section-toggle expanded" id="rpGroupToggle">▶</span>
47
+ </div>
43
48
  </div>
44
49
  <div class="rp-section-body" id="rpGroupBody">
45
50
  <div class="group-list" id="groupList">
package/web/ui/index.html CHANGED
@@ -151,6 +151,29 @@ tr:hover{background:var(--surface2)}
151
151
  [data-theme="claude"] .badge-red{background:#c9444422}
152
152
  [data-theme="claude"] .badge-yellow{background:#c4862b22}
153
153
  [data-theme="claude"] .badge-blue{background:#4a7fc922}
154
+ /* ── Mobile Responsive ── */
155
+ @media(max-width:768px){
156
+ body{flex-direction:column}
157
+ .sidebar{position:fixed;left:0;top:0;height:100vh;width:260px;max-width:80vw;z-index:50;transform:translateX(-100%);transition:transform .3s ease;flex-shrink:0}
158
+ .sidebar.mobile-open{transform:translateX(0)}
159
+ .sidebar.collapsed{width:260px;transform:translateX(-100%)}
160
+ .sidebar.collapsed.mobile-open{transform:translateX(0)}
161
+ .sidebar-toggle{display:none!important}
162
+ .mobile-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:45;display:none}
163
+ .mobile-overlay.active{display:block}
164
+ .header{padding:12px 16px}
165
+ .header h2{font-size:16px}
166
+ .content{padding:16px}
167
+ .grid{grid-template-columns:1fr}
168
+ .form-row{grid-template-columns:1fr}
169
+ .table-wrap{overflow-x:auto}
170
+ .modal{width:95%;max-height:90vh;padding:16px}
171
+ .modal-wide{max-width:95%}
172
+ .tabs{gap:0;overflow-x:auto}
173
+ .toast{left:16px;right:16px;bottom:16px}
174
+ .agent-card{flex-direction:column;align-items:flex-start}
175
+ .agent-card .flex.flex-col{flex-direction:row;gap:4px}
176
+ }
154
177
  </style>
155
178
  </head>
156
179
  <body>
@@ -181,8 +204,9 @@ tr:hover{background:var(--surface2)}
181
204
  · <a href="#" onclick="checkUpdate(true)" style="color:var(--primary)">检查更新</a>
182
205
  </div>
183
206
  </div>
207
+ <div class="mobile-overlay" id="adminMobileOverlay" onclick="closeMobileSidebar()"></div>
184
208
  <div class="main">
185
- <div class="header"><div style="display:flex;align-items:center;gap:12px"><h2 id="pageTitle">📊 仪表盘</h2><button class="header-btn" id="themeToggle" title="切换主题"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button></div><div style="display:flex;align-items:center;gap:8px"><span class="status-dot"></span>运行中</div></div>
209
+ <div class="header"><div style="display:flex;align-items:center;gap:12px"><button class="header-btn" id="mobileMenuBtn" onclick="toggleMobileSidebar()" style="display:none" title="菜单"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button><h2 id="pageTitle">📊 仪表盘</h2><button class="header-btn" id="themeToggle" title="切换主题"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button></div><div style="display:flex;align-items:center;gap:8px"><span class="status-dot"></span>运行中</div></div>
186
210
  <div class="content" id="content"></div>
187
211
  </div>
188
212
  <div id="modalContainer"></div>
@@ -250,12 +274,28 @@ function initTheme(){const s=localStorage.getItem('myagent-theme')||'claude';doc
250
274
  function toggleTheme(){const c=document.documentElement.getAttribute('data-theme')||'claude';const n=c==='claude'?'dark':'claude';document.documentElement.setAttribute('data-theme',n);localStorage.setItem('myagent-theme',n);updateThemeIcon(n)}
251
275
  function updateThemeIcon(t){const b=document.getElementById('themeToggle');if(!b)return;if(t==='dark'){b.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';b.title='切换到 Claude 风格'}else{b.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';b.title='切换到夜间模式'}}
252
276
  // ── Sidebar Collapse ──
253
- function toggleSidebar(){const s=document.getElementById('adminSidebar');const t=document.getElementById('sidebarToggle');if(!s)return;s.classList.toggle('collapsed');const isCollapsed=s.classList.contains('collapsed');t.textContent=isCollapsed?'▶':'◀';localStorage.setItem('myagent-admin-sidebar-collapsed',isCollapsed)}
277
+ function toggleSidebar(){const s=document.getElementById('adminSidebar');const t=document.getElementById('sidebarToggle');if(!s)return;s.classList.toggle('collapsed');const isCollapsed=s.classList.contains('collapsed');t.textContent=isCollapsed?'▶':'◀';localStorage.setItem('myagent-admin-sidebar-collapsed',isCollapsed);closeMobileSidebar()}
254
278
  // Initialize
255
279
  initTheme();
256
280
  document.getElementById('themeToggle')?.addEventListener('click',toggleTheme);
257
281
  if(localStorage.getItem('myagent-admin-sidebar-collapsed')==='true'){document.getElementById('adminSidebar')?.classList.add('collapsed');const t=document.getElementById('sidebarToggle');if(t)t.textContent='▶'}
258
282
 
283
+ // ── Mobile Sidebar ──
284
+ function toggleMobileSidebar(){
285
+ const s=document.getElementById('adminSidebar');
286
+ const o=document.getElementById('adminMobileOverlay');
287
+ s.classList.toggle('mobile-open');
288
+ o.classList.toggle('active');
289
+ }
290
+ function closeMobileSidebar(){
291
+ document.getElementById('adminSidebar').classList.remove('mobile-open');
292
+ document.getElementById('adminMobileOverlay').classList.remove('active');
293
+ }
294
+ // Show hamburger on mobile
295
+ function checkMobile(){const btn=document.getElementById('mobileMenuBtn');if(btn)btn.style.display=window.innerWidth<=768?'grid':'none'}
296
+ window.addEventListener('resize',checkMobile);
297
+ checkMobile();
298
+
259
299
  loadVersion();
260
300
  setTimeout(()=>checkUpdate(false),30000);
261
301
  function showConfirm(title,msg,onOk){
@@ -266,6 +306,7 @@ function showConfirm(title,msg,onOk){
266
306
  }
267
307
 
268
308
  function showPage(page){
309
+ closeMobileSidebar();
269
310
  currentPage=page;
270
311
  document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===page));
271
312
  $('pageTitle').textContent=pages[page]||page;