pi-tau-web-server 1.0.8

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 (98) hide show
  1. package/README.md +303 -0
  2. package/bin/auth.js +96 -0
  3. package/bin/config.js +99 -0
  4. package/bin/model-utils.js +134 -0
  5. package/bin/server-main.js +1189 -0
  6. package/bin/sessions.js +492 -0
  7. package/bin/tau.js +8 -0
  8. package/bin/tree.js +248 -0
  9. package/bin/types.js +2 -0
  10. package/package.json +74 -0
  11. package/public/app-main.js +2025 -0
  12. package/public/app-types.js +1 -0
  13. package/public/app.js +1 -0
  14. package/public/command-palette.js +42 -0
  15. package/public/dialogs.js +199 -0
  16. package/public/file-browser.js +196 -0
  17. package/public/icons/apple-touch-icon.png +0 -0
  18. package/public/icons/favicon-16.png +0 -0
  19. package/public/icons/favicon-32.png +0 -0
  20. package/public/icons/tau-192.png +0 -0
  21. package/public/icons/tau-512.png +0 -0
  22. package/public/icons/tau-logo.png +0 -0
  23. package/public/icons/tau-logo.svg +13 -0
  24. package/public/icons/tau-maskable-512.png +0 -0
  25. package/public/icons/tau-new.png +0 -0
  26. package/public/index.html +264 -0
  27. package/public/launcher-panel.js +54 -0
  28. package/public/launcher.js +84 -0
  29. package/public/manifest.json +28 -0
  30. package/public/markdown.js +336 -0
  31. package/public/message-renderer.js +268 -0
  32. package/public/model-picker.js +478 -0
  33. package/public/session-sidebar.js +460 -0
  34. package/public/session-stats-card.js +123 -0
  35. package/public/state.js +81 -0
  36. package/public/style.css +3864 -0
  37. package/public/sw.js +66 -0
  38. package/public/themes.js +72 -0
  39. package/public/tool-card.js +317 -0
  40. package/public/tree-view.js +474 -0
  41. package/public/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  42. package/public/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  43. package/public/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  44. package/public/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  45. package/public/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  46. package/public/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  47. package/public/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  48. package/public/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  49. package/public/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  50. package/public/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  51. package/public/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  52. package/public/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  53. package/public/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  54. package/public/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  55. package/public/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  56. package/public/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  57. package/public/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  58. package/public/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  59. package/public/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  60. package/public/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  61. package/public/vendor/katex/katex.min.css +1 -0
  62. package/public/vendor/katex/katex.min.js +1 -0
  63. package/public/voice-input.js +74 -0
  64. package/public/websocket-client.js +156 -0
  65. package/scripts/copy-katex.mjs +32 -0
  66. package/src/pi-extension/tau-tree.ts +71 -0
  67. package/src/public/app-main.ts +2190 -0
  68. package/src/public/app-types.ts +96 -0
  69. package/src/public/app.ts +1 -0
  70. package/src/public/command-palette.ts +53 -0
  71. package/src/public/dialogs.ts +251 -0
  72. package/src/public/file-browser.ts +224 -0
  73. package/src/public/launcher-panel.ts +68 -0
  74. package/src/public/launcher.ts +101 -0
  75. package/src/public/legacy-dom.d.ts +29 -0
  76. package/src/public/markdown.ts +372 -0
  77. package/src/public/message-renderer.ts +311 -0
  78. package/src/public/model-picker.ts +500 -0
  79. package/src/public/session-sidebar.ts +522 -0
  80. package/src/public/session-stats-card.ts +176 -0
  81. package/src/public/state.ts +96 -0
  82. package/src/public/sw.ts +79 -0
  83. package/src/public/themes.ts +73 -0
  84. package/src/public/tool-card.ts +375 -0
  85. package/src/public/tree-view.ts +527 -0
  86. package/src/public/voice-input.ts +98 -0
  87. package/src/public/websocket-client.ts +165 -0
  88. package/src/server/auth.ts +88 -0
  89. package/src/server/config.ts +88 -0
  90. package/src/server/model-utils.ts +122 -0
  91. package/src/server/server-main.ts +1004 -0
  92. package/src/server/sessions.ts +481 -0
  93. package/src/server/tau.ts +9 -0
  94. package/src/server/tree.ts +288 -0
  95. package/src/server/types.ts +68 -0
  96. package/tsconfig.json +3 -0
  97. package/tsconfig.public.json +15 -0
  98. package/tsconfig.server.json +16 -0
