@hanzo/browser-extension 1.7.5 → 1.9.31

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 (133) hide show
  1. package/LICENSE +9 -18
  2. package/dist/browser-extension/README.md +1 -1
  3. package/dist/browser-extension/background-firefox.js +4184 -491
  4. package/dist/browser-extension/background.js +6938 -2413
  5. package/dist/browser-extension/browser-control.js +652 -18
  6. package/dist/browser-extension/chrome/background.js +6938 -2413
  7. package/dist/browser-extension/chrome/content-script.js +1642 -123
  8. package/dist/browser-extension/chrome/icon128.png +0 -0
  9. package/dist/browser-extension/chrome/icon16.png +0 -0
  10. package/dist/browser-extension/chrome/icon48.png +0 -0
  11. package/dist/browser-extension/chrome/manifest.json +20 -11
  12. package/dist/browser-extension/chrome/popup.css +352 -110
  13. package/dist/browser-extension/chrome/popup.html +159 -48
  14. package/dist/browser-extension/chrome/popup.js +1418 -46
  15. package/dist/browser-extension/chrome/sidebar.css +772 -156
  16. package/dist/browser-extension/chrome/sidebar.html +268 -85
  17. package/dist/browser-extension/chrome/sidebar.js +25846 -84
  18. package/dist/browser-extension/cli.js +2 -2
  19. package/dist/browser-extension/content-script.js +1642 -123
  20. package/dist/browser-extension/firefox/background.js +4184 -491
  21. package/dist/browser-extension/firefox/content-script.js +1642 -123
  22. package/dist/browser-extension/firefox/icon128.png +0 -0
  23. package/dist/browser-extension/firefox/icon16.png +0 -0
  24. package/dist/browser-extension/firefox/icon48.png +0 -0
  25. package/dist/browser-extension/firefox/manifest.json +24 -13
  26. package/dist/browser-extension/firefox/popup.css +352 -110
  27. package/dist/browser-extension/firefox/popup.html +159 -48
  28. package/dist/browser-extension/firefox/popup.js +1418 -46
  29. package/dist/browser-extension/firefox/sidebar.css +772 -156
  30. package/dist/browser-extension/firefox/sidebar.html +268 -85
  31. package/dist/browser-extension/firefox/sidebar.js +25846 -84
  32. package/dist/browser-extension/icon128.png +0 -0
  33. package/dist/browser-extension/icon16.png +0 -0
  34. package/dist/browser-extension/icon48.png +0 -0
  35. package/dist/browser-extension/manifest.json +20 -11
  36. package/dist/browser-extension/package.json +1 -1
  37. package/dist/browser-extension/popup.js +1418 -46
  38. package/dist/browser-extension/safari/Info.plist +3 -3
  39. package/dist/browser-extension/safari/background.js +6938 -2413
  40. package/dist/browser-extension/safari/content-script.js +1642 -123
  41. package/dist/browser-extension/safari/icon128.png +0 -0
  42. package/dist/browser-extension/safari/icon16.png +0 -0
  43. package/dist/browser-extension/safari/icon48.png +0 -0
  44. package/dist/browser-extension/safari/popup.css +352 -110
  45. package/dist/browser-extension/safari/popup.html +159 -48
  46. package/dist/browser-extension/safari/popup.js +1418 -46
  47. package/dist/browser-extension/safari/sidebar.css +772 -156
  48. package/dist/browser-extension/safari/sidebar.html +268 -85
  49. package/dist/browser-extension/safari/sidebar.js +25846 -84
  50. package/dist/browser-extension/sidebar.js +25846 -84
  51. package/dist/browser-extension/webgpu-ai.js +78 -5
  52. package/images/icon128.png +0 -0
  53. package/images/icon16.png +0 -0
  54. package/images/icon48.png +0 -0
  55. package/manifest.json +7 -2
  56. package/package.json +27 -10
  57. package/src/chat-widget.tsx +96 -0
  58. package/src/gui-primitives.tsx +116 -0
  59. package/src/icon128.png +0 -0
  60. package/src/icon16.png +0 -0
  61. package/src/icon32.png +0 -0
  62. package/src/icon48.png +0 -0
  63. package/src/manifest-firefox.json +23 -12
  64. package/src/manifest.json +19 -10
  65. package/src/package.json +1 -1
  66. package/src/popup.css +352 -110
  67. package/src/popup.html +159 -48
  68. package/src/popup.js +196 -0
  69. package/src/sidebar.css +772 -156
  70. package/src/sidebar.html +268 -85
  71. package/src/sidebar.js +885 -0
  72. package/.github/workflows/docs.yml +0 -44
  73. package/.github/workflows/release.yml +0 -55
  74. package/README.md +0 -126
  75. package/dist/browser-extension/ai-worker.js +0 -30
  76. package/dist/browser-extension/cdp-bridge-server.js +0 -444
  77. package/dist/browser-extension/chrome/icon32.png +0 -0
  78. package/dist/browser-extension/firefox/icon32.png +0 -0
  79. package/dist/browser-extension/hanzo-browser-extension-firefox-1.1.0.zip +0 -0
  80. package/dist/browser-extension/icon32.png +0 -0
  81. package/dist/browser-extension/popup.css +0 -468
  82. package/dist/browser-extension/popup.html +0 -191
  83. package/dist/browser-extension/safari/icon32.png +0 -0
  84. package/dist/browser-extension/sidebar.css +0 -718
  85. package/dist/browser-extension/sidebar.html +0 -184
  86. package/dist/cdp-bridge-server.js +0 -411
  87. package/dist/hanzo-ai-chrome-1.7.2.zip +0 -0
  88. package/dist/hanzo-ai-chrome-1.7.3.zip +0 -0
  89. package/dist/hanzo-ai-firefox-1.7.2.zip +0 -0
  90. package/dist/hanzo-ai-firefox-1.7.3.zip +0 -0
  91. package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.pbxproj +0 -1047
  92. package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  93. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json +0 -11
  94. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -98
  95. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png +0 -0
  96. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png +0 -0
  97. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png +0 -0
  98. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png +0 -0
  99. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png +0 -0
  100. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png +0 -0
  101. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png +0 -0
  102. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png +0 -0
  103. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png +0 -0
  104. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png +0 -0
  105. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/universal-icon-1024@1x.png +0 -0
  106. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/Contents.json +0 -6
  107. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json +0 -21
  108. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon128.png +0 -0
  109. package/dist/safari/Hanzo AI/Shared (App)/Resources/Base.lproj/Main.html +0 -20
  110. package/dist/safari/Hanzo AI/Shared (App)/Resources/Icon.png +0 -0
  111. package/dist/safari/Hanzo AI/Shared (App)/Resources/Script.js +0 -24
  112. package/dist/safari/Hanzo AI/Shared (App)/Resources/Style.css +0 -61
  113. package/dist/safari/Hanzo AI/Shared (App)/ViewController.swift +0 -81
  114. package/dist/safari/Hanzo AI/Shared (Extension)/SafariWebExtensionHandler.swift +0 -42
  115. package/dist/safari/Hanzo AI/iOS (App)/AppDelegate.swift +0 -24
  116. package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/LaunchScreen.storyboard +0 -36
  117. package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/Main.storyboard +0 -38
  118. package/dist/safari/Hanzo AI/iOS (App)/Info.plist +0 -27
  119. package/dist/safari/Hanzo AI/iOS (App)/SceneDelegate.swift +0 -18
  120. package/dist/safari/Hanzo AI/iOS (Extension)/Info.plist +0 -13
  121. package/dist/safari/Hanzo AI/macOS (App)/AppDelegate.swift +0 -21
  122. package/dist/safari/Hanzo AI/macOS (App)/Base.lproj/Main.storyboard +0 -124
  123. package/dist/safari/Hanzo AI/macOS (App)/Info.plist +0 -8
  124. package/dist/safari/Hanzo AI/macOS (Extension)/Info.plist +0 -13
  125. package/docs/_config.yml +0 -13
  126. package/docs/_layouts/default.html +0 -435
  127. package/docs/index.md +0 -54
  128. package/install-extension.sh +0 -75
  129. package/test/claude-integration.test.ts +0 -251
  130. package/test/server-frameworks-edge-cases.test.ts +0 -145
  131. package/test/server-frameworks.test.ts +0 -395
  132. package/test-cdp-connection.js +0 -140
  133. package/tsconfig.json +0 -19
