tagword-codeagent 1.1.6__py3-none-any.whl

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.
Files changed (173) hide show
  1. codeagent/__init__.py +10 -0
  2. codeagent/__main__.py +13 -0
  3. codeagent/cli/__init__.py +13 -0
  4. codeagent/cli/entrypoint.py +161 -0
  5. codeagent/cli/main.py +979 -0
  6. codeagent/core/__init__.py +2 -0
  7. codeagent/core/attachments.py +342 -0
  8. codeagent/core/audio_models.py +50 -0
  9. codeagent/core/bootstrap.py +178 -0
  10. codeagent/core/default_presets.py +159 -0
  11. codeagent/core/env.py +106 -0
  12. codeagent/core/folder_picker.py +151 -0
  13. codeagent/core/image_understanding.py +90 -0
  14. codeagent/core/models.py +6 -0
  15. codeagent/core/paths.py +238 -0
  16. codeagent/core/pricing.py +107 -0
  17. codeagent/core/process_ports.py +258 -0
  18. codeagent/core/seed_bridge.py +21 -0
  19. codeagent/core/settings.py +98 -0
  20. codeagent/core/speech_synth.py +330 -0
  21. codeagent/core/team_config.py +247 -0
  22. codeagent/core/team_manager.py +245 -0
  23. codeagent/core/token_counter.py +41 -0
  24. codeagent/core/tts_voices.py +103 -0
  25. codeagent/core/usage_billing.py +72 -0
  26. codeagent/core/vision_models.py +93 -0
  27. codeagent/memory/diary.py +144 -0
  28. codeagent/persona_defaults/README.md +12 -0
  29. codeagent/persona_defaults/agent.md +352 -0
  30. codeagent/persona_defaults/identity.md +29 -0
  31. codeagent/persona_defaults/memory.md +4 -0
  32. codeagent/persona_defaults/skills.md +325 -0
  33. codeagent/persona_defaults/soul.md +38 -0
  34. codeagent/persona_defaults/tools.md +41 -0
  35. codeagent/persona_defaults/user.md +18 -0
  36. codeagent/runtime/__init__.py +11 -0
  37. codeagent/runtime/agent_context.py +6 -0
  38. codeagent/runtime/agent_runtime.py +21 -0
  39. codeagent/runtime/compact_prompt.py +9 -0
  40. codeagent/runtime/compact_state.py +40 -0
  41. codeagent/runtime/llm_worker.py +132 -0
  42. codeagent/runtime/orchestrator.py +161 -0
  43. codeagent/runtime/prompt_enrichment.py +324 -0
  44. codeagent/runtime/session_manager.py +30 -0
  45. codeagent/runtime/task_split.py +38 -0
  46. codeagent/runtime/turn_loop.py +13 -0
  47. codeagent/runtime/worker.py +48 -0
  48. codeagent/server/__init__.py +505 -0
  49. codeagent/server/app_factory.py +1292 -0
  50. codeagent/server/attachment_api.py +261 -0
  51. codeagent/server/hub.py +175 -0
  52. codeagent/server/icon.png +0 -0
  53. codeagent/server/self_healing.py +143 -0
  54. codeagent/server/team_engine.py +243 -0
  55. codeagent/server/tts_api.py +51 -0
  56. codeagent/server/webui_api_app.py +3180 -0
  57. codeagent/skills/__init__.py +2 -0
  58. codeagent/skills/select.py +144 -0
  59. codeagent/tools/__init__.py +6 -0
  60. codeagent/tools/agent_tools.py +10 -0
  61. codeagent/web/__init__.py +1 -0
  62. codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
  63. codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
  64. codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
  65. codeagent/web/auth_impl.py +326 -0
  66. codeagent/web/static/00-marked.min.js +6 -0
  67. codeagent/web/static/00-mermaid.min.js +2314 -0
  68. codeagent/web/static/00-purify.min.js +3 -0
  69. codeagent/web/static/00-storage.js +83 -0
  70. codeagent/web/static/00-utils.js +51 -0
  71. codeagent/web/static/01-theme.css +96 -0
  72. codeagent/web/static/01a-scroll.js +86 -0
  73. codeagent/web/static/01b-think.js +62 -0
  74. codeagent/web/static/01c-session-identity.js +81 -0
  75. codeagent/web/static/01c-session-thinking.js +61 -0
  76. codeagent/web/static/01c-session-tree-menus.js +349 -0
  77. codeagent/web/static/01c-session-tree.js +531 -0
  78. codeagent/web/static/01c-session-utilities.js +83 -0
  79. codeagent/web/static/01c-vision-model.js +132 -0
  80. codeagent/web/static/01d-image-gen-model.js +84 -0
  81. codeagent/web/static/01e-audio-model.js +97 -0
  82. codeagent/web/static/01f-compose-ui.js +88 -0
  83. codeagent/web/static/01g-tts-settings.js +128 -0
  84. codeagent/web/static/01h-music-model.js +85 -0
  85. codeagent/web/static/01i-model-stack.js +291 -0
  86. codeagent/web/static/01j-video-gen-model.js +85 -0
  87. codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
  88. codeagent/web/static/01k-model-stack-state.js +209 -0
  89. codeagent/web/static/01q-inflight.js +195 -0
  90. codeagent/web/static/02-reset.css +22 -0
  91. codeagent/web/static/02-session-ui.js +201 -0
  92. codeagent/web/static/03-layout-p1.css +2 -0
  93. codeagent/web/static/03-layout-p2.css +2 -0
  94. codeagent/web/static/03a-links-stream.js +88 -0
  95. codeagent/web/static/03b-stream-text.js +119 -0
  96. codeagent/web/static/03c-render-bubble.js +520 -0
  97. codeagent/web/static/03d-agent-bubble.js +126 -0
  98. codeagent/web/static/03e-tool-trace.js +182 -0
  99. codeagent/web/static/03f-timeline.js +54 -0
  100. codeagent/web/static/03g-bubble-tts.js +292 -0
  101. codeagent/web/static/04-components-p1.css +1 -0
  102. codeagent/web/static/04-components-p2b.css +1 -0
  103. codeagent/web/static/04-components-p3.css +2 -0
  104. codeagent/web/static/04-components-p4.css +1 -0
  105. codeagent/web/static/04-components-p5.css +1 -0
  106. codeagent/web/static/04-components-p6.css +58 -0
  107. codeagent/web/static/04-components-p7.css +4 -0
  108. codeagent/web/static/04-components-p8.css +1 -0
  109. codeagent/web/static/04-components-p9.css +1 -0
  110. codeagent/web/static/04-ws-connect.js +351 -0
  111. codeagent/web/static/04-ws-helpers.js +116 -0
  112. codeagent/web/static/05-chat-p1.css +1058 -0
  113. codeagent/web/static/05-chat-p2.css +1 -0
  114. codeagent/web/static/05-chat-p3.css +295 -0
  115. codeagent/web/static/05a-session-history.js +176 -0
  116. codeagent/web/static/05b-session-list.js +114 -0
  117. codeagent/web/static/05c-dialog.js +123 -0
  118. codeagent/web/static/05d-project-create.js +172 -0
  119. codeagent/web/static/05e-project-remote.js +143 -0
  120. codeagent/web/static/06-chat.js +552 -0
  121. codeagent/web/static/06-utilities.css +181 -0
  122. codeagent/web/static/06b-attachments.js +304 -0
  123. codeagent/web/static/06c-camera.js +488 -0
  124. codeagent/web/static/07-activity-bar.css +127 -0
  125. codeagent/web/static/09-git-p1.css +1 -0
  126. codeagent/web/static/09-git-p2.css +1 -0
  127. codeagent/web/static/09a-init-bind.js +110 -0
  128. codeagent/web/static/09b-panel-toggle.js +80 -0
  129. codeagent/web/static/09c-api-refresh.js +100 -0
  130. codeagent/web/static/09d-status-log.js +80 -0
  131. codeagent/web/static/09e-commit.js +80 -0
  132. codeagent/web/static/09f-remote.js +93 -0
  133. codeagent/web/static/11a-cron-parse.js +117 -0
  134. codeagent/web/static/11b-cron-panel.js +100 -0
  135. codeagent/web/static/11c-cron-card.js +133 -0
  136. codeagent/web/static/11d-cron-form.js +105 -0
  137. codeagent/web/static/11e-env-config.js +168 -0
  138. codeagent/web/static/11f-env-llm-a-state.js +136 -0
  139. codeagent/web/static/11f-env-llm-b-form.js +409 -0
  140. codeagent/web/static/11f-env-llm-c-board.js +322 -0
  141. codeagent/web/static/11g-env-paths-git.js +973 -0
  142. codeagent/web/static/11h-env-mcp-a-state.js +161 -0
  143. codeagent/web/static/11h-env-mcp-b-form.js +229 -0
  144. codeagent/web/static/11h-env-mcp-c-board.js +565 -0
  145. codeagent/web/static/12-plugins.js +127 -0
  146. codeagent/web/static/13a-skills.js +156 -0
  147. codeagent/web/static/13a-todo-utils.js +140 -0
  148. codeagent/web/static/13b-todo-fetch.js +106 -0
  149. codeagent/web/static/13b-tools.js +127 -0
  150. codeagent/web/static/13c-todo-crud.js +92 -0
  151. codeagent/web/static/14-activity-bar.js +177 -0
  152. codeagent/web/static/15-plans.css +194 -0
  153. codeagent/web/static/15-plans.js +219 -0
  154. codeagent/web/static/16-mobile.css +612 -0
  155. codeagent/web/static/16-mobile.js +178 -0
  156. codeagent/web/static/17-preset-ux.css +287 -0
  157. codeagent/web/static/18-agent-mgr.css +616 -0
  158. codeagent/web/static/18-agent-mgr.js +204 -0
  159. codeagent/web/static/24-health-dashboard.js +68 -0
  160. codeagent/web/static/99-brand-polish.css +152 -0
  161. codeagent/web/static/body.html +824 -0
  162. codeagent/web/static/setup/setup_form.html +171 -0
  163. codeagent/web/static/setup/setup_head.html +100 -0
  164. codeagent/web/static/setup/setup_tail.html +264 -0
  165. codeagent/web/static/setup/setup_test.html +38 -0
  166. codeagent/web/web_login.html +265 -0
  167. codeagent/web/webui.html +35 -0
  168. tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
  169. tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
  170. tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
  171. tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
  172. tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
  173. tagword_codeagent-1.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,195 @@
