@newsails/veil-cli 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/.veil/agents/analyst/AGENT.md +21 -0
  2. package/.veil/agents/analyst/agent.json +23 -0
  3. package/.veil/agents/assistant/AGENT.md +15 -0
  4. package/.veil/agents/assistant/agent.json +19 -0
  5. package/.veil/agents/coder/AGENT.md +18 -0
  6. package/.veil/agents/coder/agent.json +19 -0
  7. package/.veil/agents/hello/AGENT.md +5 -0
  8. package/.veil/agents/hello/agent.json +13 -0
  9. package/.veil/agents/writer/AGENT.md +12 -0
  10. package/.veil/agents/writer/agent.json +17 -0
  11. package/.veil/memory/MEMORY.md +343 -0
  12. package/.veil/memory/agents/analyst/MEMORY.md +55 -0
  13. package/.veil/memory/agents/hello/MEMORY.md +12 -0
  14. package/.veil/runtime.pid +1 -0
  15. package/.veil/settings.json +10 -0
  16. package/.veil-studio/studio.db +0 -0
  17. package/.veil-studio/studio.db-shm +0 -0
  18. package/.veil-studio/studio.db-wal +0 -0
  19. package/PLAN/01-vision.md +26 -0
  20. package/PLAN/02-tech-stack.md +94 -0
  21. package/PLAN/03-agents.md +232 -0
  22. package/PLAN/04-runtime.md +171 -0
  23. package/PLAN/05-tools.md +211 -0
  24. package/PLAN/06-communication.md +243 -0
  25. package/PLAN/07-storage.md +218 -0
  26. package/PLAN/08-api-cli.md +153 -0
  27. package/PLAN/09-permissions.md +108 -0
  28. package/PLAN/10-ably.md +105 -0
  29. package/PLAN/11-file-formats.md +442 -0
  30. package/PLAN/12-folder-structure.md +205 -0
  31. package/PLAN/13-operations.md +212 -0
  32. package/PLAN/README.md +23 -0
  33. package/README.md +128 -0
  34. package/REPORT.md +174 -0
  35. package/TODO.md +45 -0
  36. package/ai-tests/FRONTEND_PROMPT.md +220 -0
  37. package/ai-tests/Research & Planning.md +814 -0
  38. package/ai-tests/prompt-001-basic-api.md +230 -0
  39. package/ai-tests/prompt-002-basic-flows.md +230 -0
  40. package/ai-tests/prompt-003-agent-behaviors.md +220 -0
  41. package/api/middleware.js +60 -0
  42. package/api/routes/agents.js +193 -0
  43. package/api/routes/chat.js +93 -0
  44. package/api/routes/completions.js +122 -0
  45. package/api/routes/daemons.js +80 -0
  46. package/api/routes/memory.js +169 -0
  47. package/api/routes/models.js +40 -0
  48. package/api/routes/remote-methods.js +74 -0
  49. package/api/routes/sessions.js +208 -0
  50. package/api/routes/settings.js +108 -0
  51. package/api/routes/system.js +50 -0
  52. package/api/routes/tasks.js +270 -0
  53. package/api/server.js +120 -0
  54. package/cli/formatter.js +70 -0
  55. package/cli/index.js +443 -0
  56. package/cli/parser.js +113 -0
  57. package/config/config.json +10 -0
  58. package/config/models.json +6826 -0
  59. package/core/agent.js +329 -0
  60. package/core/cancel.js +38 -0
  61. package/core/compaction.js +176 -0
  62. package/core/events.js +13 -0
  63. package/core/loop.js +564 -0
  64. package/core/memory.js +51 -0
  65. package/core/prompt.js +185 -0
  66. package/core/queue.js +96 -0
  67. package/core/registry.js +291 -0
  68. package/core/remote-methods.js +124 -0
  69. package/core/router.js +386 -0
  70. package/core/running-sessions.js +18 -0
  71. package/docs/api/01-system.md +84 -0
  72. package/docs/api/02-agents.md +374 -0
  73. package/docs/api/03-chat.md +269 -0
  74. package/docs/api/04-tasks.md +470 -0
  75. package/docs/api/05-sessions.md +444 -0
  76. package/docs/api/06-daemons.md +142 -0
  77. package/docs/api/07-memory.md +186 -0
  78. package/docs/api/08-settings.md +133 -0
  79. package/docs/api/09-models.md +119 -0
  80. package/docs/api/09-websocket.md +350 -0
  81. package/docs/api/10-completions.md +134 -0
  82. package/docs/api/README.md +116 -0
  83. package/docs/guide/01-quickstart.md +220 -0
  84. package/docs/guide/02-folder-structure.md +185 -0
  85. package/docs/guide/03-configuration.md +252 -0
  86. package/docs/guide/04-agents.md +267 -0
  87. package/docs/guide/05-cli.md +290 -0
  88. package/docs/guide/06-tools.md +643 -0
  89. package/docs/guide/07-permissions.md +236 -0
  90. package/docs/guide/08-memory.md +139 -0
  91. package/docs/guide/09-multi-agent.md +271 -0
  92. package/docs/guide/10-daemons.md +226 -0
  93. package/docs/guide/README.md +53 -0
  94. package/docs/index.html +623 -0
  95. package/examples/README.md +151 -0
  96. package/examples/agents/assistant/AGENT.md +31 -0
  97. package/examples/agents/assistant/SOUL.md +9 -0
  98. package/examples/agents/assistant/agent.json +74 -0
  99. package/examples/agents/hello/AGENT.md +15 -0
  100. package/examples/agents/hello/agent.json +14 -0
  101. package/examples/agents/monitor/AGENT.md +51 -0
  102. package/examples/agents/monitor/agent.json +33 -0
  103. package/examples/agents/monitor/heartbeats/monitor.md +24 -0
  104. package/examples/agents/orchestrator/AGENT.md +70 -0
  105. package/examples/agents/orchestrator/agent.json +30 -0
  106. package/examples/agents/researcher/AGENT.md +52 -0
  107. package/examples/agents/researcher/agent.json +49 -0
  108. package/examples/agents/researcher/skills/web-research.md +28 -0
  109. package/examples/skills/code-review.md +72 -0
  110. package/examples/skills/summarise.md +59 -0
  111. package/examples/skills/web-research.md +42 -0
  112. package/examples/tools/word-count/index.js +27 -0
  113. package/examples/tools/word-count/tool.json +18 -0
  114. package/infrastructure/database.js +563 -0
  115. package/infrastructure/scheduler.js +122 -0
  116. package/llm/client.js +206 -0
  117. package/migrations/001-initial.sql +121 -0
  118. package/migrations/002-debuggability.sql +13 -0
  119. package/migrations/003-drop-orphaned-columns.sql +72 -0
  120. package/migrations/004-session-message-token-fields.sql +78 -0
  121. package/migrations/005-session-thinking.sql +5 -0
  122. package/package.json +30 -0
  123. package/schemas/agent.json +143 -0
  124. package/schemas/settings.json +111 -0
  125. package/scripts/fetch-models.js +93 -0
  126. package/session-debug-scenario.md +248 -0
  127. package/settings/fields.js +52 -0
  128. package/system-prompts/base-core.md +7 -0
  129. package/system-prompts/environment.md +13 -0
  130. package/system-prompts/reminders/anti-drift.md +6 -0
  131. package/system-prompts/reminders/stall-recovery.md +10 -0
  132. package/system-prompts/safety-rules.md +25 -0
  133. package/system-prompts/task-heuristics.md +27 -0
  134. package/test/client.js +71 -0
  135. package/test/integration/01-health.test.js +25 -0
  136. package/test/integration/02-agents.test.js +80 -0
  137. package/test/integration/03-chat-hello.test.js +48 -0
  138. package/test/integration/04-chat-multiturn.test.js +61 -0
  139. package/test/integration/05-chat-writer.test.js +48 -0
  140. package/test/integration/06-task-basic.test.js +68 -0
  141. package/test/integration/07-task-tools.test.js +74 -0
  142. package/test/integration/08-task-code-analysis.test.js +69 -0
  143. package/test/integration/09-memory-analyst.test.js +63 -0
  144. package/test/integration/10-task-advanced.test.js +85 -0
  145. package/test/integration/11-sessions-advanced.test.js +84 -0
  146. package/test/integration/12-assistant-chat-tools.test.js +75 -0
  147. package/test/integration/13-edge-cases.test.js +99 -0
  148. package/test/integration/14-cancel.test.js +62 -0
  149. package/test/integration/15-debug.test.js +106 -0
  150. package/test/integration/16-memory-api.test.js +83 -0
  151. package/test/integration/17-settings-api.test.js +41 -0
  152. package/test/integration/18-tool-search-activation.test.js +119 -0
  153. package/test/results/.gitkeep +0 -0
  154. package/test/runner.js +206 -0
  155. package/test/smoke.js +216 -0
  156. package/tools/agent_message.js +85 -0
  157. package/tools/agent_send.js +80 -0
  158. package/tools/agent_spawn.js +44 -0
  159. package/tools/bash.js +49 -0
  160. package/tools/edit_file.js +41 -0
  161. package/tools/glob.js +64 -0
  162. package/tools/grep.js +82 -0
  163. package/tools/list_dir.js +63 -0
  164. package/tools/log_write.js +31 -0
  165. package/tools/memory_read.js +38 -0
  166. package/tools/memory_search.js +65 -0
  167. package/tools/memory_write.js +42 -0
  168. package/tools/read_file.js +48 -0
  169. package/tools/sleep.js +22 -0
  170. package/tools/task_create.js +41 -0
  171. package/tools/task_respond.js +37 -0
  172. package/tools/task_spawn.js +64 -0
  173. package/tools/task_status.js +39 -0
  174. package/tools/task_subscribe.js +37 -0
  175. package/tools/todo_read.js +26 -0
  176. package/tools/todo_write.js +38 -0
  177. package/tools/tool_activate.js +24 -0
  178. package/tools/tool_search.js +24 -0
  179. package/tools/web_fetch.js +50 -0
  180. package/tools/web_search.js +52 -0
  181. package/tools/write_file.js +28 -0
  182. package/ui/api.js +190 -0
  183. package/ui/app.js +281 -0
  184. package/ui/index.html +382 -0
  185. package/ui/views/agents.js +377 -0
  186. package/ui/views/chat.js +610 -0
  187. package/ui/views/connection.js +96 -0
  188. package/ui/views/daemons.js +129 -0
  189. package/ui/views/feed.js +194 -0
  190. package/ui/views/memory.js +263 -0
  191. package/ui/views/models.js +146 -0
  192. package/ui/views/sessions.js +314 -0
  193. package/ui/views/settings.js +142 -0
  194. package/ui/views/tasks.js +415 -0
  195. package/utils/context.js +49 -0
  196. package/utils/id.js +16 -0
  197. package/utils/models.js +88 -0
  198. package/utils/paths.js +213 -0
  199. package/utils/settings.js +172 -0
