debug-agent-py 0.2.1__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.
@@ -0,0 +1,582 @@
1
+ """Embedded chat UI — single-file HTML, no external dependencies.
2
+
3
+ Ported from the .NET Debug Agent ChatPage with Python-specific branding.
4
+ Exact same CSS/JS to ensure identical SSE parsing and rendering behavior.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ _CHAT_TEMPLATE = r"""<!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ <title>Python Debug Agent</title>
16
+ <style>
17
+ :root {
18
+ --bg: #0d1117;
19
+ --surface: #161b22;
20
+ --surface-hover: #1c2128;
21
+ --border: #30363d;
22
+ --text: #e6edf3;
23
+ --text-muted: #8b949e;
24
+ --accent: #58a6ff;
25
+ --accent-hover: #79c0ff;
26
+ --green: #3fb950;
27
+ --orange: #d29922;
28
+ --red: #f85149;
29
+ --purple: #bc8cff;
30
+ --max-width: 880px;
31
+ --code-bg: #010409;
32
+ }
33
+ * { margin: 0; padding: 0; box-sizing: border-box; }
34
+ body {
35
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
36
+ background: var(--bg);
37
+ color: var(--text);
38
+ height: 100vh;
39
+ display: flex;
40
+ flex-direction: column;
41
+ }
42
+ .header {
43
+ border-bottom: 1px solid var(--border);
44
+ padding: 12px 24px;
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ background: var(--surface);
49
+ flex-shrink: 0;
50
+ }
51
+ .header-title {
52
+ font-size: 15px; font-weight: 600;
53
+ display: flex; align-items: center; gap: 8px;
54
+ }
55
+ .header-title .dot {
56
+ width: 8px; height: 8px; border-radius: 50%;
57
+ background: var(--green); box-shadow: 0 0 6px var(--green);
58
+ }
59
+ .header-info { font-size: 12px; color: var(--text-muted); }
60
+ .chat-container { flex: 1; overflow-y: auto; padding: 24px; }
61
+ .chat-messages {
62
+ max-width: var(--max-width); margin: 0 auto;
63
+ display: flex; flex-direction: column; gap: 16px;
64
+ }
65
+ .message { display: flex; gap: 12px; animation: fadeIn 0.2s ease; }
66
+ @keyframes fadeIn {
67
+ from { opacity: 0; transform: translateY(8px); }
68
+ to { opacity: 1; transform: translateY(0); }
69
+ }
70
+ .message-avatar {
71
+ width: 32px; height: 32px; border-radius: 6px;
72
+ display: flex; align-items: center; justify-content: center;
73
+ font-size: 14px; flex-shrink: 0; font-weight: 600;
74
+ }
75
+ .message.user .message-avatar { background: var(--accent); color: #fff; }
76
+ .message.agent .message-avatar { background: #6e40c9; color: #fff; }
77
+ .message-content {
78
+ flex: 1; line-height: 1.65; font-size: 14px;
79
+ overflow-wrap: break-word; word-break: break-word;
80
+ }
81
+ .message.agent .message-content {
82
+ background: var(--surface); border: 1px solid var(--border);
83
+ border-radius: 8px; padding: 16px 20px;
84
+ }
85
+
86
+ .tool-badges {
87
+ max-width: var(--max-width); margin: 0 auto;
88
+ display: flex; flex-wrap: wrap; gap: 6px;
89
+ }
90
+ .tool-badge {
91
+ display: inline-flex; align-items: center; gap: 5px;
92
+ padding: 3px 10px; border-radius: 12px;
93
+ font-size: 12px; font-family: 'SF Mono', Monaco, monospace;
94
+ border: 1px solid var(--border); background: var(--surface);
95
+ color: var(--text-muted); line-height: 1.4;
96
+ animation: fadeIn 0.2s ease;
97
+ }
98
+ .tool-badge.pending { border-color: var(--orange); color: var(--orange); }
99
+ .tool-badge.success { border-color: rgba(63,185,80,0.4); color: var(--green); }
100
+ .tool-badge.error { border-color: rgba(248,81,73,0.4); color: var(--red); }
101
+ .tool-badge .dot-icon {
102
+ width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
103
+ }
104
+ .tool-badge.pending .dot-icon {
105
+ background: var(--orange);
106
+ animation: spinFade 0.8s linear infinite;
107
+ }
108
+ .tool-badge.success .dot-icon { background: var(--green); }
109
+ .tool-badge.error .dot-icon { background: var(--red); }
110
+ @keyframes spinFade {
111
+ 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; }
112
+ }
113
+
114
+ .message-content > *:first-child { margin-top: 0; }
115
+ .message-content > *:last-child { margin-bottom: 0; }
116
+ .md p { margin: 0 0 10px; }
117
+ .md h1, .md h2, .md h3, .md h4, .md h5, .md h6 {
118
+ margin: 18px 0 8px; font-weight: 600; line-height: 1.3;
119
+ }
120
+ .md h1 { font-size: 1.4em; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
121
+ .md h2 { font-size: 1.25em; border-bottom: 1px solid var(--border); padding-bottom: 5px; }
122
+ .md h3 { font-size: 1.1em; }
123
+ .md h4 { font-size: 1em; color: var(--text-muted); }
124
+ .md ul, .md ol { margin: 6px 0 10px; padding-left: 22px; }
125
+ .md li { margin-bottom: 3px; }
126
+ .md li > ul, .md li > ol { margin: 3px 0; }
127
+ .md code {
128
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
129
+ font-size: 0.88em; background: rgba(110,118,129,0.25);
130
+ padding: 2px 5px; border-radius: 4px;
131
+ }
132
+ .md pre {
133
+ background: var(--code-bg); border: 1px solid var(--border);
134
+ border-radius: 8px; overflow: hidden; margin: 10px 0; position: relative;
135
+ }
136
+ .md pre .code-header {
137
+ display: flex; align-items: center; justify-content: space-between;
138
+ padding: 5px 12px; background: rgba(255,255,255,0.04);
139
+ border-bottom: 1px solid var(--border); font-size: 11px;
140
+ color: var(--text-muted); font-family: 'SF Mono', Monaco, monospace;
141
+ }
142
+ .md pre .copy-btn {
143
+ background: none; border: 1px solid var(--border); border-radius: 4px;
144
+ color: var(--text-muted); padding: 1px 8px; font-size: 11px; cursor: pointer;
145
+ transition: all 0.15s;
146
+ }
147
+ .md pre .copy-btn:hover { background: var(--surface-hover); color: var(--text); }
148
+ .md pre code {
149
+ display: block; background: none; padding: 10px 14px;
150
+ font-size: 13px; line-height: 1.5; overflow-x: auto;
151
+ }
152
+ .md blockquote {
153
+ margin: 8px 0; padding: 6px 14px; border-left: 3px solid var(--accent);
154
+ background: rgba(88,166,255,0.06); color: var(--text-muted);
155
+ border-radius: 0 6px 6px 0;
156
+ }
157
+ .md blockquote p { margin: 0; }
158
+ .md table {
159
+ border-collapse: collapse; width: 100%; margin: 10px 0;
160
+ font-size: 13px; display: block; overflow-x: auto;
161
+ }
162
+ .md thead { background: rgba(255,255,255,0.04); }
163
+ .md th, .md td { border: 1px solid var(--border); padding: 5px 12px; text-align: left; }
164
+ .md th { font-weight: 600; white-space: nowrap; }
165
+ .md tr:nth-child(even) { background: rgba(255,255,255,0.02); }
166
+ .md hr { border: none; border-top: 1px solid var(--border); margin: 14px 0; }
167
+ .md a { color: var(--accent); text-decoration: none; }
168
+ .md a:hover { text-decoration: underline; }
169
+ .md strong { font-weight: 600; }
170
+ .md em { font-style: italic; }
171
+
172
+ .typing { display: inline-flex; gap: 4px; padding: 4px 0; }
173
+ .typing span {
174
+ width: 6px; height: 6px; background: var(--text-muted);
175
+ border-radius: 50%; animation: bounce 1.4s ease infinite;
176
+ }
177
+ .typing span:nth-child(2) { animation-delay: 0.2s; }
178
+ .typing span:nth-child(3) { animation-delay: 0.4s; }
179
+ @keyframes bounce {
180
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
181
+ 30% { transform: translateY(-6px); opacity: 1; }
182
+ }
183
+
184
+ .input-area {
185
+ border-top: 1px solid var(--border); padding: 16px 24px;
186
+ background: var(--surface); flex-shrink: 0;
187
+ }
188
+ .input-wrapper {
189
+ max-width: var(--max-width); margin: 0 auto;
190
+ display: flex; gap: 8px; align-items: flex-end;
191
+ }
192
+ .input-field {
193
+ flex: 1; background: var(--bg); border: 1px solid var(--border);
194
+ border-radius: 8px; padding: 10px 14px; color: var(--text);
195
+ font-size: 14px; font-family: inherit; resize: none;
196
+ max-height: 120px; min-height: 42px; line-height: 1.5;
197
+ transition: border-color 0.15s;
198
+ }
199
+ .input-field:focus { outline: none; border-color: var(--accent); }
200
+ .input-field::placeholder { color: var(--text-muted); }
201
+ .send-btn {
202
+ background: var(--accent); color: #fff; border: none; border-radius: 8px;
203
+ padding: 10px 18px; font-size: 14px; font-weight: 500; cursor: pointer;
204
+ transition: background 0.15s; white-space: nowrap;
205
+ }
206
+ .send-btn:hover:not(:disabled) { background: var(--accent-hover); }
207
+ .send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
208
+
209
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
210
+ ::-webkit-scrollbar-track { background: var(--bg); }
211
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
212
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
213
+
214
+ .error-banner {
215
+ max-width: var(--max-width); margin: 0 auto; padding: 8px 12px;
216
+ background: rgba(248, 81, 73, 0.1); border: 1px solid var(--red);
217
+ border-radius: 6px; font-size: 13px; color: var(--red);
218
+ }
219
+ .system-notice {
220
+ max-width: var(--max-width); margin: 0 auto; padding: 8px 12px;
221
+ background: rgba(210, 153, 34, 0.08); border: 1px solid var(--orange);
222
+ border-radius: 6px; font-size: 12px; color: var(--orange); text-align: center;
223
+ }
224
+ </style>
225
+ </head>
226
+ <body>
227
+ <div class="header">
228
+ <div class="header-title">
229
+ <span class="dot"></span>
230
+ Python Debug Agent
231
+ </div>
232
+ <div class="header-info">
233
+ Connected to live runtime &middot; <a href="#" id="clear-link" style="color:var(--text-muted);text-decoration:none;">Clear</a>
234
+ </div>
235
+ </div>
236
+
237
+ <div class="chat-container" id="chat-container">
238
+ <div class="chat-messages" id="chat-messages">
239
+ <div class="message agent">
240
+ <div class="message-avatar">AI</div>
241
+ <div class="message-content md"><p>Hi! I'm your <strong>Python Debug Agent</strong>.</p><p>I can inspect memory, GC, threads, modules, database connections, async tasks, routes, HTTP requests, and more. What's the issue you're debugging?</p></div>
242
+ </div>
243
+ </div>
244
+ </div>
245
+
246
+ <div class="input-area">
247
+ <div class="input-wrapper">
248
+ <textarea class="input-field" id="input" placeholder="Describe the issue... (Shift+Enter for newline)" rows="1"></textarea>
249
+ <button class="send-btn" id="send">Send</button>
250
+ </div>
251
+ </div>
252
+
253
+ <script>
254
+ const sessionId = 'session-' + Math.random().toString(36).substring(2, 11);
255
+ const BASE_PATH = window.location.pathname.replace(/\/+$/, '');
256
+
257
+ const chatMessages = document.getElementById('chat-messages');
258
+ const chatContainer = document.getElementById('chat-container');
259
+ const input = document.getElementById('input');
260
+ const sendBtn = document.getElementById('send');
261
+ const clearLink = document.getElementById('clear-link');
262
+
263
+ let isStreaming = false;
264
+ let currentAgentContent = null;
265
+ let renderTimer = null;
266
+ let toolBadgesContainer = null;
267
+
268
+ input.addEventListener('input', () => {
269
+ input.style.height = 'auto';
270
+ input.style.height = Math.min(input.scrollHeight, 120) + 'px';
271
+ });
272
+ input.addEventListener('keydown', (e) => {
273
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
274
+ });
275
+ sendBtn.addEventListener('click', sendMessage);
276
+ clearLink.addEventListener('click', (e) => {
277
+ e.preventDefault();
278
+ if (confirm('Clear conversation history?')) {
279
+ fetch(BASE_PATH + '/api/clear', {
280
+ method: 'POST', headers: {'Content-Type':'application/json'},
281
+ body: JSON.stringify({sessionId: sessionId})
282
+ }).then(() => {
283
+ chatMessages.innerHTML = '';
284
+ addAgentMessage('Conversation cleared. What can I help you debug?');
285
+ });
286
+ }
287
+ });
288
+
289
+ function autoScroll() {
290
+ chatContainer.scrollTop = chatContainer.scrollHeight;
291
+ }
292
+
293
+ function escapeHtml(text) {
294
+ const div = document.createElement('div');
295
+ div.textContent = text;
296
+ return div.innerHTML;
297
+ }
298
+
299
+ function renderMarkdown(text) {
300
+ if (!text) return '';
301
+ const codeBlocks = [];
302
+ let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (m, lang, code) => {
303
+ const idx = codeBlocks.length;
304
+ codeBlocks.push({ lang: lang || '', code: code.replace(/\n$/, '') });
305
+ return '\x00CODEBLOCK' + idx + '\x00';
306
+ });
307
+
308
+ processed = escapeHtml(processed);
309
+
310
+ const lines = processed.split('\n');
311
+ const result = [];
312
+ let inList = false, listType = '';
313
+ let inTable = false, tableRows = [];
314
+ let inBlockquote = false;
315
+
316
+ function closeList() { if (inList) { result.push('</' + listType + '>'); inList = false; } }
317
+ function closeTable() { if (inTable) { result.push(renderTable(tableRows)); tableRows = []; inTable = false; } }
318
+ function closeBlockquote() { if (inBlockquote) { result.push('</blockquote>'); inBlockquote = false; } }
319
+
320
+ for (let i = 0; i < lines.length; i++) {
321
+ let line = lines[i];
322
+ if (/^\x00CODEBLOCK\d+\x00$/.test(line.trim())) { closeList(); closeTable(); closeBlockquote(); result.push(line.trim()); continue; }
323
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
324
+ if (headingMatch) { closeList(); closeTable(); closeBlockquote(); const level = headingMatch[1].length; result.push('<h' + level + '>' + renderInline(headingMatch[2]) + '</h' + level + '>'); continue; }
325
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { closeList(); closeTable(); closeBlockquote(); result.push('<hr>'); continue; }
326
+ const bqMatch = line.match(/^>\s?(.*)$/);
327
+ if (bqMatch) { closeList(); closeTable(); if (!inBlockquote) { result.push('<blockquote>'); inBlockquote = true; } result.push('<p>' + renderInline(bqMatch[1]) + '</p>'); continue; } else { closeBlockquote(); }
328
+ if (/^\|.*\|\s*$/.test(line.trim())) { closeList(); if (/^\|[\s:|-]+\|$/.test(line.trim())) continue; if (!inTable) inTable = true; tableRows.push(line.trim()); continue; } else { closeTable(); }
329
+ const olMatch = line.match(/^\d+\.\s+(.+)$/);
330
+ if (olMatch) { if (!inList || listType !== 'ol') { closeList(); result.push('<ol>'); inList = true; listType = 'ol'; } result.push('<li>' + renderInline(olMatch[1]) + '</li>'); continue; }
331
+ const ulMatch = line.match(/^[-*+]\s+(.+)$/);
332
+ if (ulMatch) { if (!inList || listType !== 'ul') { closeList(); result.push('<ul>'); inList = true; listType = 'ul'; } result.push('<li>' + renderInline(ulMatch[1]) + '</li>'); continue; }
333
+ if (line.trim() === '') { closeList(); closeTable(); closeBlockquote(); continue; }
334
+ closeList(); closeTable(); closeBlockquote();
335
+ result.push('<p>' + renderInline(line) + '</p>');
336
+ }
337
+ closeList(); closeTable(); closeBlockquote();
338
+
339
+ let html = result.join('\n');
340
+ html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (m, idx) => {
341
+ const block = codeBlocks[parseInt(idx)];
342
+ return wrapCodeBlock(block.lang, block.code);
343
+ });
344
+ return html;
345
+ }
346
+
347
+ function renderTable(rows) {
348
+ if (rows.length < 1) return '';
349
+ const parseRow = (row) => row.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
350
+ let html = '<table><thead><tr>';
351
+ const headerCells = parseRow(rows[0]);
352
+ for (const cell of headerCells) html += '<th>' + renderInline(cell) + '</th>';
353
+ html += '</tr></thead><tbody>';
354
+ for (let i = 1; i < rows.length; i++) {
355
+ html += '<tr>';
356
+ for (const cell of parseRow(rows[i])) html += '<td>' + renderInline(cell) + '</td>';
357
+ html += '</tr>';
358
+ }
359
+ html += '</tbody></table>';
360
+ return html;
361
+ }
362
+
363
+ function wrapCodeBlock(lang, code) {
364
+ const langLabel = lang ? lang.toUpperCase() : 'CODE';
365
+ return '<pre><div class="code-header"><span>' + escapeHtml(langLabel) + '</span>'
366
+ + '<button class="copy-btn" onclick="copyCode(this)">Copy</button></div>'
367
+ + '<code>' + escapeHtml(code) + '</code></pre>';
368
+ }
369
+
370
+ function renderInline(text) {
371
+ let t = text;
372
+ t = t.replace(/`([^`]+)`/g, '<code>$1</code>');
373
+ t = t.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
374
+ t = t.replace(/__([^_]+)__/g, '<strong>$1</strong>');
375
+ t = t.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
376
+ t = t.replace(/(?<!_)_([^_]+)_(?!_)/g, '<em>$1</em>');
377
+ t = t.replace(/~~([^~]+)~~/g, '<del>$1</del>');
378
+ t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
379
+ return t;
380
+ }
381
+
382
+ function copyCode(btn) {
383
+ const codeEl = btn.closest('pre').querySelector('code');
384
+ navigator.clipboard.writeText(codeEl.textContent).then(() => {
385
+ btn.textContent = 'Copied!';
386
+ setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
387
+ }).catch(() => { btn.textContent = 'Failed'; setTimeout(() => { btn.textContent = 'Copy'; }, 2000); });
388
+ }
389
+
390
+ function addUserMessage(text) {
391
+ const div = document.createElement('div');
392
+ div.className = 'message user';
393
+ div.innerHTML = '<div class="message-avatar">U</div><div class="message-content">' + escapeHtml(text) + '</div>';
394
+ chatMessages.appendChild(div);
395
+ autoScroll();
396
+ }
397
+
398
+ function addAgentMessage(text) {
399
+ const div = document.createElement('div');
400
+ div.className = 'message agent';
401
+ div.innerHTML = '<div class="message-avatar">AI</div><div class="message-content md">' + renderMarkdown(text) + '</div>';
402
+ chatMessages.appendChild(div);
403
+ autoScroll();
404
+ return div.querySelector('.message-content');
405
+ }
406
+
407
+ function startAgentStreaming() {
408
+ const div = document.createElement('div');
409
+ div.className = 'message agent';
410
+ div.innerHTML = '<div class="message-avatar">AI</div><div class="message-content md"><div class="typing"><span></span><span></span><span></span></div></div>';
411
+ chatMessages.appendChild(div);
412
+ autoScroll();
413
+ currentAgentContent = div.querySelector('.message-content');
414
+ currentAgentContent._rawText = '';
415
+ return currentAgentContent;
416
+ }
417
+
418
+ function appendContentChunk(chunk) {
419
+ if (!currentAgentContent) return;
420
+ currentAgentContent._rawText += chunk;
421
+ if (renderTimer) return;
422
+ renderTimer = setTimeout(() => {
423
+ renderTimer = null;
424
+ if (currentAgentContent && currentAgentContent._rawText) {
425
+ currentAgentContent.innerHTML = renderMarkdown(currentAgentContent._rawText);
426
+ autoScroll();
427
+ }
428
+ }, 50);
429
+ }
430
+
431
+ function finalizeAgentContent() {
432
+ if (renderTimer) { clearTimeout(renderTimer); renderTimer = null; }
433
+ if (currentAgentContent && currentAgentContent._rawText) {
434
+ currentAgentContent.innerHTML = renderMarkdown(currentAgentContent._rawText);
435
+ autoScroll();
436
+ }
437
+ }
438
+
439
+ function ensureToolBadgesContainer() {
440
+ if (!toolBadgesContainer) {
441
+ toolBadgesContainer = document.createElement('div');
442
+ toolBadgesContainer.className = 'tool-badges';
443
+ chatMessages.appendChild(toolBadgesContainer);
444
+ }
445
+ return toolBadgesContainer;
446
+ }
447
+
448
+ function addToolBadge(toolName) {
449
+ const container = ensureToolBadgesContainer();
450
+ const badge = document.createElement('span');
451
+ badge.className = 'tool-badge pending';
452
+ badge.dataset.tool = toolName;
453
+ badge.innerHTML = '<span class="dot-icon"></span>' + escapeHtml(toolName);
454
+ container.appendChild(badge);
455
+ autoScroll();
456
+ return badge;
457
+ }
458
+
459
+ function completeToolBadge(badge, toolName, success) {
460
+ if (!badge) return;
461
+ badge.classList.remove('pending');
462
+ badge.classList.add(success ? 'success' : 'error');
463
+ autoScroll();
464
+ }
465
+
466
+ function resetToolBadges() { toolBadgesContainer = null; }
467
+
468
+ function addError(message) {
469
+ const div = document.createElement('div');
470
+ div.className = 'error-banner';
471
+ div.textContent = message;
472
+ chatMessages.appendChild(div);
473
+ autoScroll();
474
+ }
475
+
476
+ function addSystemNotice(message) {
477
+ const div = document.createElement('div');
478
+ div.className = 'system-notice';
479
+ div.innerHTML = message;
480
+ chatMessages.appendChild(div);
481
+ autoScroll();
482
+ }
483
+
484
+ async function sendMessage() {
485
+ const text = input.value.trim();
486
+ if (!text || isStreaming) return;
487
+
488
+ isStreaming = true;
489
+ sendBtn.disabled = true;
490
+ sendBtn.textContent = '...';
491
+ input.value = '';
492
+ input.style.height = 'auto';
493
+
494
+ addUserMessage(text);
495
+ startAgentStreaming();
496
+ resetToolBadges();
497
+
498
+ const badgeMap = {};
499
+
500
+ try {
501
+ const response = await fetch(BASE_PATH + '/api/chat', {
502
+ method: 'POST',
503
+ headers: {'Content-Type': 'application/json'},
504
+ body: JSON.stringify({message: text, sessionId: sessionId})
505
+ });
506
+ if (!response.ok) throw new Error('HTTP ' + response.status);
507
+
508
+ const reader = response.body.getReader();
509
+ const decoder = new TextDecoder();
510
+ let buffer = '';
511
+ let currentEvent = '';
512
+ let currentData = '';
513
+ let streamDone = false;
514
+
515
+ while (!streamDone) {
516
+ const {done, value} = await reader.read();
517
+ if (done) break;
518
+
519
+ buffer += decoder.decode(value, {stream: true});
520
+ const lines = buffer.split('\n');
521
+ buffer = lines.pop();
522
+
523
+ for (const line of lines) {
524
+ const trimmed = line.replace(/\r$/, '');
525
+ if (trimmed.startsWith('event:')) {
526
+ currentEvent = trimmed.substring(6).trim();
527
+ } else if (trimmed.startsWith('data:')) {
528
+ const dataPart = trimmed.substring(5);
529
+ if (currentData.length > 0) currentData += '\n';
530
+ currentData += dataPart.startsWith(' ') ? dataPart.substring(1) : dataPart;
531
+ } else if (trimmed === '') {
532
+ if (currentEvent === 'content') {
533
+ let chunk;
534
+ try { chunk = JSON.parse(currentData); }
535
+ catch(e) { chunk = currentData; }
536
+ appendContentChunk(chunk);
537
+ } else if (currentEvent === 'tool_start') {
538
+ const name = currentData;
539
+ badgeMap[name] = addToolBadge(name);
540
+ } else if (currentEvent === 'tool_result') {
541
+ const colonIdx = currentData.indexOf(':');
542
+ const name = colonIdx > 0 ? currentData.substring(0, colonIdx).trim() : currentData;
543
+ completeToolBadge(badgeMap[name], name, true);
544
+ } else if (currentEvent === 'done') {
545
+ streamDone = true;
546
+ } else if (currentEvent === 'context_compressed') {
547
+ try {
548
+ const info = JSON.parse(currentData);
549
+ addSystemNotice('Context auto-compressed: ' + info.originalTokens + ' to ~' + info.compressedTokens + ' tokens (' + info.removedRounds + ' old rounds removed)');
550
+ } catch(e) {
551
+ addSystemNotice('Context auto-compressed');
552
+ }
553
+ } else if (currentEvent === 'error') {
554
+ addError(currentData);
555
+ }
556
+ currentEvent = '';
557
+ currentData = '';
558
+ }
559
+ }
560
+ }
561
+ finalizeAgentContent();
562
+ } catch (err) {
563
+ addError('Connection error: ' + err.message);
564
+ finalizeAgentContent();
565
+ } finally {
566
+ isStreaming = false;
567
+ sendBtn.disabled = false;
568
+ sendBtn.textContent = 'Send';
569
+ input.focus();
570
+ }
571
+ }
572
+ </script>
573
+ </body>
574
+ </html>"""
575
+
576
+
577
+ def render(base_path: str = "/agent") -> str:
578
+ """Render the chat page HTML. base_path is unused — JS derives path from window.location."""
579
+ return _CHAT_TEMPLATE
580
+
581
+
582
+ CHAT_HTML = render("/agent")