package/public/sw.js ADDED
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ // Tau Service Worker — minimal, just enables PWA install
3
+ // No aggressive caching since Tau connects to a live local server
4
+ /// <reference lib="webworker" />
5
+ const CACHE_NAME = 'tau-v4';
6
+ const serviceWorker = self;
7
+ // Cache only the app shell on install
8
+ serviceWorker.addEventListener('install', (event) => {
9
+ event.waitUntil(caches.open(CACHE_NAME).then((cache) => {
10
+ return cache.addAll([
11
+ '/',
12
+ '/style.css',
13
+ '/app.js',
14
+ '/app-main.js',
15
+ '/state.js',
16
+ '/themes.js',
17
+ '/markdown.js',
18
+ '/message-renderer.js',
19
+ '/tool-card.js',
20
+ '/tree-view.js',
21
+ '/dialogs.js',
22
+ '/session-sidebar.js',
23
+ '/session-stats-card.js',
24
+ '/websocket-client.js',
25
+ '/manifest.json',
26
+ // KaTeX shell assets; fonts are picked up on demand by the
27
+ // network-first runtime cache so a missing font can't fail install.
28
+ '/vendor/katex/katex.min.js',
29
+ '/vendor/katex/katex.min.css',
30
+ ]);
31
+ }));
32
+ serviceWorker.skipWaiting();
33
+ });
34
+ // Clean old caches
35
+ serviceWorker.addEventListener('activate', (event) => {
36
+ event.waitUntil(caches.keys().then((names) => {
37
+ return Promise.all(names.filter((name) => name !== CACHE_NAME).map((name) => caches.delete(name)));
38
+ }));
39
+ serviceWorker.clients.claim();
40
+ });
41
+ // Network-first strategy — always try live server, fall back to cache
42
+ serviceWorker.addEventListener('fetch', (event) => {
43
+ const url = new URL(event.request.url);
44
+ // Don't cache API/WebSocket requests
45
+ if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws')) {
46
+ return;
47
+ }
48
+ event.respondWith(fetch(event.request)
49
+ .then((response) => {
50
+ // Update cache with fresh response
51
+ if (response.ok && event.request.method === 'GET') {
52
+ const clone = response.clone();
53
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
54
+ }
55
+ return response;
56
+ })
57
+ .catch(() => {
58
+ // Offline — serve from cache
59
+ return caches.match(event.request).then((cached) => {
60
+ return cached || new Response('Tau is offline — start your pi session to connect.', {
61
+ status: 503,
62
+ headers: { 'Content-Type': 'text/plain' },
63
+ });
64
+ });
65
+ }));
66
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Theme system — four themes: two light, two dark
3
+ */
4
+ export const themes = {
5
+ night: {
6
+ name: 'Dusk',
7
+ dark: true,
8
+ colors: ['#212121', '#a0a0a0', '#777777', '#666666'],
9
+ vars: {},
10
+ },
11
+ dawn: {
12
+ name: 'Dawn',
13
+ dark: true,
14
+ colors: ['#1a1d26', '#7a8ab0', '#6a5a80', '#5a7a9a'],
15
+ vars: {},
16
+ },
17
+ midnight: {
18
+ name: 'Midnight',
19
+ dark: true,
20
+ colors: ['#000000', '#5a7a9a', '#4a5565', '#4a5a72'],
21
+ vars: {},
22
+ },
23
+ clean: {
24
+ name: 'Clean',
25
+ dark: false,
26
+ colors: ['#ffffff', '#0580c4', '#007aff', '#5ac8fa'],
27
+ vars: {},
28
+ },
29
+ terracotta: {
30
+ name: 'Terracotta',
31
+ dark: false,
32
+ colors: ['#f4f1ec', '#b06a48', '#5c2860', '#3a6a9b'],
33
+ vars: {},
34
+ },
35
+ sage: {
36
+ name: 'Sage',
37
+ dark: false,
38
+ colors: ['#f0f2ec', '#6a7d5a', '#4a3860', '#3a6a7a'],
39
+ vars: {},
40
+ },
41
+ };
42
+ export function applyTheme(themeId) {
43
+ const root = document.documentElement;
44
+ // Validate
45
+ if (!(themeId in themes))
46
+ themeId = 'night';
47
+ root.setAttribute('data-theme', themeId);
48
+ localStorage.setItem('tau-theme', themeId);
49
+ }
50
+ export function getCurrentTheme() {
51
+ const saved = localStorage.getItem('tau-theme');
52
+ // Migrate old values
53
+ if (saved === 'dark')
54
+ return 'night';
55
+ if (saved === 'light')
56
+ return 'terracotta';
57
+ if (saved && saved in themes)
58
+ return saved;
59
+ // Auto-detect from OS
60
+ if (window.matchMedia?.('(prefers-color-scheme: light)').matches)
61
+ return 'terracotta';
62
+ return 'night';
63
+ }
64
+ // Listen for OS theme changes if no explicit preference saved
65
+ if (!localStorage.getItem('tau-theme')) {
66
+ window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
67
+ if (!localStorage.getItem('tau-theme')) {
68
+ const root = document.documentElement;
69
+ root.setAttribute('data-theme', e.matches ? 'terracotta' : 'night');
70
+ }
71
+ });
72
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Tool Card - Renders and updates tool execution cards (collapsible)
3
+ */
4
+ export class ToolCardRenderer {
5
+ container;
6
+ toolCards;
7
+ constructor(container) {
8
+ this.container = container;
9
+ this.toolCards = new Map(); // toolCallId -> element
10
+ }
11
+ createToolCard(toolExecution) {
12
+ const { toolCallId, toolName, args, status } = toolExecution;
13
+ const card = document.createElement('div');
14
+ card.className = 'tool-card';
15
+ card.dataset.toolCallId = String(toolCallId || '');
16
+ const argsPreview = this.getArgsPreview(String(toolName || ''), args);
17
+ const argsJson = this.formatJson(args);
18
+ const isExpanded = (status === 'streaming' || status === 'pending');
19
+ const isEdit = (toolName === 'edit' || toolName === 'Edit') && args && (args.oldText || args.old_text) && (args.newText || args.new_text);
20
+ card.innerHTML = `
21
+ <div class="tool-card-header" onclick="this.parentElement.querySelector('.tool-card-body').classList.toggle('expanded'); this.querySelector('.tool-card-chevron').classList.toggle('expanded')">
22
+ <div class="tool-header-left">
23
+ <span class="tool-card-chevron${isExpanded ? ' expanded' : ''}"><svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor"><path d="M2 1l4 3-4 3z"/></svg></span>
24
+ <span class="tool-name">${this.escapeHtml(toolName || '')}</span>
25
+ ${argsPreview ? `<span class="tool-args-preview">${this.escapeHtml(argsPreview)}</span>` : ''}
26
+ </div>
27
+ <div class="tool-header-right">
28
+ <button class="tool-action-btn copy-output-btn" title="Copy output" onclick="event.stopPropagation(); var t=this.closest('.tool-card').querySelector('.tool-output'); if(!t||!t.textContent.trim())return; var s=t.textContent,b=this; (navigator.clipboard?navigator.clipboard.writeText(s):new Promise(function(r){var a=document.createElement('textarea');a.value=s;a.style.cssText='position:fixed;left:-9999px';document.body.appendChild(a);a.select();document.execCommand('copy');document.body.removeChild(a);r()})).then(function(){b.classList.add('copied');setTimeout(function(){b.classList.remove('copied')},1500)})"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button>
29
+ <div class="tool-status ${status}">${status}</div>
30
+ </div>
31
+ </div>
32
+ <div class="tool-card-body${isExpanded ? ' expanded' : ''}">
33
+ ${!isEdit && argsJson ? `<div class="tool-args">${this.escapeHtml(argsJson)}</div>` : ''}
34
+ <div class="tool-output-wrapper">
35
+ <div class="tool-output"></div>
36
+ </div>
37
+ </div>
38
+ `;
39
+ // Insert diff view for Edit tools
40
+ if (isEdit) {
41
+ const diffEl = this.renderDiff(String(args.oldText || args.old_text || ''), String(args.newText || args.new_text || ''));
42
+ const body = card.querySelector('.tool-card-body');
43
+ if (body)
44
+ body.insertBefore(diffEl, body.firstChild);
45
+ }
46
+ this.container.appendChild(card);
47
+ this.toolCards.set(String(toolCallId || ''), card);
48
+ this.scrollToBottom();
49
+ return card;
50
+ }
51
+ updateToolCard(toolExecution) {
52
+ let card = this.toolCards.get(String(toolExecution.toolCallId || ''));
53
+ if (!card) {
54
+ card = this.createToolCard(toolExecution);
55
+ }
56
+ // Update status
57
+ const statusElement = card.querySelector('.tool-status');
58
+ if (statusElement) {
59
+ statusElement.className = `tool-status ${toolExecution.status}`;
60
+ statusElement.textContent = toolExecution.status ?? null;
61
+ }
62
+ // Auto-expand when streaming
63
+ if (toolExecution.status === 'streaming') {
64
+ const body = card.querySelector('.tool-card-body');
65
+ const chevron = card.querySelector('.tool-card-chevron');
66
+ if (body)
67
+ body.classList.add('expanded');
68
+ if (chevron)
69
+ chevron.classList.add('expanded');
70
+ }
71
+ // Update output
72
+ const outputElement = card.querySelector('.tool-output');
73
+ if (outputElement && toolExecution.output) {
74
+ outputElement.textContent = toolExecution.output;
75
+ this.scrollToBottom();
76
+ }
77
+ }
78
+ finalizeToolCard(toolCallId, result, isError) {
79
+ const card = this.toolCards.get(toolCallId);
80
+ if (!card)
81
+ return;
82
+ // Update status
83
+ const statusElement = card.querySelector('.tool-status');
84
+ if (statusElement) {
85
+ const status = isError ? 'error' : 'complete';
86
+ statusElement.className = `tool-status ${status}`;
87
+ statusElement.textContent = status;
88
+ }
89
+ // Update output with final result
90
+ const outputElement = card.querySelector('.tool-output');
91
+ if (outputElement && result) {
92
+ const output = this.formatResult(result);
93
+ outputElement.textContent = output;
94
+ }
95
+ // Collapse completed cards (less noise)
96
+ if (!isError) {
97
+ const body = card.querySelector('.tool-card-body');
98
+ const chevron = card.querySelector('.tool-card-chevron');
99
+ if (body)
100
+ body.classList.remove('expanded');
101
+ if (chevron)
102
+ chevron.classList.remove('expanded');
103
+ }
104
+ }
105
+ /**
106
+ * Create a pre-collapsed card for session history using DOM methods (no innerHTML)
107
+ */
108
+ createHistoryCard(toolExecution) {
109
+ const { toolCallId, toolName, args } = toolExecution;
110
+ const card = document.createElement('div');
111
+ card.className = 'tool-card';
112
+ card.dataset.toolCallId = String(toolCallId || '');
113
+ // Header
114
+ const header = document.createElement('div');
115
+ header.className = 'tool-card-header';
116
+ const headerLeft = document.createElement('div');
117
+ headerLeft.className = 'tool-header-left';
118
+ const chevron = document.createElement('span');
119
+ chevron.className = 'tool-card-chevron';
120
+ chevron.innerHTML = '<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor"><path d="M2 1l4 3-4 3z"/></svg>';
121
+ headerLeft.appendChild(chevron);
122
+ const name = document.createElement('span');
123
+ name.className = 'tool-name';
124
+ name.textContent = String(toolName || '');
125
+ headerLeft.appendChild(name);
126
+ const preview = this.getArgsPreview(String(toolName || ''), args);
127
+ if (preview) {
128
+ const previewEl = document.createElement('span');
129
+ previewEl.className = 'tool-args-preview';
130
+ previewEl.textContent = preview;
131
+ headerLeft.appendChild(previewEl);
132
+ }
133
+ header.appendChild(headerLeft);
134
+ // Right side: copy button + status
135
+ const headerRight = document.createElement('div');
136
+ headerRight.className = 'tool-header-right';
137
+ const copyBtn = document.createElement('button');
138
+ copyBtn.className = 'tool-action-btn copy-output-btn';
139
+ copyBtn.title = 'Copy output';
140
+ copyBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
141
+ copyBtn.addEventListener('click', (e) => {
142
+ e.stopPropagation();
143
+ const output = card.querySelector('.tool-output');
144
+ if (!output || !output.textContent.trim())
145
+ return;
146
+ const text = output.textContent;
147
+ (navigator.clipboard ? navigator.clipboard.writeText(text) : new Promise((r) => {
148
+ const ta = document.createElement('textarea');
149
+ ta.value = text;
150
+ ta.style.cssText = 'position:fixed;left:-9999px';
151
+ document.body.appendChild(ta);
152
+ ta.select();
153
+ document.execCommand('copy');
154
+ document.body.removeChild(ta);
155
+ r();
156
+ })).then(() => {
157
+ copyBtn.classList.add('copied');
158
+ setTimeout(() => copyBtn.classList.remove('copied'), 1500);
159
+ });
160
+ });
161
+ headerRight.appendChild(copyBtn);
162
+ const status = document.createElement('div');
163
+ status.className = 'tool-status complete';
164
+ status.textContent = 'complete';
165
+ headerRight.appendChild(status);
166
+ header.appendChild(headerRight);
167
+ // Toggle expand on click
168
+ header.addEventListener('click', () => {
169
+ body.classList.toggle('expanded');
170
+ chevron.classList.toggle('expanded');
171
+ });
172
+ card.appendChild(header);
173
+ // Body (collapsed by default)
174
+ const body = document.createElement('div');
175
+ body.className = 'tool-card-body';
176
+ const isEdit = (toolName === 'edit' || toolName === 'Edit') && args && (args.oldText || args.old_text) && (args.newText || args.new_text);
177
+ if (isEdit) {
178
+ body.appendChild(this.renderDiff(String(args.oldText || args.old_text || ''), String(args.newText || args.new_text || '')));
179
+ }
180
+ else {
181
+ const argsJson = this.formatJson(args);
182
+ if (argsJson) {
183
+ const argsEl = document.createElement('div');
184
+ argsEl.className = 'tool-args';
185
+ argsEl.textContent = argsJson;
186
+ body.appendChild(argsEl);
187
+ }
188
+ }
189
+ const outputEl = document.createElement('div');
190
+ outputEl.className = 'tool-output';
191
+ body.appendChild(outputEl);
192
+ card.appendChild(body);
193
+ this.container.appendChild(card);
194
+ this.toolCards.set(String(toolCallId || ''), card);
195
+ return card;
196
+ }
197
+ /**
198
+ * Add result to a history card (stays collapsed)
199
+ */
200
+ addHistoryResult(toolCallId, result, isError) {
201
+ const card = this.toolCards.get(toolCallId);
202
+ if (!card)
203
+ return;
204
+ if (isError) {
205
+ const statusEl = card.querySelector('.tool-status');
206
+ if (statusEl) {
207
+ statusEl.className = 'tool-status error';
208
+ statusEl.textContent = 'error';
209
+ }
210
+ }
211
+ const outputElement = card.querySelector('.tool-output');
212
+ if (outputElement && result) {
213
+ outputElement.textContent = this.formatResult(result);
214
+ }
215
+ }
216
+ /** Compact preview for the header line */
217
+ getArgsPreview(toolName, args) {
218
+ if (!args || Object.keys(args).length === 0)
219
+ return '';
220
+ // Show the most relevant arg inline
221
+ if (args.path)
222
+ return String(args.path);
223
+ if (args.command)
224
+ return String(args.command).substring(0, 80);
225
+ if (args.query)
226
+ return String(args.query).substring(0, 60);
227
+ if (args.url)
228
+ return String(args.url);
229
+ // Fallback: first string value
230
+ for (const val of Object.values(args)) {
231
+ if (typeof val === 'string' && val.length > 0) {
232
+ return val.substring(0, 60);
233
+ }
234
+ }
235
+ return '';
236
+ }
237
+ formatJson(obj) {
238
+ if (!obj)
239
+ return '';
240
+ try {
241
+ if (Object.keys(obj).length === 0)
242
+ return '';
243
+ return JSON.stringify(obj, null, 2);
244
+ }
245
+ catch {
246
+ return String(obj);
247
+ }
248
+ }
249
+ /** Render a simple inline diff for Edit tool */
250
+ renderDiff(oldText, newText) {
251
+ const container = document.createElement('div');
252
+ container.className = 'tool-diff';
253
+ const oldLines = oldText.split('\n');
254
+ const newLines = newText.split('\n');
255
+ // Removed lines
256
+ for (const line of oldLines) {
257
+ const el = document.createElement('div');
258
+ el.className = 'diff-line diff-removed';
259
+ el.textContent = '- ' + line;
260
+ container.appendChild(el);
261
+ }
262
+ // Added lines
263
+ for (const line of newLines) {
264
+ const el = document.createElement('div');
265
+ el.className = 'diff-line diff-added';
266
+ el.textContent = '+ ' + line;
267
+ container.appendChild(el);
268
+ }
269
+ return container;
270
+ }
271
+ formatResult(result) {
272
+ if (!result)
273
+ return '';
274
+ if (result.content && Array.isArray(result.content)) {
275
+ return result.content
276
+ .map((block) => {
277
+ if (block.type === 'text')
278
+ return block.text;
279
+ return JSON.stringify(block);
280
+ })
281
+ .join('\n');
282
+ }
283
+ return JSON.stringify(result, null, 2);
284
+ }
285
+ escapeHtml(text) {
286
+ const div = document.createElement('div');
287
+ div.textContent = String(text ?? '');
288
+ return div.innerHTML;
289
+ }
290
+ scrollToBottom() {
291
+ if (this.container) {
292
+ const threshold = 100;
293
+ const isNear = this.container.scrollHeight - this.container.scrollTop - this.container.clientHeight < threshold;
294
+ if (isNear) {
295
+ requestAnimationFrame(() => {
296
+ this.container.scrollTop = this.container.scrollHeight;
297
+ });
298
+ }
299
+ }
300
+ }
301
+ expandAll() {
302
+ this.toolCards.forEach(card => {
303
+ card.querySelector('.tool-card-body')?.classList.add('expanded');
304
+ card.querySelector('.tool-card-chevron')?.classList.add('expanded');
305
+ });
306
+ }
307
+ collapseAll() {
308
+ this.toolCards.forEach(card => {
309
+ card.querySelector('.tool-card-body')?.classList.remove('expanded');
310
+ card.querySelector('.tool-card-chevron')?.classList.remove('expanded');
311
+ });
312
+ }
313
+ clear() {
314
+ this.toolCards.forEach((card) => card.remove());
315
+ this.toolCards.clear();
316
+ }
317
+ }