@@ -0,0 +1,610 @@
1
+ 'use strict';
2
+ window.Veil = window.Veil || {};
3
+ window.Veil.views = window.Veil.views || {};
4
+
5
+ window.Veil.views.chat = {
6
+ _sessionId: null,
7
+ _streaming: false,
8
+ _agent: null,
9
+ _currentStreamEl: null, /* .msg-assistant div being streamed into */
10
+ _currentStreamText: '', /* accumulated text for the current LLM iteration */
11
+ _lastMsgAgent: null, /* agent name of the last appended assistant bubble */
12
+ _autoScroll: true, /* false when user scrolls up during streaming */
13
+
14
+ render() {
15
+ return `
16
+ <div style="display:flex;flex-direction:column;height:100%;">
17
+
18
+ <!-- Top bar -->
19
+ <div style="flex-shrink:0;padding:10px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-wrap:wrap;background:var(--surface);">
20
+ <div style="display:flex;align-items:center;gap:8px;">
21
+ <span style="font-size:11.5px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:0.05em;">Agent</span>
22
+ <select id="chat-agent-sel" class="input" style="width:auto;padding:4px 10px;font-size:13px;background:var(--surface2);"></select>
23
+ </div>
24
+ <div style="width:1px;height:18px;background:var(--border);"></div>
25
+ <div style="display:flex;align-items:center;gap:6px;">
26
+ <span style="font-size:11.5px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:0.05em;white-space:nowrap;">Session</span>
27
+ <code id="chat-session-id" style="font-size:11.5px;color:var(--text2);font-family:'JetBrains Mono',monospace;background:var(--surface2);border:1px solid var(--border);border-radius:5px;padding:2px 8px;">(new)</code>
28
+ </div>
29
+ <div style="display:flex;gap:6px;">
30
+ <button class="btn btn-ghost btn-sm" id="btn-new-session">+ New</button>
31
+ <button class="btn btn-ghost btn-sm" id="btn-resume-session">Resume…</button>
32
+ </div>
33
+ <div style="margin-left:auto;display:flex;align-items:center;gap:10px;">
34
+ <div id="chat-usage" style="font-size:11.5px;color:var(--text3);"></div>
35
+ <div id="chat-ctx-ring"></div>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- Messages -->
40
+ <div id="chat-messages" style="flex:1;overflow-y:auto;padding:28px 0;display:flex;flex-direction:column;gap:0;"></div>
41
+
42
+ <!-- Input area -->
43
+ <div style="flex-shrink:0;padding:10px 16px 14px;background:var(--bg);">
44
+ <div style="background:var(--surface);border:1px solid var(--border2);border-radius:12px;display:flex;align-items:flex-end;gap:4px;padding:5px 6px 5px 14px;transition:border-color 0.15s;" id="chat-input-wrap">
45
+ <textarea id="chat-input" rows="1"
46
+ placeholder="Message the agent… (Shift+Enter for newline)"
47
+ style="background:transparent;border:none;outline:none;resize:none;padding:7px 0;font-size:14px;color:var(--text);font-family:inherit;line-height:1.55;flex:1;overflow-y:auto;max-height:40vh;"></textarea>
48
+ <button id="chat-send" class="btn btn-primary btn-sm" style="padding:5px 12px;flex-shrink:0;margin-bottom:2px;">↵</button>
49
+ </div>
50
+ </div>
51
+ </div>`;
52
+ },
53
+
54
+ async mount(params) {
55
+ params = params || {};
56
+ this._sessionId = null;
57
+ this._streaming = false;
58
+ this._currentStreamEl = null;
59
+ this._currentStreamText = '';
60
+ this._lastMsgAgent = null;
61
+
62
+ await this._populateAgentSelector(params.agentName);
63
+
64
+ /* Apply params from navigate() — e.g. Resume from Sessions view */
65
+ if (params.sessionId) {
66
+ this._sessionId = params.sessionId;
67
+ const sidEl = document.getElementById('chat-session-id');
68
+ if (sidEl) sidEl.textContent = params.sessionId.slice(0, 22) + (params.sessionId.length > 22 ? '…' : '');
69
+ window.Veil.utils.toast('Resumed session', 'success');
70
+ await this._loadHistory(params.sessionId);
71
+ }
72
+
73
+ const inputWrap = document.getElementById('chat-input-wrap');
74
+ const inputEl = document.getElementById('chat-input');
75
+
76
+ /* Auto-grow textarea */
77
+ const _growInput = () => {
78
+ inputEl.style.height = 'auto';
79
+ inputEl.style.height = inputEl.scrollHeight + 'px';
80
+ };
81
+ inputEl.addEventListener('input', _growInput);
82
+
83
+ /* Focus ring */
84
+ inputEl.addEventListener('focus', () => { if (inputWrap) inputWrap.style.borderColor = 'var(--accent)'; });
85
+ inputEl.addEventListener('blur', () => { if (inputWrap) inputWrap.style.borderColor = 'var(--border2)'; });
86
+
87
+ /* Smart auto-scroll: stop following when user scrolls up during streaming */
88
+ const msgsEl = document.getElementById('chat-messages');
89
+ if (msgsEl) {
90
+ msgsEl.addEventListener('scroll', () => {
91
+ if (!this._streaming) return;
92
+ const gap = msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight;
93
+ this._autoScroll = gap < 80;
94
+ });
95
+ }
96
+
97
+ document.getElementById('btn-new-session').addEventListener('click', () => {
98
+ this._sessionId = null;
99
+ this._lastMsgAgent = null;
100
+ this._autoScroll = true;
101
+ const sidEl = document.getElementById('chat-session-id');
102
+ if (sidEl) sidEl.textContent = '(new)';
103
+ document.getElementById('chat-messages').innerHTML = '';
104
+ document.getElementById('chat-usage').textContent = '';
105
+ window.Veil.utils.toast('New session started', 'success');
106
+ });
107
+
108
+ document.getElementById('btn-resume-session').addEventListener('click', async () => {
109
+ const id = prompt('Paste session ID to resume:');
110
+ if (id && id.trim()) {
111
+ this._sessionId = id.trim();
112
+ this._lastMsgAgent = null;
113
+ const sidEl = document.getElementById('chat-session-id');
114
+ if (sidEl) sidEl.textContent = id.trim().slice(0, 22) + (id.trim().length > 22 ? '…' : '');
115
+ document.getElementById('chat-messages').innerHTML = '';
116
+ window.Veil.utils.toast('Resuming session', 'success');
117
+ await this._loadHistory(id.trim());
118
+ }
119
+ });
120
+
121
+ document.getElementById('chat-send').addEventListener('click', () => this._sendMessage());
122
+
123
+ document.getElementById('chat-input').addEventListener('keydown', e => {
124
+ if (e.key === 'Enter' && !e.shiftKey) {
125
+ e.preventDefault();
126
+ this._sendMessage();
127
+ }
128
+ });
129
+ },
130
+
131
+ unmount() {
132
+ this._streaming = false;
133
+ this._agent = null;
134
+ this._lastMsgAgent = null;
135
+ },
136
+
137
+ async _populateAgentSelector(preferredAgent) {
138
+ const sel = document.getElementById('chat-agent-sel');
139
+ if (!sel) return;
140
+ try {
141
+ const data = await window.Veil.state.api.listAgents();
142
+ const agents = (data.agents || []).filter(a => a.modes && a.modes.includes('chat'));
143
+ if (agents.length === 0) {
144
+ sel.innerHTML = '<option value="">No chat-enabled agents</option>';
145
+ } else {
146
+ const preferred = preferredAgent || window.Veil.state.currentAgent;
147
+ sel.innerHTML = agents.map(a =>
148
+ `<option value="${window.Veil.utils.esc(a.name)}" ${a.name === preferred ? 'selected' : ''}>${window.Veil.utils.esc(a.name)}</option>`
149
+ ).join('');
150
+ }
151
+ this._agent = sel.value;
152
+ sel.addEventListener('change', () => {
153
+ this._agent = sel.value;
154
+ this._sessionId = null;
155
+ this._lastMsgAgent = null;
156
+ document.getElementById('chat-session-id').textContent = '(new)';
157
+ document.getElementById('chat-messages').innerHTML = '';
158
+ });
159
+ } catch (err) {
160
+ sel.innerHTML = `<option value="">Error loading agents</option>`;
161
+ }
162
+ },
163
+
164
+ async _sendMessage() {
165
+ const input = document.getElementById('chat-input');
166
+ const sendBtn = document.getElementById('chat-send');
167
+ const agentSel = document.getElementById('chat-agent-sel');
168
+ if (!input || this._streaming) return;
169
+
170
+ const message = input.value.trim();
171
+ if (!message) return;
172
+
173
+ const agentName = agentSel?.value || this._agent;
174
+ if (!agentName) { window.Veil.utils.toast('Select an agent first', 'warn'); return; }
175
+ this._agent = agentName;
176
+
177
+ input.value = '';
178
+ input.style.height = 'auto'; /* reset input height after clear */
179
+ this._streaming = true;
180
+ this._autoScroll = true; /* always follow on new send */
181
+ this._currentStreamEl = null;
182
+ this._currentStreamText = '';
183
+ sendBtn.disabled = true;
184
+ sendBtn.innerHTML = window.Veil.utils.spinner() + ' …';
185
+
186
+ this._appendUserMessage(message);
187
+
188
+ /* Create bubble immediately with thinking indicator */
189
+ this._currentStreamEl = this._createAssistantBubble();
190
+ const thinkEl = this._currentStreamEl?.querySelector('.msg-text');
191
+ if (thinkEl) thinkEl.innerHTML = '<span class="thinking-dots">Thinking</span>';
192
+
193
+ const body = { message };
194
+ if (this._sessionId) body.sessionId = this._sessionId;
195
+
196
+ const done = () => {
197
+ /* clean up any leftover streaming cursor */
198
+ if (this._currentStreamEl) {
199
+ this._currentStreamEl.querySelector('.msg-text')?.classList.remove('streaming');
200
+ this._currentStreamEl = null;
201
+ }
202
+ this._streaming = false;
203
+ if (sendBtn) { sendBtn.disabled = false; sendBtn.textContent = '↵ Send'; }
204
+ this._scrollToBottom();
205
+ };
206
+
207
+ await window.Veil.state.api.chatStream(agentName, body, {
208
+
209
+ /* inference.chunk — streaming token fragment */
210
+ onChunk: (data) => {
211
+ /* bubble already created before send; clear thinking indicator on first chunk */
212
+ if (!this._currentStreamEl) this._currentStreamEl = this._createAssistantBubble();
213
+ this._currentStreamText += (data.content || '');
214
+ const textEl = this._currentStreamEl.querySelector('.msg-text');
215
+ if (textEl) {
216
+ /* render markdown in real-time — marked handles partial text gracefully */
217
+ this._renderMarkdown(textEl, this._currentStreamText);
218
+ textEl.classList.add('streaming');
219
+ }
220
+ this._scrollToBottom();
221
+ },
222
+
223
+ /* inference.tool — tool name known early, before args are ready */
224
+ onToolName: (data) => {
225
+ /* bubble already created; clear thinking indicator if still showing */
226
+ if (!this._currentStreamEl) this._currentStreamEl = this._createAssistantBubble();
227
+ const textEl = this._currentStreamEl.querySelector('.msg-text');
228
+ if (textEl?.querySelector('.thinking-dots')) { textEl.innerHTML = ''; textEl.style.display = 'none'; }
229
+ const toolsEl = this._currentStreamEl.querySelector('.msg-tools');
230
+ if (!toolsEl) return;
231
+ const ind = document.createElement('div');
232
+ ind.className = 'tool-loading';
233
+ ind.dataset.toolName = data.name || '';
234
+ ind.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 0;font-size:12.5px;color:var(--yellow);';
235
+ ind.innerHTML = `<span class="spin" style="display:inline-block;width:10px;height:10px;border:2px solid var(--yellow-dim);border-top-color:var(--yellow);border-radius:50%;"></span>⚙ ${window.Veil.utils.esc(data.name)}…`;
236
+ toolsEl.appendChild(ind);
237
+ this._scrollToBottom();
238
+ },
239
+
240
+ /* message — canonical session-API-compatible message object */
241
+ onMessage: (data) => {
242
+ const U = window.Veil.utils;
243
+
244
+ if (data.role === 'assistant') {
245
+ /* Finalize (or create) the assistant bubble */
246
+ if (!this._currentStreamEl) this._currentStreamEl = this._createAssistantBubble();
247
+ const textEl = this._currentStreamEl.querySelector('.msg-text');
248
+ const toolsEl = this._currentStreamEl.querySelector('.msg-tools');
249
+
250
+ /* Stop streaming cursor and render as markdown */
251
+ if (textEl) {
252
+ textEl.classList.remove('streaming');
253
+ if (data.content) {
254
+ this._renderMarkdown(textEl, data.content);
255
+ /* store raw md on parent wrap for copy button */
256
+ const wrap = this._currentStreamEl.closest('.msg-bubble-wrap');
257
+ if (wrap) wrap.dataset.md = data.content;
258
+ } else if (!this._currentStreamText) {
259
+ /* tool-call turn with no prose — hide the empty text area */
260
+ textEl.style.display = 'none';
261
+ }
262
+ }
263
+
264
+ /* Replace loading indicators with real tool-call blocks */
265
+ if (toolsEl) {
266
+ toolsEl.querySelectorAll('.tool-loading').forEach(e => e.remove());
267
+ if (data.tool_calls && data.tool_calls.length) {
268
+ data.tool_calls.forEach(tc => {
269
+ const name = tc.function?.name || tc.name || '?';
270
+ let argsStr = '';
271
+ try {
272
+ const args = tc.function?.arguments ? JSON.parse(tc.function.arguments) : null;
273
+ argsStr = args ? JSON.stringify(args, null, 2) : (tc.function?.arguments || '');
274
+ } catch { argsStr = tc.function?.arguments || ''; }
275
+
276
+ const block = document.createElement('div');
277
+ block.className = 'tool-block';
278
+ block.dataset.callId = tc.id || '';
279
+ block.style.cssText = 'padding:7px 10px;margin-top:6px;';
280
+ block.innerHTML = `
281
+ <div class="tool-header" style="display:flex;align-items:center;gap:8px;cursor:pointer;">
282
+ <span style="color:var(--yellow);font-weight:600;font-size:13px;">⚙ ${U.esc(name)}</span>
283
+ <span class="tool-toggle" style="color:var(--text3);font-size:11.5px;margin-left:auto;">▸ show</span>
284
+ </div>
285
+ <div class="tool-body" style="display:none;margin-top:6px;">
286
+ ${argsStr ? `<pre style="color:var(--text2);font-size:12px;padding:6px 8px;background:var(--bg);border-radius:5px;overflow-x:auto;">${U.esc(argsStr.slice(0, 800))}</pre>` : ''}
287
+ <div class="tool-result" style="display:none;margin-top:5px;">
288
+ <pre class="tool-result-pre" style="color:var(--green);font-size:12px;padding:6px 8px;background:var(--bg);border-radius:5px;overflow-x:auto;"></pre>
289
+ </div>
290
+ </div>`;
291
+ block.querySelector('.tool-header').addEventListener('click', () => {
292
+ const body = block.querySelector('.tool-body');
293
+ const tog = block.querySelector('.tool-toggle');
294
+ const open = body.style.display === 'none';
295
+ body.style.display = open ? 'block' : 'none';
296
+ tog.textContent = open ? '▾ hide' : '▸ show';
297
+ });
298
+ toolsEl.appendChild(block);
299
+ });
300
+ }
301
+ }
302
+
303
+ /* Per-iteration token usage footer */
304
+ if (data.tokenUsage) {
305
+ const footer = document.createElement('div');
306
+ footer.style.cssText = 'font-size:11.5px;color:var(--text3);margin-top:6px;';
307
+ footer.textContent = U.fmtTokens(data.tokenUsage.input, data.tokenUsage.output, data.tokenUsage.cost);
308
+ this._currentStreamEl.appendChild(footer);
309
+ }
310
+
311
+ this._currentStreamEl = null;
312
+ this._currentStreamText = '';
313
+ this._scrollToBottom();
314
+
315
+ } else if (data.role === 'tool') {
316
+ /* Match to the tool-call block by tool_call_id */
317
+ const callId = data.tool_call_id || '';
318
+ const msgBlock = document.getElementById('chat-messages');
319
+ let matched = callId ? msgBlock?.querySelector(`[data-call-id="${CSS.escape(callId)}"]`) : null;
320
+
321
+ if (matched) {
322
+ /* Inject result but keep block collapsed — user can expand */
323
+ const resultDiv = matched.querySelector('.tool-result');
324
+ const resultPre = matched.querySelector('.tool-result-pre');
325
+ const tog = matched.querySelector('.tool-toggle');
326
+ if (resultDiv) resultDiv.style.display = 'block';
327
+ if (resultPre) resultPre.textContent = (data.content || '').slice(0, 2000);
328
+ /* update toggle label to signal result is ready */
329
+ if (tog) tog.textContent = '▸ result';
330
+ } else {
331
+ /* No matching block — render as standalone tool result */
332
+ const U = window.Veil.utils;
333
+ const el = document.getElementById('chat-messages');
334
+ if (el) {
335
+ const wrap = document.createElement('div');
336
+ wrap.style.cssText = 'max-width:82%;';
337
+ wrap.innerHTML = `
338
+ <div class="msg-tool" style="padding:8px 12px;">
339
+ <div style="font-size:11.5px;color:var(--yellow);margin-bottom:5px;font-weight:600;">tool result</div>
340
+ <pre style="font-size:12.5px;color:var(--green);">${U.esc((data.content || '').slice(0, 2000))}</pre>
341
+ </div>`;
342
+ el.appendChild(wrap);
343
+ }
344
+ }
345
+ this._scrollToBottom();
346
+ }
347
+ },
348
+
349
+ /* done — turn complete */
350
+ onDone: (data) => {
351
+ /* Update session ID from done.session.id */
352
+ if (data.session?.id) {
353
+ this._sessionId = data.session.id;
354
+ const sidEl = document.getElementById('chat-session-id');
355
+ if (sidEl) {
356
+ const sid = data.session.id;
357
+ sidEl.textContent = sid.slice(0, 22) + (sid.length > 22 ? '…' : '');
358
+ }
359
+ }
360
+ /* Update turn-level usage in the top bar */
361
+ const usageEl = document.getElementById('chat-usage');
362
+ if (usageEl && data.tokenUsage) {
363
+ const U = window.Veil.utils;
364
+ const u = data.tokenUsage;
365
+ const parts = [U.fmtTokens(u.input, u.output, u.cost)];
366
+ if (data.iterations) parts.push(data.iterations + ' iter');
367
+ if (data.durationMs) parts.push(U.formatMs(data.durationMs));
368
+ usageEl.textContent = parts.join(' · ');
369
+ }
370
+ /* Refresh context ring from live session data */
371
+ if (this._sessionId) this._updateContextRing(this._sessionId);
372
+ done();
373
+ },
374
+
375
+ /* error */
376
+ onError: (data) => {
377
+ if (!this._currentStreamEl) this._currentStreamEl = this._createAssistantBubble();
378
+ const textEl = this._currentStreamEl.querySelector('.msg-text');
379
+ if (textEl) {
380
+ textEl.classList.remove('streaming');
381
+ textEl.style.display = '';
382
+ textEl.style.color = 'var(--red)';
383
+ textEl.textContent = 'Error: ' + (data.error || JSON.stringify(data));
384
+ }
385
+ this._currentStreamEl = null;
386
+ done();
387
+ },
388
+ });
389
+ },
390
+
391
+ _scrollToBottom(force) {
392
+ const el = document.getElementById('chat-messages');
393
+ if (!el) return;
394
+ if (force || this._autoScroll) el.scrollTop = el.scrollHeight;
395
+ },
396
+
397
+ /* Render markdown into a text element */
398
+ _renderMarkdown(el, text) {
399
+ if (window.marked && window.DOMPurify) {
400
+ el.className = 'msg-text msg-md';
401
+ el.style.whiteSpace = '';
402
+ el.innerHTML = DOMPurify.sanitize(marked.parse(text));
403
+ } else {
404
+ el.className = 'msg-text';
405
+ el.style.whiteSpace = 'pre-wrap';
406
+ el.textContent = text;
407
+ }
408
+ },
409
+
410
+ /* Fetch session and update context ring in the top bar */
411
+ async _updateContextRing(sessionId) {
412
+ if (!sessionId) return;
413
+ try {
414
+ const data = await window.Veil.state.api.getSession(sessionId);
415
+ const sess = data.session;
416
+ const el = document.getElementById('chat-ctx-ring');
417
+ if (!el || !sess) return;
418
+ const U = window.Veil.utils;
419
+ const tip = [
420
+ { label: 'Context', value: (sess.context_size || 0).toLocaleString() + (sess.context_size_limit ? ' / ' + sess.context_size_limit.toLocaleString() : '') + ' tokens' },
421
+ { label: 'Total in', value: (sess.total_input_tokens || 0).toLocaleString() + ' tokens' },
422
+ { label: 'Total out', value: (sess.total_output_tokens || 0).toLocaleString() + ' tokens' },
423
+ { label: 'Total cached', value: (sess.total_cache_tokens || 0).toLocaleString() + ' tokens' },
424
+ { label: 'Session cost', value: U.formatCost(sess.cost) || '$0' },
425
+ { label: 'Messages', value: sess.message_count || 0 },
426
+ ];
427
+ el.innerHTML = U.contextRing(sess.context_size, sess.context_size_limit, tip);
428
+ } catch {}
429
+ },
430
+
431
+ /* Load session history and render messages for resumed sessions */
432
+ async _loadHistory(sessionId) {
433
+ if (!sessionId) return;
434
+ const msgsEl = document.getElementById('chat-messages');
435
+ if (!msgsEl) return;
436
+ msgsEl.innerHTML = `<div style="padding:20px;color:var(--text3);display:flex;align-items:center;gap:8px;">${window.Veil.utils.spinner()} Loading history…</div>`;
437
+ try {
438
+ const data = await window.Veil.state.api.getSessionMessages(sessionId, { limit: 100 });
439
+ const msgs = data.messages || [];
440
+ msgsEl.innerHTML = '';
441
+ this._lastMsgAgent = null;
442
+ for (const msg of msgs) {
443
+ if (msg.role === 'system') continue; /* skip system prompt */
444
+ this._renderHistoricalMessage(msg);
445
+ }
446
+ /* Also update the context ring */
447
+ this._updateContextRing(sessionId);
448
+ this._scrollToBottom();
449
+ } catch (err) {
450
+ msgsEl.innerHTML = `<div style="padding:20px;color:var(--red);font-size:13px;">Failed to load history: ${window.Veil.utils.esc(err.message)}</div>`;
451
+ }
452
+ },
453
+
454
+ /* Render a single historical message into the chat */
455
+ _renderHistoricalMessage(msg) {
456
+ const U = window.Veil.utils;
457
+ if (msg.role === 'user') {
458
+ this._appendUserMessage(msg.content || '');
459
+ } else if (msg.role === 'assistant') {
460
+ const inner = this._createAssistantBubble();
461
+ if (!inner) return;
462
+ const textEl = inner.querySelector('.msg-text');
463
+ const toolsEl = inner.querySelector('.msg-tools');
464
+ if (msg.content && textEl) {
465
+ this._renderMarkdown(textEl, msg.content);
466
+ const wrap = inner.closest('.msg-bubble-wrap');
467
+ if (wrap) wrap.dataset.md = msg.content;
468
+ } else if (textEl) {
469
+ textEl.style.display = 'none';
470
+ }
471
+ /* Render tool calls compactly */
472
+ if (msg.tool_calls && toolsEl) {
473
+ const calls = typeof msg.tool_calls === 'string' ? JSON.parse(msg.tool_calls) : msg.tool_calls;
474
+ calls.forEach(tc => {
475
+ const name = tc.function?.name || tc.name || '?';
476
+ let argsStr = '';
477
+ try {
478
+ const a = tc.function?.arguments ? JSON.parse(tc.function.arguments) : null;
479
+ argsStr = a ? JSON.stringify(a, null, 2) : (tc.function?.arguments || '');
480
+ } catch { argsStr = tc.function?.arguments || ''; }
481
+ const block = document.createElement('div');
482
+ block.className = 'tool-block';
483
+ block.dataset.callId = tc.id || '';
484
+ block.style.cssText = 'padding:7px 10px;margin-top:6px;';
485
+ block.innerHTML = `
486
+ <div class="tool-header" style="display:flex;align-items:center;gap:8px;cursor:pointer;">
487
+ <span style="color:var(--yellow);font-weight:600;font-size:13px;">⚙ ${U.esc(name)}</span>
488
+ <span class="tool-toggle" style="color:var(--text3);font-size:11.5px;margin-left:auto;">▸ show</span>
489
+ </div>
490
+ <div class="tool-body" style="display:none;margin-top:6px;">
491
+ ${argsStr ? `<pre style="color:var(--text2);font-size:12px;padding:6px 8px;background:var(--bg);border-radius:5px;overflow-x:auto;">${U.esc(argsStr.slice(0, 800))}</pre>` : ''}
492
+ </div>`;
493
+ block.querySelector('.tool-header').addEventListener('click', () => {
494
+ const b = block.querySelector('.tool-body');
495
+ const t = block.querySelector('.tool-toggle');
496
+ const open = b.style.display === 'none';
497
+ b.style.display = open ? 'block' : 'none';
498
+ t.textContent = open ? '▾ hide' : '▸ show';
499
+ });
500
+ toolsEl.appendChild(block);
501
+ });
502
+ }
503
+ /* Token footer */
504
+ if (msg.input_tokens || msg.output_tokens) {
505
+ const footer = document.createElement('div');
506
+ footer.style.cssText = 'font-size:11.5px;color:var(--text3);margin-top:6px;';
507
+ footer.textContent = U.fmtTokens(msg.input_tokens, msg.output_tokens, msg.cost);
508
+ if (msg.model_key) footer.textContent += ' · ' + msg.model_key.split('/').pop();
509
+ inner.appendChild(footer);
510
+ }
511
+ } else if (msg.role === 'tool') {
512
+ /* Compact tool result — try to match to last open call block, otherwise standalone */
513
+ const msgsEl = document.getElementById('chat-messages');
514
+ const callId = msg.tool_call_id || '';
515
+ const matched = callId ? msgsEl?.querySelector(`[data-call-id="${CSS.escape(callId)}"]`) : null;
516
+ if (matched) {
517
+ const resultDiv = matched.querySelector('.tool-result') || (() => {
518
+ const d = document.createElement('div');
519
+ d.className = 'tool-result';
520
+ d.style.marginTop = '5px';
521
+ matched.querySelector('.tool-body')?.appendChild(d);
522
+ return d;
523
+ })();
524
+ const resultPre = matched.querySelector('.tool-result-pre') || (() => {
525
+ const p = document.createElement('pre');
526
+ p.className = 'tool-result-pre';
527
+ p.style.cssText = 'color:var(--green);font-size:12px;padding:6px 8px;background:var(--bg);border-radius:5px;overflow-x:auto;';
528
+ resultDiv.appendChild(p);
529
+ return p;
530
+ })();
531
+ resultDiv.style.display = 'block';
532
+ resultPre.textContent = (msg.content || '').slice(0, 2000);
533
+ const tog = matched.querySelector('.tool-toggle');
534
+ if (tog) tog.textContent = '▸ result';
535
+ }
536
+ }
537
+ },
538
+
539
+ /* Create a new assistant bubble — Claude-style: avatar + plain text, no box */
540
+ _createAssistantBubble() {
541
+ const el = document.getElementById('chat-messages');
542
+ if (!el) return null;
543
+ const U = window.Veil.utils;
544
+ const name = this._agent || 'assistant';
545
+ const initial = name.slice(0, 2).toUpperCase();
546
+ const isConsecutive = this._lastMsgAgent === name;
547
+ this._lastMsgAgent = name;
548
+
549
+ const wrap = document.createElement('div');
550
+ wrap.className = 'fade-in msg-bubble-wrap';
551
+ wrap.style.cssText = 'padding:' + (isConsecutive ? '4px 24px 14px' : '14px 24px') + ';';
552
+
553
+ if (isConsecutive) {
554
+ wrap.innerHTML = `
555
+ <div style="padding-left:40px;">
556
+ <div class="msg-inner" style="flex:1;min-width:0;">
557
+ <div class="msg-text" style="font-size:14px;line-height:1.65;color:var(--text);white-space:pre-wrap;word-break:break-word;"></div>
558
+ <div class="msg-tools" style="margin-top:8px;"></div>
559
+ </div>
560
+ </div>
561
+ <button class="msg-copy-btn" title="Copy">Copy</button>`;
562
+ } else {
563
+ wrap.innerHTML = `
564
+ <div style="display:flex;gap:12px;align-items:flex-start;">
565
+ <div class="avatar avatar-accent" style="margin-top:2px;flex-shrink:0;">${U.esc(initial)}</div>
566
+ <div class="msg-inner" style="flex:1;min-width:0;">
567
+ <div style="font-size:12px;font-weight:600;color:var(--accent);margin-bottom:7px;letter-spacing:0.01em;">${U.esc(name)}</div>
568
+ <div class="msg-text" style="font-size:14px;line-height:1.65;color:var(--text);white-space:pre-wrap;word-break:break-word;"></div>
569
+ <div class="msg-tools" style="margin-top:8px;"></div>
570
+ </div>
571
+ </div>
572
+ <button class="msg-copy-btn" title="Copy">Copy</button>`;
573
+ }
574
+
575
+ wrap.querySelector('.msg-copy-btn').addEventListener('click', () => {
576
+ const md = wrap.dataset.md || wrap.querySelector('.msg-text')?.textContent || '';
577
+ navigator.clipboard.writeText(md).then(() => {
578
+ const btn = wrap.querySelector('.msg-copy-btn');
579
+ if (btn) { btn.textContent = '✓ Copied'; setTimeout(() => { btn.textContent = 'Copy'; }, 1500); }
580
+ });
581
+ });
582
+
583
+ el.appendChild(wrap);
584
+ this._scrollToBottom();
585
+ return wrap.querySelector('.msg-inner');
586
+ },
587
+
588
+ _appendUserMessage(text) {
589
+ const el = document.getElementById('chat-messages');
590
+ if (!el) return;
591
+ const U = window.Veil.utils;
592
+ const div = document.createElement('div');
593
+ div.className = 'fade-in msg-bubble-wrap';
594
+ div.style.cssText = 'display:flex;justify-content:flex-end;padding:10px 24px;';
595
+ div.innerHTML = `
596
+ <div class="msg-user" style="max-width:70%;padding:12px 16px;position:relative;">
597
+ <div style="font-size:14px;line-height:1.65;color:var(--text);white-space:pre-wrap;word-break:break-word;">${U.esc(text)}</div>
598
+ </div>
599
+ <button class="msg-copy-btn" title="Copy">Copy</button>`;
600
+ div.querySelector('.msg-copy-btn').addEventListener('click', () => {
601
+ navigator.clipboard.writeText(text).then(() => {
602
+ const btn = div.querySelector('.msg-copy-btn');
603
+ if (btn) { btn.textContent = '✓ Copied'; setTimeout(() => { btn.textContent = 'Copy'; }, 1500); }
604
+ });
605
+ });
606
+ el.appendChild(div);
607
+ this._lastMsgAgent = null; /* reset consecutive tracking after user message */
608
+ this._scrollToBottom();
609
+ },
610
+ };