package/src/sidebar.js ADDED
@@ -0,0 +1,885 @@
1
+ // Hanzo AI Browser Extension Sidebar — Chat + Tools + Settings
2
+
3
+ class HanzoSidebar {
4
+ constructor() {
5
+ this.agents = new Map();
6
+ this.currentTab = 'chat';
7
+ this.messages = [];
8
+ this.streaming = false;
9
+ this.streamController = null;
10
+ this.authenticated = false;
11
+ this.ragContext = null;
12
+
13
+ this.initializeUI();
14
+ this.setupEventListeners();
15
+ this.checkAuth();
16
+ }
17
+
18
+ initializeUI() {
19
+ this.el = {
20
+ // Auth
21
+ authSection: document.getElementById('auth-section'),
22
+ authBtn: document.getElementById('auth-btn'),
23
+ tabBar: document.getElementById('tab-bar'),
24
+ userBadge: document.getElementById('user-badge'),
25
+ headerAvatar: document.getElementById('header-avatar'),
26
+
27
+ // Chat
28
+ tabChat: document.getElementById('tab-chat'),
29
+ chatMessages: document.getElementById('chat-messages'),
30
+ chatInput: document.getElementById('chat-input'),
31
+ sendBtn: document.getElementById('send-btn'),
32
+ modelSelect: document.getElementById('model-select'),
33
+ ragEnabled: document.getElementById('rag-enabled'),
34
+ tabContextEnabled: document.getElementById('tab-context-enabled'),
35
+ ragStatus: document.getElementById('rag-status'),
36
+
37
+ // Tools
38
+ tabTools: document.getElementById('tab-tools'),
39
+ mcpStatus: document.getElementById('mcp-status'),
40
+ mcpTools: document.getElementById('mcp-tools'),
41
+ mcpToolList: document.getElementById('mcp-tool-list'),
42
+ agentCount: document.getElementById('agent-count'),
43
+ agentList: document.getElementById('agent-list'),
44
+ tabFs: document.getElementById('tab-fs'),
45
+ refreshTabs: document.getElementById('refresh-tabs'),
46
+ gpuStatus: document.getElementById('gpu-status'),
47
+ gpuModel: document.getElementById('gpu-model'),
48
+ launchAgent: document.getElementById('launch-agent'),
49
+
50
+ // Settings
51
+ tabSettings: document.getElementById('tab-settings'),
52
+ userAvatar: document.getElementById('user-avatar'),
53
+ userName: document.getElementById('user-name'),
54
+ userEmail: document.getElementById('user-email'),
55
+ logoutBtn: document.getElementById('logout-btn'),
56
+ saveSettings: document.getElementById('save-settings'),
57
+ defaultModel: document.getElementById('default-model'),
58
+ ragUseZap: document.getElementById('rag-use-zap'),
59
+ ragIncludeTabContext: document.getElementById('rag-include-tab-context'),
60
+ ragTopK: document.getElementById('rag-top-k'),
61
+ ragKb: document.getElementById('rag-kb'),
62
+ ragEndpoint: document.getElementById('rag-endpoint'),
63
+ ragApiKey: document.getElementById('rag-api-key'),
64
+ };
65
+ }
66
+
67
+ setupEventListeners() {
68
+ // Auth
69
+ this.el.authBtn.addEventListener('click', () => this.login());
70
+
71
+ // Tabs
72
+ document.querySelectorAll('.tab').forEach(tab => {
73
+ tab.addEventListener('click', () => this.switchTab(tab.dataset.tab));
74
+ });
75
+
76
+ // Chat
77
+ this.el.sendBtn.addEventListener('click', () => this.sendMessage());
78
+ this.el.chatInput.addEventListener('keydown', (e) => {
79
+ if (e.key === 'Enter' && !e.shiftKey) {
80
+ e.preventDefault();
81
+ this.sendMessage();
82
+ }
83
+ });
84
+ this.el.chatInput.addEventListener('input', () => this.autoResize());
85
+ this.el.ragEnabled?.addEventListener('change', () => {
86
+ this.setRagStatus(this.el.ragEnabled.checked ? 'RAG enabled' : 'RAG disabled');
87
+ });
88
+ this.el.tabContextEnabled?.addEventListener('change', () => {
89
+ if (!this.el.ragEnabled?.checked) return;
90
+ this.setRagStatus(this.el.tabContextEnabled.checked ? 'Tab context on' : 'Tab context off');
91
+ });
92
+
93
+ // Tools
94
+ this.el.refreshTabs.addEventListener('click', () => this.refreshTabFilesystem());
95
+ this.el.launchAgent.addEventListener('click', () => this.showAgentLauncher());
96
+
97
+ // Settings
98
+ this.el.logoutBtn.addEventListener('click', () => this.logout());
99
+ this.el.saveSettings.addEventListener('click', () => this.saveSettings());
100
+
101
+ // Background messages
102
+ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
103
+ this.handleMessage(request);
104
+ return true;
105
+ });
106
+ }
107
+
108
+ // ===========================================================================
109
+ // Auth
110
+ // ===========================================================================
111
+
112
+ async checkAuth() {
113
+ // Always show tabs and initialize tools — auth only gates chat
114
+ this.el.tabBar.classList.remove('hidden');
115
+ this.el.authSection.classList.add('hidden');
116
+ this.switchTab('tools'); // Default to tools (always works)
117
+
118
+ // Initialize features that work without auth
119
+ this.loadModels(); // Public API — no auth required
120
+ this.connectToMCP();
121
+ this.refreshTabFilesystem();
122
+ this.checkWebGPU();
123
+ this.loadSettings();
124
+ this.startMonitoring();
125
+
126
+ try {
127
+ const response = await chrome.runtime.sendMessage({ action: 'auth.status' });
128
+ if (response?.success && response.authenticated) {
129
+ this.setUser(response.user);
130
+ this.switchTab('chat');
131
+ } else {
132
+ this.showChatLoginPrompt();
133
+ }
134
+ } catch (error) {
135
+ console.error('Auth check failed:', error);
136
+ this.showChatLoginPrompt();
137
+ }
138
+ }
139
+
140
+ async login() {
141
+ this.el.authBtn.disabled = true;
142
+ this.el.authBtn.textContent = 'Signing in...';
143
+
144
+ try {
145
+ const response = await chrome.runtime.sendMessage({ action: 'auth.login' });
146
+ if (response?.success) {
147
+ this.setUser(response.user);
148
+ this.hideChatLoginPrompt();
149
+ } else {
150
+ this.showError(response?.error || 'Sign in failed');
151
+ }
152
+ } catch (error) {
153
+ this.showError('Sign in failed');
154
+ } finally {
155
+ this.el.authBtn.disabled = false;
156
+ this.el.authBtn.textContent = 'Sign in with Hanzo';
157
+ }
158
+ }
159
+
160
+ async logout() {
161
+ await chrome.runtime.sendMessage({ action: 'auth.logout' });
162
+ this.el.userBadge.classList.add('hidden');
163
+ this.el.userName.textContent = '';
164
+ this.el.userEmail.textContent = '';
165
+ this.showChatLoginPrompt();
166
+ this.switchTab('tools');
167
+ }
168
+
169
+ setUser(user) {
170
+ this.authenticated = true;
171
+ if (user) {
172
+ const avatar = user.picture || user.avatar || '';
173
+ if (avatar) {
174
+ this.el.headerAvatar.src = avatar;
175
+ this.el.userBadge.classList.remove('hidden');
176
+ this.el.userAvatar.src = avatar;
177
+ }
178
+ this.el.userName.textContent = user.name || user.displayName || 'User';
179
+ this.el.userEmail.textContent = user.email || '';
180
+ }
181
+ this.loadModels();
182
+ this.loadConversation();
183
+ }
184
+
185
+ showChatLoginPrompt() {
186
+ this.authenticated = false;
187
+ // Show login prompt in chat area instead of blocking entire sidebar
188
+ const chatLogin = document.getElementById('chat-login-prompt');
189
+ if (chatLogin) chatLogin.classList.remove('hidden');
190
+ const chatComposer = document.getElementById('chat-composer');
191
+ if (chatComposer) chatComposer.classList.add('hidden');
192
+ }
193
+
194
+ hideChatLoginPrompt() {
195
+ this.authenticated = true;
196
+ const chatLogin = document.getElementById('chat-login-prompt');
197
+ if (chatLogin) chatLogin.classList.add('hidden');
198
+ const chatComposer = document.getElementById('chat-composer');
199
+ if (chatComposer) chatComposer.classList.remove('hidden');
200
+ }
201
+
202
+ // ===========================================================================
203
+ // Tab Navigation
204
+ // ===========================================================================
205
+
206
+ switchTab(name) {
207
+ this.currentTab = name;
208
+
209
+ // Update tab buttons
210
+ document.querySelectorAll('.tab').forEach(t => {
211
+ t.classList.toggle('active', t.dataset.tab === name);
212
+ });
213
+
214
+ // Show/hide content
215
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.add('hidden'));
216
+ const target = document.getElementById(`tab-${name}`);
217
+ if (target) target.classList.remove('hidden');
218
+ }
219
+
220
+ // ===========================================================================
221
+ // Chat
222
+ // ===========================================================================
223
+
224
+ async loadModels() {
225
+ try {
226
+ const response = await chrome.runtime.sendMessage({ action: 'chat.listModels' });
227
+ if (response?.success && response.models?.length) {
228
+ const select = this.el.modelSelect;
229
+ select.innerHTML = '';
230
+ for (const model of response.models) {
231
+ const opt = document.createElement('option');
232
+ opt.value = model.id;
233
+ opt.textContent = model.name || model.id;
234
+ select.appendChild(opt);
235
+ }
236
+ // Also populate settings default model
237
+ this.el.defaultModel.innerHTML = select.innerHTML;
238
+ }
239
+ } catch {
240
+ // Keep default options
241
+ }
242
+ }
243
+
244
+ async sendMessage() {
245
+ const text = this.el.chatInput.value.trim();
246
+ if (!text || this.streaming) return;
247
+ if (!this.authenticated) {
248
+ this.showError('Please sign in to use chat');
249
+ return;
250
+ }
251
+
252
+ // Get token
253
+ const tokenResp = await chrome.runtime.sendMessage({ action: 'auth.getToken' });
254
+ if (!tokenResp?.success || !tokenResp.token) {
255
+ this.showError('Please sign in again');
256
+ return;
257
+ }
258
+
259
+ const userMessage = { role: 'user', content: text };
260
+ const model = this.el.modelSelect.value;
261
+ let requestMessages = [...this.messages, userMessage];
262
+
263
+ this.ragContext = null;
264
+ if (this.el.ragEnabled?.checked) {
265
+ this.setRagStatus('Retrieving context...');
266
+ const ragResp = await this.requestRagContext(text);
267
+ if (ragResp?.success && Array.isArray(ragResp.snippets) && ragResp.snippets.length) {
268
+ this.ragContext = ragResp;
269
+ requestMessages = [
270
+ { role: 'system', content: this.buildRagSystemPrompt(ragResp) },
271
+ ...requestMessages,
272
+ ];
273
+ this.setRagStatus(`RAG: ${ragResp.snippets.length} snippet(s) from ${ragResp.source || 'context'}`);
274
+ } else if (ragResp?.error) {
275
+ this.setRagStatus(`RAG unavailable (${ragResp.error})`);
276
+ } else {
277
+ this.setRagStatus('No context found');
278
+ }
279
+ } else {
280
+ this.setRagStatus('RAG disabled');
281
+ }
282
+
283
+ // Add user message to transcript
284
+ this.messages.push(userMessage);
285
+ this.appendMessage('user', text);
286
+
287
+ // Clear input
288
+ this.el.chatInput.value = '';
289
+ this.autoResize();
290
+
291
+ // Show typing
292
+ this.streaming = true;
293
+ this.el.sendBtn.disabled = true;
294
+ const typingEl = this.showTyping();
295
+
296
+ // Create assistant message element
297
+ const assistantEl = this.appendMessage('assistant', '');
298
+ let fullContent = '';
299
+
300
+ try {
301
+ // Stream via direct fetch (sidebar has access via CSP)
302
+ const response = await fetch('https://api.hanzo.ai/v1/chat/completions', {
303
+ method: 'POST',
304
+ headers: {
305
+ 'Content-Type': 'application/json',
306
+ 'Authorization': `Bearer ${tokenResp.token}`,
307
+ },
308
+ body: JSON.stringify({
309
+ model,
310
+ messages: requestMessages,
311
+ stream: true,
312
+ }),
313
+ signal: (this.streamController = new AbortController()).signal,
314
+ });
315
+
316
+ if (!response.ok) {
317
+ const errText = await response.text();
318
+ throw new Error(`API error ${response.status}: ${errText}`);
319
+ }
320
+
321
+ typingEl.remove();
322
+ const reader = response.body.getReader();
323
+ const decoder = new TextDecoder();
324
+ let buffer = '';
325
+
326
+ while (true) {
327
+ const { done, value } = await reader.read();
328
+ if (done) break;
329
+
330
+ buffer += decoder.decode(value, { stream: true });
331
+ const lines = buffer.split('\n');
332
+ buffer = lines.pop() || '';
333
+
334
+ for (const line of lines) {
335
+ const trimmed = line.trim();
336
+ if (!trimmed || trimmed.startsWith(':')) continue;
337
+ if (trimmed.startsWith('data: ')) {
338
+ const data = trimmed.slice(6);
339
+ if (data === '[DONE]') break;
340
+ try {
341
+ const parsed = JSON.parse(data);
342
+ const delta = parsed.choices?.[0]?.delta?.content;
343
+ if (delta) {
344
+ fullContent += delta;
345
+ assistantEl.innerHTML = this.renderMarkdown(fullContent);
346
+ this.scrollToBottom();
347
+ }
348
+ } catch {}
349
+ }
350
+ }
351
+ }
352
+
353
+ if (this.ragContext?.snippets?.length) {
354
+ fullContent += this.buildRagCitationBlock(this.ragContext);
355
+ assistantEl.innerHTML = this.renderMarkdown(fullContent);
356
+ }
357
+
358
+ this.messages.push({ role: 'assistant', content: fullContent });
359
+ this.saveConversation();
360
+ } catch (error) {
361
+ typingEl.remove();
362
+ if (error?.name !== 'AbortError') {
363
+ assistantEl.remove();
364
+ this.appendError(error?.message || 'Chat failed');
365
+ }
366
+ } finally {
367
+ this.streaming = false;
368
+ this.streamController = null;
369
+ this.el.sendBtn.disabled = false;
370
+ }
371
+ }
372
+
373
+ async getActiveTabContext() {
374
+ try {
375
+ const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
376
+ if (!activeTab) return null;
377
+ return {
378
+ url: activeTab.url || '',
379
+ title: activeTab.title || '',
380
+ };
381
+ } catch {
382
+ return null;
383
+ }
384
+ }
385
+
386
+ async requestRagContext(query) {
387
+ try {
388
+ const pageContext = this.el.tabContextEnabled?.checked ? await this.getActiveTabContext() : null;
389
+ const topK = Math.max(1, Math.min(parseInt(this.el.ragTopK?.value || '5', 10) || 5, 20));
390
+
391
+ return await chrome.runtime.sendMessage({
392
+ action: 'rag.query',
393
+ query,
394
+ topK,
395
+ includeTabContext: !!this.el.tabContextEnabled?.checked,
396
+ useZapMemory: !!this.el.ragUseZap?.checked,
397
+ knowledgeBase: this.el.ragKb?.value || '',
398
+ endpoint: this.el.ragEndpoint?.value || '',
399
+ apiKey: this.el.ragApiKey?.value || '',
400
+ pageContext: pageContext || undefined,
401
+ });
402
+ } catch (error) {
403
+ return { success: false, error: error?.message || 'Failed to query RAG' };
404
+ }
405
+ }
406
+
407
+ buildRagSystemPrompt(ragResponse) {
408
+ const snippets = (ragResponse?.snippets || []).slice(0, 8);
409
+ const lines = snippets.map((snippet, index) => {
410
+ const title = snippet.title || snippet.source || `Snippet ${index + 1}`;
411
+ const score = typeof snippet.score === 'number' ? ` (score ${snippet.score.toFixed(3)})` : '';
412
+ const sourceLine = snippet.url ? `${title}${score} — ${snippet.url}` : `${title}${score}`;
413
+ return `[${index + 1}] ${sourceLine}\n${snippet.content}`;
414
+ });
415
+
416
+ return [
417
+ 'You are assisting inside Hanzo Browser Extension.',
418
+ 'Use the retrieved context below when relevant, and state uncertainty if context is insufficient.',
419
+ 'Retrieved Context:',
420
+ ...lines,
421
+ ].join('\n\n');
422
+ }
423
+
424
+ buildRagCitationBlock(ragResponse) {
425
+ const snippets = (ragResponse?.snippets || []).slice(0, 5);
426
+ if (!snippets.length) return '';
427
+
428
+ const rows = snippets.map((snippet, index) => {
429
+ const title = snippet.title || snippet.source || `Snippet ${index + 1}`;
430
+ if (snippet.url) {
431
+ return `- ${title}: ${snippet.url}`;
432
+ }
433
+ return `- ${title}`;
434
+ });
435
+
436
+ return `\n\n---\nContext Sources:\n${rows.join('\n')}`;
437
+ }
438
+
439
+ setRagStatus(text) {
440
+ if (!this.el.ragStatus) return;
441
+ this.el.ragStatus.textContent = text || 'Ready';
442
+ }
443
+
444
+ appendMessage(role, content) {
445
+ // Remove welcome message
446
+ const welcome = this.el.chatMessages.querySelector('.chat-welcome');
447
+ if (welcome) welcome.remove();
448
+
449
+ const div = document.createElement('div');
450
+ div.className = `msg msg-${role}`;
451
+ div.innerHTML = role === 'assistant' ? this.renderMarkdown(content) : this.escapeHtml(content);
452
+ this.el.chatMessages.appendChild(div);
453
+ this.scrollToBottom();
454
+ return div;
455
+ }
456
+
457
+ appendError(message) {
458
+ const div = document.createElement('div');
459
+ div.className = 'msg-error';
460
+ div.textContent = message;
461
+ this.el.chatMessages.appendChild(div);
462
+ this.scrollToBottom();
463
+ }
464
+
465
+ showTyping() {
466
+ const div = document.createElement('div');
467
+ div.className = 'typing-indicator';
468
+ div.innerHTML = '<span></span><span></span><span></span>';
469
+ this.el.chatMessages.appendChild(div);
470
+ this.scrollToBottom();
471
+ return div;
472
+ }
473
+
474
+ scrollToBottom() {
475
+ this.el.chatMessages.scrollTop = this.el.chatMessages.scrollHeight;
476
+ }
477
+
478
+ autoResize() {
479
+ const el = this.el.chatInput;
480
+ el.style.height = 'auto';
481
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
482
+ }
483
+
484
+ // Minimal markdown: code blocks, inline code, bold, italic, line breaks
485
+ renderMarkdown(text) {
486
+ if (!text) return '';
487
+ let html = this.escapeHtml(text);
488
+
489
+ // Code blocks: ```lang\n...\n```
490
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
491
+ return `<pre><code>${code.trim()}</code></pre>`;
492
+ });
493
+
494
+ // Inline code: `...`
495
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
496
+
497
+ // Bold: **...**
498
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
499
+
500
+ // Italic: *...*
501
+ html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
502
+
503
+ // Line breaks
504
+ html = html.replace(/\n/g, '<br>');
505
+
506
+ return html;
507
+ }
508
+
509
+ escapeHtml(text) {
510
+ const div = document.createElement('div');
511
+ div.textContent = text;
512
+ return div.innerHTML;
513
+ }
514
+
515
+ async loadConversation() {
516
+ return new Promise((resolve) => {
517
+ chrome.storage.local.get(['hanzo_chat_messages'], (result) => {
518
+ if (result.hanzo_chat_messages?.length) {
519
+ this.messages = result.hanzo_chat_messages;
520
+ // Render stored messages
521
+ const welcome = this.el.chatMessages.querySelector('.chat-welcome');
522
+ if (welcome) welcome.remove();
523
+ for (const msg of this.messages) {
524
+ this.appendMessage(msg.role, msg.content);
525
+ }
526
+ }
527
+ resolve();
528
+ });
529
+ });
530
+ }
531
+
532
+ saveConversation() {
533
+ // Keep last 50 messages
534
+ const toStore = this.messages.slice(-50);
535
+ chrome.storage.local.set({ hanzo_chat_messages: toStore });
536
+ }
537
+
538
+ // ===========================================================================
539
+ // Tools Tab
540
+ // ===========================================================================
541
+
542
+ async connectToMCP() {
543
+ try {
544
+ const zapResponse = await chrome.runtime.sendMessage({ action: 'zap.status' });
545
+ if (zapResponse?.success) {
546
+ const zap = zapResponse.zap;
547
+ const toolCount = zap.mcps?.reduce((sum, m) => sum + (m.tools?.length || 0), 0) || 0;
548
+ this.updateMCPStatus(zap.connected, toolCount, zap.mcps?.length || 0);
549
+ if (zap.connected) {
550
+ const toolsResp = await chrome.runtime.sendMessage({ action: 'zap.listTools' });
551
+ this.updateMCPToolList(toolsResp?.success ? toolsResp.tools : []);
552
+ } else {
553
+ this.updateMCPToolList([]);
554
+ }
555
+ } else {
556
+ this.updateMCPStatus(false);
557
+ this.updateMCPToolList([]);
558
+ }
559
+ } catch {
560
+ this.updateMCPStatus(false);
561
+ this.updateMCPToolList([]);
562
+ }
563
+ }
564
+
565
+ updateMCPStatus(connected, toolCount = 0, mcpCount = 0) {
566
+ this.el.mcpStatus.innerHTML = connected ? `
567
+ <span class="status-indicator connected"></span>
568
+ ${mcpCount > 0 ? `ZAP: ${mcpCount} MCP(s)` : 'Connected'}
569
+ ` : `
570
+ <span class="status-indicator disconnected"></span>
571
+ Disconnected
572
+ `;
573
+ this.el.mcpTools.textContent = toolCount;
574
+ }
575
+
576
+ updateMCPToolList(tools) {
577
+ if (!this.el.mcpToolList) return;
578
+ if (!tools || !tools.length) {
579
+ this.el.mcpToolList.textContent = 'No tools discovered yet.';
580
+ return;
581
+ }
582
+
583
+ const names = Array.from(new Set(tools.map((tool) => tool.name))).sort();
584
+ const preview = names.slice(0, 20);
585
+ const suffix = names.length > preview.length ? `\n... +${names.length - preview.length} more` : '';
586
+ this.el.mcpToolList.textContent = `${preview.join('\n')}${suffix}`;
587
+ }
588
+
589
+ async refreshTabFilesystem() {
590
+ try {
591
+ const tabs = await chrome.tabs.query({});
592
+ this.el.tabFs.innerHTML = tabs.map((tab, i) => `
593
+ <div class="tab-item ${tab.active ? 'active' : ''}" data-tab-id="${tab.id}">
594
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
595
+ <path d="M2 2v12h12V2H2zm1 1h10v10H3V3z"/>
596
+ </svg>
597
+ <span title="${this.escapeHtml(tab.url || '')}">/tabs/${i}/${this.sanitizePath(tab.title || 'untitled')}</span>
598
+ </div>
599
+ `).join('');
600
+
601
+ this.el.tabFs.querySelectorAll('.tab-item').forEach(item => {
602
+ item.addEventListener('click', () => {
603
+ chrome.tabs.update(parseInt(item.dataset.tabId), { active: true });
604
+ });
605
+ });
606
+ } catch {}
607
+ }
608
+
609
+ sanitizePath(title) {
610
+ return title.replace(/[^a-zA-Z0-9-_]/g, '_').toLowerCase().substring(0, 30);
611
+ }
612
+
613
+ async checkWebGPU() {
614
+ try {
615
+ const response = await chrome.runtime.sendMessage({ action: 'checkWebGPU' });
616
+ if (response?.available) {
617
+ this.el.gpuStatus.innerHTML = `
618
+ <span class="status-indicator connected"></span>
619
+ Available
620
+ `;
621
+ this.el.gpuModel.textContent = response.adapter || 'Unknown';
622
+ } else {
623
+ this.el.gpuStatus.innerHTML = `
624
+ <span class="status-indicator disconnected"></span>
625
+ Not Available
626
+ `;
627
+ }
628
+ } catch {}
629
+ }
630
+
631
+ updateAgentList() {
632
+ const arr = Array.from(this.agents.values());
633
+ this.el.agentCount.textContent = arr.length;
634
+ if (arr.length === 0) {
635
+ this.el.agentList.innerHTML = '<div class="empty-state">No active agents</div>';
636
+ return;
637
+ }
638
+ this.el.agentList.innerHTML = arr.map(a => `
639
+ <div class="agent-item">
640
+ <div class="agent-icon">
641
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
642
+ <circle cx="12" cy="12" r="10" stroke-width="2"/>
643
+ <path d="M12 6v6l4 2" stroke-width="2"/>
644
+ </svg>
645
+ </div>
646
+ <div class="agent-info">
647
+ <div class="agent-name">${this.escapeHtml(a.name)}</div>
648
+ <div class="agent-status">${a.status} • Tab ${a.tabId}</div>
649
+ </div>
650
+ <div class="agent-actions">
651
+ <button class="icon-btn small" data-stop="${a.id}" title="Stop">
652
+ <svg viewBox="0 0 16 16" fill="currentColor">
653
+ <rect x="4" y="4" width="8" height="8" stroke-width="2"/>
654
+ </svg>
655
+ </button>
656
+ </div>
657
+ </div>
658
+ `).join('');
659
+
660
+ this.el.agentList.querySelectorAll('[data-stop]').forEach(btn => {
661
+ btn.addEventListener('click', () => {
662
+ const id = btn.dataset.stop;
663
+ chrome.runtime.sendMessage({ action: 'stopAgent', agentId: id });
664
+ this.agents.delete(id);
665
+ this.updateAgentList();
666
+ });
667
+ });
668
+ }
669
+
670
+ showAgentLauncher() {
671
+ // Create agent launcher modal overlay
672
+ const overlay = document.createElement('div');
673
+ overlay.className = 'modal-overlay';
674
+ overlay.innerHTML = `
675
+ <div class="modal-content">
676
+ <div class="modal-header">
677
+ <h3>Launch Agent</h3>
678
+ <button class="icon-btn close-modal">&times;</button>
679
+ </div>
680
+ <div class="modal-body">
681
+ <label class="setting-item">
682
+ <span>Target Tab</span>
683
+ <select id="agent-tab-select">
684
+ <option value="active">Active Tab</option>
685
+ </select>
686
+ </label>
687
+ <label class="setting-item">
688
+ <span>Agent Type</span>
689
+ <select id="agent-type-select">
690
+ <option value="browser-control">Browser Automation</option>
691
+ <option value="page-analyzer">Page Analyzer</option>
692
+ <option value="form-filler">Form Filler</option>
693
+ <option value="data-extractor">Data Extractor</option>
694
+ </select>
695
+ </label>
696
+ <label class="setting-item">
697
+ <span>Instructions</span>
698
+ <textarea id="agent-instructions" rows="3" placeholder="Describe what the agent should do..."></textarea>
699
+ </label>
700
+ </div>
701
+ <div class="modal-footer">
702
+ <button class="secondary-btn cancel-modal">Cancel</button>
703
+ <button class="primary-btn launch-modal">Launch</button>
704
+ </div>
705
+ </div>
706
+ `;
707
+ document.body.appendChild(overlay);
708
+
709
+ // Populate tabs
710
+ chrome.tabs.query({}, (tabs) => {
711
+ const select = overlay.querySelector('#agent-tab-select');
712
+ tabs.forEach(tab => {
713
+ if (tab.id) {
714
+ const opt = document.createElement('option');
715
+ opt.value = tab.id;
716
+ opt.textContent = `${tab.title?.substring(0, 40) || 'Untitled'} ${tab.active ? '(active)' : ''}`;
717
+ select.appendChild(opt);
718
+ }
719
+ });
720
+ });
721
+
722
+ // Close handlers
723
+ overlay.querySelector('.close-modal').addEventListener('click', () => overlay.remove());
724
+ overlay.querySelector('.cancel-modal').addEventListener('click', () => overlay.remove());
725
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
726
+
727
+ // Launch handler
728
+ overlay.querySelector('.launch-modal').addEventListener('click', async () => {
729
+ const tabSelect = overlay.querySelector('#agent-tab-select');
730
+ const typeSelect = overlay.querySelector('#agent-type-select');
731
+ const instructions = overlay.querySelector('#agent-instructions');
732
+
733
+ let tabId;
734
+ if (tabSelect.value === 'active') {
735
+ const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
736
+ tabId = activeTab?.id;
737
+ } else {
738
+ tabId = parseInt(tabSelect.value);
739
+ }
740
+
741
+ if (!tabId) {
742
+ this.showError('No tab selected');
743
+ return;
744
+ }
745
+
746
+ const agentType = typeSelect.value;
747
+ const agentInstructions = instructions.value?.trim() || '';
748
+ overlay.remove();
749
+
750
+ try {
751
+ const response = await chrome.runtime.sendMessage({
752
+ action: 'launchAIWorker',
753
+ tabId,
754
+ model: agentType,
755
+ instructions: agentInstructions,
756
+ });
757
+
758
+ if (response?.success) {
759
+ const agentId = response.agentId || `agent-${Date.now().toString(36)}`;
760
+ this.agents.set(agentId, {
761
+ id: agentId,
762
+ name: agentType,
763
+ tabId,
764
+ status: 'running',
765
+ });
766
+ this.updateAgentList();
767
+ this.showNotification(`Agent launched on tab ${tabId}`);
768
+ } else {
769
+ this.showError(response?.error || 'Failed to launch agent');
770
+ }
771
+ } catch (error) {
772
+ this.showError('Failed to launch agent: ' + error.message);
773
+ }
774
+ });
775
+ }
776
+
777
+ // ===========================================================================
778
+ // Settings Tab
779
+ // ===========================================================================
780
+
781
+ loadSettings() {
782
+ chrome.storage.local.get([
783
+ 'mcpPort',
784
+ 'cdpPort',
785
+ 'zapPorts',
786
+ 'hanzo_default_model',
787
+ 'hanzo_rag_endpoint',
788
+ 'hanzo_rag_api_key',
789
+ 'hanzo_rag_kb',
790
+ 'hanzo_rag_top_k',
791
+ 'hanzo_rag_include_tab_context',
792
+ 'hanzo_rag_use_zap',
793
+ 'hanzo_chat_rag_enabled',
794
+ 'hanzo_chat_tab_context_enabled',
795
+ ], (result) => {
796
+ const mcpPort = document.getElementById('mcp-port-setting');
797
+ const cdpPort = document.getElementById('cdp-port-setting');
798
+ const zapPorts = document.getElementById('zap-ports-setting');
799
+
800
+ if (mcpPort) mcpPort.value = result.mcpPort || 3001;
801
+ if (cdpPort) cdpPort.value = result.cdpPort || 9223;
802
+ if (zapPorts) zapPorts.value = (result.zapPorts || [9999,9998,9997,9996,9995]).join(',');
803
+ if (result.hanzo_default_model && this.el.defaultModel) {
804
+ this.el.defaultModel.value = result.hanzo_default_model;
805
+ }
806
+
807
+ if (this.el.ragEndpoint) this.el.ragEndpoint.value = result.hanzo_rag_endpoint || '';
808
+ if (this.el.ragApiKey) this.el.ragApiKey.value = result.hanzo_rag_api_key || '';
809
+ if (this.el.ragKb) this.el.ragKb.value = result.hanzo_rag_kb || '';
810
+ if (this.el.ragTopK) this.el.ragTopK.value = String(result.hanzo_rag_top_k || 5);
811
+ if (this.el.ragIncludeTabContext) this.el.ragIncludeTabContext.checked = result.hanzo_rag_include_tab_context !== false;
812
+ if (this.el.ragUseZap) this.el.ragUseZap.checked = result.hanzo_rag_use_zap !== false;
813
+ if (this.el.ragEnabled) this.el.ragEnabled.checked = result.hanzo_chat_rag_enabled !== false;
814
+ if (this.el.tabContextEnabled) this.el.tabContextEnabled.checked = result.hanzo_chat_tab_context_enabled !== false;
815
+ this.setRagStatus(this.el.ragEnabled?.checked ? 'Ready' : 'RAG disabled');
816
+ });
817
+ }
818
+
819
+ async saveSettings() {
820
+ const mcpPort = parseInt(document.getElementById('mcp-port-setting')?.value) || 3001;
821
+ const cdpPort = parseInt(document.getElementById('cdp-port-setting')?.value) || 9223;
822
+ const zapPorts = (document.getElementById('zap-ports-setting')?.value || '')
823
+ .split(',').map(p => parseInt(p.trim())).filter(p => p > 0 && p < 65536);
824
+
825
+ await chrome.storage.local.set({
826
+ mcpPort,
827
+ cdpPort,
828
+ zapPorts: zapPorts.length ? zapPorts : [9999,9998,9997,9996,9995],
829
+ hanzo_default_model: this.el.defaultModel?.value || 'gpt-4o',
830
+ 'safe-mode': document.getElementById('safe-mode')?.checked,
831
+ 'enable-webgpu': document.getElementById('enable-webgpu')?.checked,
832
+ maxAgents: parseInt(document.getElementById('max-agents')?.value) || 3,
833
+ hanzo_rag_endpoint: this.el.ragEndpoint?.value?.trim() || '',
834
+ hanzo_rag_api_key: this.el.ragApiKey?.value?.trim() || '',
835
+ hanzo_rag_kb: this.el.ragKb?.value?.trim() || '',
836
+ hanzo_rag_top_k: Math.max(1, Math.min(parseInt(this.el.ragTopK?.value || '5', 10) || 5, 20)),
837
+ hanzo_rag_include_tab_context: !!this.el.ragIncludeTabContext?.checked,
838
+ hanzo_rag_use_zap: !!this.el.ragUseZap?.checked,
839
+ hanzo_chat_rag_enabled: !!this.el.ragEnabled?.checked,
840
+ hanzo_chat_tab_context_enabled: !!this.el.tabContextEnabled?.checked,
841
+ });
842
+
843
+ this.showNotification('Settings saved');
844
+ }
845
+
846
+ // ===========================================================================
847
+ // Utility
848
+ // ===========================================================================
849
+
850
+ startMonitoring() {
851
+ setInterval(() => this.connectToMCP(), 10000);
852
+ chrome.tabs.onUpdated.addListener(() => this.refreshTabFilesystem());
853
+ chrome.tabs.onRemoved.addListener(() => this.refreshTabFilesystem());
854
+ }
855
+
856
+ handleMessage(request) {
857
+ switch (request.action) {
858
+ case 'agentUpdate':
859
+ if (request.agentId && this.agents.has(request.agentId)) {
860
+ this.agents.get(request.agentId).status = request.status;
861
+ this.updateAgentList();
862
+ }
863
+ break;
864
+ }
865
+ }
866
+
867
+ showNotification(message) {
868
+ const el = document.createElement('div');
869
+ el.className = 'notification';
870
+ el.textContent = message;
871
+ document.body.appendChild(el);
872
+ setTimeout(() => el.remove(), 3000);
873
+ }
874
+
875
+ showError(message) {
876
+ const el = document.createElement('div');
877
+ el.className = 'notification error';
878
+ el.textContent = message;
879
+ document.body.appendChild(el);
880
+ setTimeout(() => el.remove(), 5000);
881
+ }
882
+ }
883
+
884
+ // Initialize
885
+ const hanzoSidebar = new HanzoSidebar();