@hanzo/browser-extension 1.7.2

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