1
+
2
+ const chatInflightBySid = {};
3
+ /** 待发送附件(06b-attachments.js 读写;须在此提前声明避免 TDZ) */
4
+ var pendingAttachments = [];
5
+
6
+ /**
7
+ * 刚刚结束运行的会话集合(sid → true)。
8
+ * agent 运行结束后显示绿色实心圆点(不闪烁),用户点击后清除。
9
+ * 通过 localStorage 持久化,刷新页面后仍可恢复。
10
+ */
11
+ const chatCompletedBySid = {};
12
+ var _COMPLETED_STORAGE_KEY = STORAGE_KEYS.SESS_COMPLETED;
13
+ /** 用户手动清除过 completed 状态的会话集合(防御:防止刷新/WS重连等路径重新标记)。sessionStorage 持久化。 */
14
+ var _CLEARED_STORAGE_KEY = STORAGE_KEYS.SESS_CLEARED;
15
+ const _userClearedCompletedSids = {};
16
+
17
+ function _persistUserCleared() {
18
+ try {
19
+ var keys = Object.keys(_userClearedCompletedSids);
20
+ if (keys.length === 0) sessionStorage.removeItem(_CLEARED_STORAGE_KEY);
21
+ else sessionStorage.setItem(_CLEARED_STORAGE_KEY, JSON.stringify(keys));
22
+ } catch (_) {}
23
+ }
24
+ function _restoreUserCleared() {
25
+ try {
26
+ var raw = sessionStorage.getItem(_CLEARED_STORAGE_KEY);
27
+ if (!raw) return;
28
+ var arr = JSON.parse(raw);
29
+ if (Array.isArray(arr)) arr.forEach(function(k) { _userClearedCompletedSids[k] = true; });
30
+ } catch (_) {}
31
+ }
32
+
33
+ /** 从 localStorage 恢复会话已完成状态,但排除用户手动清除过的。 */
34
+ function restoreCompletedSessions() {
35
+ _restoreUserCleared();
36
+ try {
37
+ var raw = localStorage.getItem(_COMPLETED_STORAGE_KEY);
38
+ if (!raw) return;
39
+ var obj = JSON.parse(raw);
40
+ if (obj && typeof obj === 'object') {
41
+ Object.keys(obj).forEach(function(k) {
42
+ if (!_userClearedCompletedSids[k]) {
43
+ chatCompletedBySid[k] = true;
44
+ }
45
+ });
46
+ }
47
+ } catch (_) {}
48
+ }
49
+
50
+ /** 将会话已完成状态持久化到 localStorage */
51
+ function persistCompletedSessions() {
52
+ try {
53
+ var keys = Object.keys(chatCompletedBySid);
54
+ if (keys.length === 0) {
55
+ localStorage.removeItem(_COMPLETED_STORAGE_KEY);
56
+ } else {
57
+ var obj = {};
58
+ keys.forEach(function(k) { obj[k] = true; });
59
+ localStorage.setItem(_COMPLETED_STORAGE_KEY, JSON.stringify(obj));
60
+ }
61
+ } catch (_) {}
62
+ }
63
+
64
+ let lastLocalAgentReplyNorm = '';
65
+ let lastLocalAgentReplyAt = 0;
66
+ let pinSessionToTopOnce = '';
67
+
68
+ function isSessionRunning(sid) {
69
+ return (chatInflightBySid[String(sid || '')] || 0) > 0;
70
+ }
71
+
72
+ function composeHasSendPayload() {
73
+ const text = msg && msg.value ? msg.value.trim() : '';
74
+ const hasPending = pendingAttachments && pendingAttachments.length > 0;
75
+ return !!(text || hasPending);
76
+ }
77
+
78
+ /** 幂等设置会话 running / idle(单一状态源,避免 +/- 计数漂移)
79
+ * ⚠️ 注意:此函数只负责 running 状态,不负责 completed(已完成绿色)状态。
80
+ * completed 状态由 run_finished WS 事件 handler 专门管理(见 04-ws-connect.js)。
81
+ */
82
+ function setSessionRunning(sid, running) {
83
+ const k = String(sid || '');
84
+ if (!k) return;
85
+ if (running) {
86
+ chatInflightBySid[k] = 1;
87
+ // 开始运行 → 清除 stale completed
88
+ delete chatCompletedBySid[k];
89
+ persistCompletedSessions();
90
+ } else {
91
+ delete chatInflightBySid[k];
92
+ // 不再自动标记 completed —— 由 run_finished handler 管
93
+ }
94
+ updateComposerButtons();
95
+ if (typeof applySessionRunningState === 'function') applySessionRunningState(k);
96
+ if (typeof applySessionCompletedState === 'function') applySessionCompletedState(k);
97
+ }
98
+
99
+ function updateComposerButtons() {
100
+ const active = isSessionRunning(sessionId);
101
+ const showStop = active && !composeHasSendPayload();
102
+ if (typeof stopBtn !== 'undefined' && stopBtn) stopBtn.disabled = !active;
103
+ if (sendBtn) {
104
+ sendBtn.classList.toggle('is-stop', showStop);
105
+ sendBtn.classList.toggle('is-finishing', active && composeHasSendPayload());
106
+ sendBtn.title = showStop
107
+ ? '停止(等待本轮回答完成)'
108
+ : (active ? '发送补充消息 (Enter)' : '发送 (Enter)');
109
+ const sendIcon = sendBtn.querySelector('.compose__send-icon');
110
+ const stopIcon = sendBtn.querySelector('.compose__stop-icon');
111
+ if (sendIcon) sendIcon.style.display = showStop ? 'none' : '';
112
+ if (stopIcon) stopIcon.style.display = showStop ? '' : 'none';
113
+ sendBtn.disabled = false;
114
+ }
115
+ }
116
+
117
+ /** 清除会话的「已完成」绿色状态(用户点击高亮会话时调用) */
118
+ function clearSessionCompleted(sid) {
119
+ const k = String(sid || '');
120
+ if (!k) return;
121
+ // 记录用户手动清除过此会话:后续 run_finished 事件不会再标记绿色
122
+ _userClearedCompletedSids[k] = true;
123
+ _persistUserCleared();
124
+ delete chatCompletedBySid[k];
125
+ persistCompletedSessions();
126
+ if (typeof applySessionCompletedState === 'function') applySessionCompletedState(k);
127
+ }
128
+
129
+ function bumpChatInflight(sid, delta) {
130
+ if (delta > 0) setSessionRunning(sid, true);
131
+ else if (delta < 0) setSessionRunning(sid, false);
132
+ }
133
+ function rememberLocalAgentReply(text) {
134
+ const n = normReply(text);
135
+ if (!n) return;
136
+ lastLocalAgentReplyNorm = n;
137
+ lastLocalAgentReplyAt = Date.now();
138
+ }
139
+ function resetAgentReplyDedupe() {
140
+ lastLocalAgentReplyNorm = '';
141
+ lastLocalAgentReplyAt = 0;
142
+ // Also reset streaming bubble state so a new conversation starts fresh.
143
+ // (global vars defined in 03-markdown.js)
144
+ if (typeof _streamBubbleWrap !== 'undefined') _streamBubbleWrap = null;
145
+ if (typeof _streamConsumedLen !== 'undefined') _streamConsumedLen = 0;
146
+ }
147
+
148
+ /**
149
+ * 页面刷新后从后端恢复正在运行中的会话状态(心跳指示)。
150
+ * 并同时从 localStorage 恢复已完成会话状态(绿色实心)。
151
+ * 由 initWebUiSessions 在最开始时调用。
152
+ */
153
+ async function restoreRunningSessions() {
154
+ // 1. 从 localStorage 恢复已完成(绿色实心)状态
155
+ restoreCompletedSessions();
156
+ // 2. 全量替换 running 状态:先清空所有 stale 状态,再填充后端快照。
157
+ // 避免 WS 断连期间丢失的 run_finished 事件导致会话"卡"在运行状态。
158
+ Object.keys(chatInflightBySid).forEach(function(sid) {
159
+ setSessionRunning(sid, false);
160
+ });
161
+ try {
162
+ const aid = (typeof agentId !== 'undefined') ? agentId : 'default';
163
+ const r = await fetch('/api/ui/sessions/running?agent_id=' + encodeURIComponent(aid));
164
+ if (!r.ok) return;
165
+ const j = await r.json();
166
+ if (j && Array.isArray(j.running)) {
167
+ j.running.forEach(function(sid) {
168
+ if (sid) setSessionRunning(String(sid), true);
169
+ });
170
+ }
171
+ } catch (_) {}
172
+ }
173
+
174
+ // ---- 项目路径显示:切换项目时更新 topbar ---- //
175
+ document.addEventListener('project-changed', function(e) {
176
+ var pathEl = document.getElementById('currentProjectPath');
177
+ if (!pathEl) return;
178
+ var pid = (e && e.detail && e.detail.projectId) || projectId || '';
179
+ var path = '';
180
+ if (pid && treeProjectsCache && treeProjectsCache.aid === agentId) {
181
+ for (var i = 0; i < treeProjectsCache.projects.length; i++) {
182
+ var p = treeProjectsCache.projects[i];
183
+ if (p.id === pid && p.path) {
184
+ path = p.path;
185
+ break;
186
+ }
187
+ }
188
+ }
189
+ if (path) {
190
+ pathEl.textContent = path;
191
+ pathEl.style.display = '';
192
+ } else {
193
+ pathEl.style.display = 'none';
194
+ }
195
+ });
@@ -0,0 +1,22 @@
1
+ /* ================================================================
2
+ * 02-reset.css — Global resets base element styles
3
+ * ================================================================ */
4
+
5
+ * { box-sizing: border-box; }
6
+ html, body { height: 100%; }
7
+ body {
8
+ margin: 0;
9
+ font-family: var(--font-sans);
10
+ font-size: 14px;
11
+ line-height: 1.55;
12
+ background: var(--bg);
13
+ color: var(--text);
14
+ -webkit-font-smoothing: antialiased;
15
+ text-rendering: optimizeLegibility;
16
+ overflow: hidden;
17
+ font-feature-settings: "cv02", "cv03", "cv04", "cv11";
18
+ }
19
+ a { color: var(--accent); text-underline-offset: 2px; }
20
+ a:hover { color: var(--accent-hover); }
21
+ button { font: inherit; color: inherit; }
22
+ ::selection { background: var(--accent-soft); color: var(--text); }
@@ -0,0 +1,201 @@
1
+ /* ================================================================
2
+ * 02-session-ui.js
3
+ * - Unread count bookkeeping (localStorage map)
4
+ * - Session list item rendering (buildSessListItem)
5
+ * - Session list DOM helpers (highlight, running state)
6
+ * - Session operations (archive, delete, copy-id)
7
+ * ================================================================ */
8
+
9
+ // ---------------- Unread count bookkeeping ----------------
10
+
11
+ function getReadMap() {
12
+ try {
13
+ const o = JSON.parse(localStorage.getItem(READ_KEY) || '{}');
14
+ return o && typeof o === 'object' ? o : {};
15
+ } catch (_) { return {}; }
16
+ }
17
+ function saveReadMap(m) {
18
+ localStorage.setItem(READ_KEY, JSON.stringify(m));
19
+ }
20
+ function ensureReadBaseline(rows) {
21
+ const m = getReadMap();
22
+ let ch = false;
23
+ (rows || []).forEach((r) => {
24
+ const sid = r.session_id;
25
+ if (sid && !Object.prototype.hasOwnProperty.call(m, sid)) {
26
+ m[sid] = r.message_count || 0;
27
+ ch = true;
28
+ }
29
+ });
30
+ if (ch) saveReadMap(m);
31
+ }
32
+ function markSessionReadByCount(sid, count) {
33
+ const m = getReadMap();
34
+ m[sid] = count;
35
+ saveReadMap(m);
36
+ }
37
+ function unreadCountFor(row) {
38
+ if (row && row.session_id === sessionId) return 0;
39
+ const m = getReadMap();
40
+ const mc = row.message_count || 0;
41
+ if (!Object.prototype.hasOwnProperty.call(m, row.session_id)) return 0;
42
+ return Math.max(0, mc - m[row.session_id]);
43
+ }
44
+ function removeSessionReadBaseline(sid) {
45
+ const m = getReadMap();
46
+ delete m[sid];
47
+ saveReadMap(m);
48
+ }
49
+ function updateMainHeaderForSession(sid) {
50
+ const titleEl = document.getElementById('currentConvTitle');
51
+ const chEl = document.getElementById('currentConvChannel');
52
+ if (!titleEl || !chEl) return;
53
+ const row = lastSessionsCache.find((r) => r.session_id === sid);
54
+ if (row) {
55
+ titleEl.textContent = row.display_title || '对话';
56
+ chEl.textContent = row.channel || 'Web 聊天';
57
+ } else {
58
+ titleEl.textContent = '新对话';
59
+ chEl.textContent = 'Web 聊天';
60
+ }
61
+ }
62
+ function orderSessionsWithPinFirst(sessions, pinSid) {
63
+ const pin = String(pinSid || '');
64
+ if (!pin || !sessions || !sessions.length) return (sessions || []).slice();
65
+ const next = sessions.slice();
66
+ const idx = next.findIndex((r) => r.session_id === pin);
67
+ if (idx > 0) { const [hit] = next.splice(idx, 1); next.unshift(hit); }
68
+ return next;
69
+ }
70
+ function highlightSessList(sid) {
71
+ // 用户点击了某个会话 → 清除其「已完成」标记
72
+ if (typeof chatCompletedBySid !== 'undefined' && typeof clearSessionCompleted === 'function') {
73
+ clearSessionCompleted(sid);
74
+ }
75
+ document.querySelectorAll('#sessList .sess-row').forEach((el) => {
76
+ el.classList.toggle('is-active', el.getAttribute('data-session-id') === sid);
77
+ });
78
+ applyAllSessionRunningStates();
79
+ applyAllSessionCompletedStates();
80
+ }
81
+ function applySessionRunningState(sid) {
82
+ const k = String(sid || '');
83
+ if (!k) return;
84
+ const active = (chatInflightBySid[k] || 0) > 0;
85
+ // 更新侧边栏列表
86
+ const rows = document.querySelectorAll('#sessList .sess-row[data-session-id="' + CSS.escape(k) + '"]');
87
+ rows.forEach((el) => el.classList.toggle('is-running', active));
88
+ // 更新树状视图
89
+ const treeSessions = document.querySelectorAll('.tree-session[data-session-id="' + CSS.escape(k) + '"]');
90
+ treeSessions.forEach((el) => el.classList.toggle('is-running', active));
91
+ // 更新当前会话的 body 标记
92
+ if (typeof sessionId !== 'undefined' && k === String(sessionId)) {
93
+ try { document.body.classList.toggle('chat-running', active); } catch (_) {}
94
+ }
95
+ }
96
+ function applyAllSessionRunningStates() {
97
+ try {
98
+ const all = new Set(Object.keys(chatInflightBySid));
99
+ if (typeof sessionId !== 'undefined' && sessionId) all.add(String(sessionId));
100
+ all.forEach(applySessionRunningState);
101
+ } catch (_) {}
102
+ }
103
+ function applySessionCompletedState(sid) {
104
+ const k = String(sid || '');
105
+ if (!k) return;
106
+ // completed 状态只由 run_finished handler 标记(带 _userClearedCompletedSids 守卫),
107
+ // 这里直接读 chatCompletedBySid 即可,不需要再叠守卫。
108
+ const completed = !!(chatCompletedBySid && chatCompletedBySid[k]);
109
+ // 当前正在查看的会话由 .is-active 控制,不叠加 completed(避免绿色覆盖 accent 高亮)
110
+ const isActive = typeof sessionId !== 'undefined' && k === String(sessionId);
111
+ const shouldShow = completed && !isActive;
112
+ // 更新侧边栏列表
113
+ const rows = document.querySelectorAll('#sessList .sess-row[data-session-id="' + CSS.escape(k) + '"]');
114
+ rows.forEach((el) => el.classList.toggle('is-completed', shouldShow && !el.classList.contains('is-running')));
115
+ // 更新树状视图
116
+ const treeSessions = document.querySelectorAll('.tree-session[data-session-id="' + CSS.escape(k) + '"]');
117
+ treeSessions.forEach((el) => el.classList.toggle('is-completed', shouldShow && !el.classList.contains('is-running')));
118
+ }
119
+ function applyAllSessionCompletedStates() {
120
+ try {
121
+ const all = new Set(Object.keys(chatCompletedBySid || {}));
122
+ all.forEach(applySessionCompletedState);
123
+ } catch (_) {}
124
+ }
125
+
126
+ // ---------------- Session list item rendering ----------------
127
+
128
+ function buildSessListItem(row) {
129
+ const wrap = document.createElement('div');
130
+ wrap.className = 'sess-row';
131
+ wrap.setAttribute('data-session-id', row.session_id);
132
+ // 检查初始运行状态
133
+ if (typeof chatInflightBySid !== 'undefined' && (chatInflightBySid[row.session_id] || 0) > 0) {
134
+ wrap.classList.add('is-running');
135
+ }
136
+ // 已完成状态(绿色)由 refreshSessionList() 末尾的 requestAnimationFrame 统一通过
137
+ // applySessionCompletedState 来管理,不在构建时单独设置,避免时序竞争(清除→重建→再标记)。
138
+
139
+ const btn = document.createElement('button');
140
+ btn.type = 'button';
141
+ btn.className = 'sess-item-main';
142
+ btn.setAttribute('data-session-id', row.session_id);
143
+ btn.setAttribute('role', 'option');
144
+ const dot = document.createElement('span');
145
+ dot.className = 'sess-item__channel-dot';
146
+ dot.setAttribute('aria-hidden', 'true');
147
+ const body = document.createElement('div');
148
+ body.className = 'sess-item__body';
149
+ const title = document.createElement('div');
150
+ title.className = 'sess-item__title';
151
+ title.textContent = row.display_title || '未命名对话';
152
+ const sub = document.createElement('div');
153
+ sub.className = 'sess-item__sub';
154
+ sub.textContent = (row.channel || '') + ' · ' + (row.message_count || 0) + ' 条';
155
+ body.appendChild(title);
156
+ body.appendChild(sub);
157
+ const badge = document.createElement('span');
158
+ badge.className = 'sess-item__badge';
159
+ const un = unreadCountFor(row);
160
+ if (un <= 0) badge.hidden = true;
161
+ else badge.textContent = un > 99 ? '99+' : String(un);
162
+ btn.appendChild(dot);
163
+ btn.appendChild(body);
164
+ btn.appendChild(badge);
165
+
166
+ const opWrap = document.createElement('div');
167
+ opWrap.className = 'sess-item__op-wrap';
168
+ const trig = document.createElement('button');
169
+ trig.type = 'button';
170
+ trig.className = 'sess-item__op-trigger';
171
+ trig.setAttribute('aria-label', '会话操作');
172
+ trig.setAttribute('aria-haspopup', 'true');
173
+ trig.textContent = '\u22EE';
174
+ const menu = document.createElement('div');
175
+ menu.className = 'sess-item__menu';
176
+ menu.setAttribute('role', 'menu');
177
+ const bCopyId = document.createElement('button');
178
+ bCopyId.type = 'button';
179
+ bCopyId.setAttribute('data-sess-act', 'copy-id');
180
+ bCopyId.setAttribute('role', 'menuitem');
181
+ bCopyId.textContent = '复制会话 ID';
182
+ const bArch = document.createElement('button');
183
+ bArch.type = 'button';
184
+ bArch.setAttribute('data-sess-act', 'archive');
185
+ bArch.setAttribute('role', 'menuitem');
186
+ bArch.textContent = '归档';
187
+ const bDel = document.createElement('button');
188
+ bDel.type = 'button';
189
+ bDel.setAttribute('data-sess-act', 'delete');
190
+ bDel.setAttribute('role', 'menuitem');
191
+ bDel.className = 'sess-item__menu-danger';
192
+ bDel.textContent = '删除';
193
+ menu.appendChild(bCopyId);
194
+ menu.appendChild(bArch);
195
+ menu.appendChild(bDel);
196
+ opWrap.appendChild(trig);
197
+ opWrap.appendChild(menu);
198
+ wrap.appendChild(btn);
199
+ wrap.appendChild(opWrap);
200
+ return wrap;
201
+ }
@@ -0,0 +1,2 @@
1
+ /* ================================================================ * 03-layout.css — App grid, sidebar, workspace, topbar, pages * ================================================================ */.app { height: 100vh; display: grid; grid-template-columns: var(--activity-bar-w, 48px) var(--sidebar-w) 1fr;}.workspace { position: relative; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--bg); overflow: hidden;}/* ============ Pages ============ */.workspace > .topbar { position: relative; z-index: 2; flex-shrink: 0;} .page { display: none; flex: none; min-height: 0; flex-direction: column; position: absolute; left: 0; right: 0; top: var(--topbar-h, 48px); bottom: 0; width: 100%; box-sizing: border-box; z-index: 0;} .page.active { display: flex; z-index: 1; animation: pageIn 0.2s cubic-bezier(0.2, 0, 0, 1);}@keyframes pageIn { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; }}.page--scroll { overflow-y: auto; padding: var(--sp-8) var(--sp-8) var(--sp-10); scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent;}.page--scroll::-webkit-scrollbar { width: 10px; }.page--scroll::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 5px; }.content-max { max-width: 920px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--sp-5);}/* ============ Topbar ============ */.topbar { position: relative; height: var(--topbar-h); padding: 0 var(--sp-6); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: var(--sp-4); background: var(--bg-panel); flex-shrink: 0;}.topbar__title-group { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; flex: 1;}.topbar__title { margin: 0; font-size: 14px; font-weight: 600; letter-spacing: -0.005em; color: var(--text); line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 48vw;}.channel-pill { font-size: 10.5px; font-weight: 550; padding: 2px 8px; border-radius: var(--r-full); background: var(--accent-soft); color: var(--accent); letter-spacing: 0.01em; line-height: 1.4; flex-shrink: 0;}
2
+ .topbar__project-path { font-size: 11px; color: var(--text-muted, #888); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 240px; padding: 1px 8px; border-left: 1px solid var(--border-color, #e5e5ea); flex-shrink: 1; min-width: 0;}.topbar__project-path::before { content: "📂 "; font-size: 10px;}.topbar__meta { display: flex; align-items: center; gap: var(--sp-2); color: var(--text-muted); font-size: 12px;}.session-id-chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: var(--r-full); background: var(--bg-subtle); border: 1px solid var(--border); color: var(--text-muted); font-family: var(--font-mono); font-size: 10.5px; max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: default;}.session-id-chip::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--success); flex-shrink: 0; box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.18);}.topbar__todo-btn { width: 28px; height: 28px; padding: 0; border: none; border-radius: var(--r-sm); background: transparent; color: var(--text-muted); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; transition: background 0.12s ease, color 0.12s ease;}.topbar__todo-btn:hover { background: var(--bg-hover); color: var(--text); }.topbar__todo-btn.is-active { color: var(--accent); background: var(--accent-soft); }.topbar__todo-btn svg { width: 16px; height: 16px; }
@@ -0,0 +1,2 @@
1
+
2
+ /* ============ Sidebar ============ */.sidebar { display: flex; flex-direction: column; background: var(--bg-panel); border-right: 1px solid var(--border); min-height: 0; overflow: hidden;}.sidebar__head { padding: var(--sp-3) var(--sp-3) var(--sp-2); flex-shrink: 0;}.brand { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-2); border-radius: var(--r-md); transition: background 0.12s ease;}.brand:hover { background: var(--bg-hover); }.brand__logo { width: 34px; height: 34px; border-radius: var(--r-md); background: #fff; padding: 2px; object-fit: contain; box-shadow: var(--shadow-xs); flex-shrink: 0;}.brand__text { min-width: 0; line-height: 1.15; }.brand__name { font-size: 14.5px; font-weight: 650; letter-spacing: -0.01em; color: var(--text);}.brand__slogan { font-size: 11px; color: var(--text-muted); margin-top: 2px; font-weight: 450;}.sidebar__section-label { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); padding: 0 var(--sp-3); margin: var(--sp-4) 0 var(--sp-2); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); flex-shrink: 0;}.sidebar__foot { padding: var(--sp-2) var(--sp-3) var(--sp-3); border-top: 1px solid var(--divider); flex-shrink: 0;}.btn-logout { width: 100%; display: flex; align-items: center; gap: var(--sp-2); padding: 7px 10px; border: none; border-radius: var(--r-md); background: transparent; color: var(--text-muted); cursor: pointer; font-size: 12.5px; font-weight: 500; transition: background 0.12s ease, color 0.12s ease;}.btn-logout:hover { background: var(--bg-hover); color: var(--danger); }.btn-logout svg { width: 14px; height: 14px; opacity: 0.85; }
@@ -0,0 +1,88 @@
1
+ /* ================================================================
2
+ * 03-markdown.js
3
+ * - Markdown rendering, bubble builder, system messages
4
+ * - Tool call XML extraction / rendering
5
+ * - Links open in new tab
6
+ * ================================================================ */
7
+
8
+ function ensureLinksOpenNewTab(el) {
9
+ try {
10
+ if (!el) return;
11
+ hydrateAttachmentImagesInBubble(el);
12
+ const links = el.querySelectorAll ? el.querySelectorAll('a[href]') : [];
13
+ links.forEach((a) => {
14
+ const href = String(a.getAttribute('href') || '');
15
+ if (href.indexOf('/api/attachments/') === 0) {
16
+ try { a.removeAttribute('target'); a.removeAttribute('rel'); } catch (_) {}
17
+ return;
18
+ }
19
+ try { a.setAttribute('target', '_blank'); a.setAttribute('rel', 'noopener noreferrer'); } catch (_) {}
20
+ });
21
+ } catch (_) {}
22
+ }
23
+
24
+ // ── Streaming text update ──────────────────────────────────────────────
25
+ let _streamBubbleWrap = null; // holds the current streaming agent bubble
26
+ let _streamConsumedLen = 0; // text offset — advanced ONLY at tool-start boundaries
27
+ let _streamLatestFullLen = 0; // length of the most recent full text seen (for split point)
28
+ let _streamLastScrollLnCount = -1; // \n count at last scroll trigger — 新增一行才滚动,避免逐字下拉
29
+
30
+ function getOrCreateStreamBubble(at) {
31
+ if (_streamBubbleWrap && _streamBubbleWrap.parentNode) {
32
+ return _streamBubbleWrap;
33
+ }
34
+ _streamBubbleWrap = null;
35
+ const wrap = document.createElement('div');
36
+ wrap.className = 'msg-row msg-agent';
37
+ const col = document.createElement('div');
38
+ col.className = 'msg-col';
39
+ const meta = document.createElement('div');
40
+ meta.className = 'msg-meta';
41
+ meta.textContent = '助手 · 输出中…';
42
+ const b = document.createElement('div');
43
+ b.className = 'bubble bubble-agent bubble-streaming';
44
+ b.innerHTML = '<p><em>正在输出…</em></p>';
45
+ col.appendChild(meta);
46
+ col.appendChild(b);
47
+ wrap.appendChild(col);
48
+ log.appendChild(wrap);
49
+ scrollLog();
50
+ _streamBubbleWrap = wrap;
51
+ _streamLastScrollLnCount = -1; // 新气泡开始,重置行计数器
52
+ return wrap;
53
+ }
54
+
55
+ /**
56
+ * Return the portion of fullText starting from _streamConsumedLen.
57
+ * Within a single round this returns the *full* text for that round
58
+ * (so progressive text_delta events accumulate correctly).
59
+ * Across rounds the offset prevents repeating earlier content.
60
+ */
61
+ function _streamDeltaText(fullText) {
62
+ const t = String(fullText || '');
63
+ if (_streamConsumedLen <= 0 || _streamConsumedLen >= t.length) return t;
64
+ return t.substring(_streamConsumedLen);
65
+ }
66
+
67
+ /** 仅返回尚未通过消费偏移写出的后缀;已全文应用则 ''(避免 REST/text_done 重复整段)。 */
68
+ function _streamUnconsumedSuffix(fullText) {
69
+ const t = String(fullText || '');
70
+ if (!t) return '';
71
+ if (_streamConsumedLen <= 0) return t;
72
+ if (_streamConsumedLen >= t.length) return '';
73
+ return t.substring(_streamConsumedLen);
74
+ }
75
+
76
+ /**
77
+ * Track the latest full-text length — does NOT advance _streamConsumedLen.
78
+ * Used by splitStreamBubbleAtToolStart to know where the current round ends.
79
+ */
80
+ function _noteStreamFullText(fullText) {
81
+ const t = String(fullText || '');
82
+ if (t.length > _streamLatestFullLen) _streamLatestFullLen = t.length;
83
+ }
84
+
85
+ /**
86
+ * Advance _streamConsumedLen (for use in fallback handlers where we know
87
+ * the text has been fully displayed and must not repeat).
88
+ */
@@ -0,0 +1,119 @@
1
+ function _advanceStreamConsumedLen(fullText) {
2
+ const t = String(fullText || '');
3
+ if (t.length > _streamConsumedLen) _streamConsumedLen = t.length;
4
+ if (t.length > _streamLatestFullLen) _streamLatestFullLen = t.length;
5
+ }
6
+
7
+ function updateStreamBubbleText(fullText) {
8
+ // Don't create/render streaming bubbles for whitespace-only content
9
+ if (!(fullText || '').trim()) {
10
+ // If a streaming bubble was already created, remove it
11
+ if (_streamBubbleWrap && _streamBubbleWrap.parentNode) {
12
+ _streamBubbleWrap.parentNode.removeChild(_streamBubbleWrap);
13
+ }
14
+ _streamBubbleWrap = null;
15
+ _streamConsumedLen = 0;
16
+ _streamLatestFullLen = 0;
17
+ return;
18
+ }
19
+ const wrap = getOrCreateStreamBubble(Date.now());
20
+ if (!wrap) return;
21
+ const b = wrap.querySelector('.bubble');
22
+ if (!b) return;
23
+ // Show text from round-start to current (incremental build-up within round)
24
+ const roundText = _streamDeltaText(fullText);
25
+ _noteStreamFullText(fullText); // record length for split, do NOT advance consumed
26
+ if (!roundText && !_streamConsumedLen) return;
27
+ try {
28
+ b.innerHTML = buildAgentBubbleInnerHtml(roundText, [], null);
29
+ ensureLinksOpenNewTab(b);
30
+ } catch (_) {
31
+ b.textContent = roundText;
32
+ }
33
+ // ── 新增一行才触发滚动,避免同一行内逐字下拉 ──
34
+ const lnCount = (roundText.match(/\n/g) || []).length;
35
+ if (lnCount > _streamLastScrollLnCount) {
36
+ scrollLog();
37
+ _streamLastScrollLnCount = lnCount;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Finalize the streaming bubble (remove streaming-indicator, render final content).
43
+ * Returns the bubble wrap element or null.
44
+ */
45
+ function finalizeStreamBubble(text) {
46
+ const wrap = _streamBubbleWrap;
47
+ _streamBubbleWrap = null;
48
+ if (!wrap || !wrap.parentNode) return null;
49
+ const b = wrap.querySelector('.bubble');
50
+ if (!b) return null;
51
+ // Remove whitespace-only bubbles
52
+ if (!(text || '').trim()) {
53
+ wrap.parentNode.removeChild(wrap);
54
+ _streamConsumedLen = 0;
55
+ _streamLatestFullLen = 0;
56
+ return null;
57
+ }
58
+ b.classList.remove('bubble-streaming');
59
+ const roundText = _streamDeltaText(text);
60
+ _noteStreamFullText(text); // record length, do NOT advance consumed
61
+ try {
62
+ // 时间轴拆分:正文只在气泡内,工具由独立 system-tools 块展示(与会话历史一致)
63
+ b.innerHTML = buildAgentBubbleInnerHtml(roundText || '', [], null);
64
+ ensureLinksOpenNewTab(b);
65
+ } catch (_) {
66
+ b.textContent = roundText || '';
67
+ }
68
+ const meta = wrap.querySelector('.msg-meta');
69
+ if (meta) {
70
+ const ts = formatBubbleTime(Date.now());
71
+ meta.textContent = ts ? '助手 · ' + ts : '助手';
72
+ }
73
+ const col = wrap.querySelector('.msg-col');
74
+ if (col && typeof appendBubbleTtsBar === 'function') {
75
+ appendBubbleTtsBar(col, text, { role: 'agent' });
76
+ }
77
+ scrollLog();
78
+ return wrap;
79
+ }
80
+
81
+ /**
82
+ * Split the streaming bubble at the current point.
83
+ * Called when a tool_start WS event arrives — the current round's text
84
+ * is finalized; offsets reset because each LLM round sends a fresh string
85
+ * (not cumulative across tool rounds).
86
+ */
87
+ function splitStreamBubbleAtToolStart() {
88
+ var wrap = _streamBubbleWrap;
89
+ if (wrap && wrap.parentNode) {
90
+ var b = wrap.querySelector('.bubble');
91
+ if (b) {
92
+ var txt = '';
93
+ try {
94
+ txt = (b.textContent || '').trim();
95
+ } catch (_) {}
96
+ var placeholderish = !txt || /^正在输出/.test(txt);
97
+ if (placeholderish) {
98
+ try {
99
+ wrap.parentNode.removeChild(wrap);
100
+ } catch (_) {}
101
+ } else {
102
+ b.classList.remove('bubble-streaming');
103
+ var meta = wrap.querySelector('.msg-meta');
104
+ if (meta) {
105
+ var ts = formatBubbleTime(Date.now());
106
+ meta.textContent = ts ? '助手 · ' + ts : '助手';
107
+ }
108
+ var col = wrap.querySelector('.msg-col');
109
+ if (col && typeof appendBubbleTtsBar === 'function') {
110
+ appendBubbleTtsBar(col, txt, { role: 'agent' });
111
+ }
112
+ }
113
+ }
114
+ }
115
+ _streamBubbleWrap = null;
116
+ _streamConsumedLen = 0;
117
+ _streamLatestFullLen = 0;
118
+ scrollLog();
119
+ }