@hanzo/browser-extension 1.7.6 → 1.9.33

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 (144) hide show
  1. package/LICENSE +9 -18
  2. package/dist/browser-extension/content-script.js +1642 -123
  3. package/images/icon128.png +0 -0
  4. package/images/icon16.png +0 -0
  5. package/images/icon48.png +0 -0
  6. package/manifest.json +7 -2
  7. package/package.json +28 -10
  8. package/src/answer/AnswerEngine.tsx +553 -0
  9. package/src/chat-widget.tsx +96 -0
  10. package/src/gui-primitives.tsx +116 -0
  11. package/src/icon128.png +0 -0
  12. package/src/icon16.png +0 -0
  13. package/src/icon32.png +0 -0
  14. package/src/icon48.png +0 -0
  15. package/src/manifest-firefox.json +32 -13
  16. package/src/manifest.json +35 -11
  17. package/src/newtab.css +277 -0
  18. package/src/newtab.html +13 -0
  19. package/src/package.json +1 -1
  20. package/src/popup.css +352 -110
  21. package/src/popup.html +186 -47
  22. package/src/popup.js +196 -0
  23. package/src/sidebar.css +772 -156
  24. package/src/sidebar.html +268 -85
  25. package/src/sidebar.js +885 -0
  26. package/.github/workflows/docs.yml +0 -44
  27. package/.github/workflows/release.yml +0 -55
  28. package/README.md +0 -126
  29. package/dist/browser-extension/README.md +0 -43
  30. package/dist/browser-extension/ai-worker.js +0 -30
  31. package/dist/browser-extension/background-firefox.js +0 -875
  32. package/dist/browser-extension/background.js +0 -2732
  33. package/dist/browser-extension/browser-control.js +0 -683
  34. package/dist/browser-extension/cdp-bridge-server.js +0 -444
  35. package/dist/browser-extension/chrome/ai-worker.js +0 -571
  36. package/dist/browser-extension/chrome/background.js +0 -2732
  37. package/dist/browser-extension/chrome/callback.html +0 -17
  38. package/dist/browser-extension/chrome/content-script.js +0 -1270
  39. package/dist/browser-extension/chrome/icon128.png +0 -0
  40. package/dist/browser-extension/chrome/icon16.png +0 -0
  41. package/dist/browser-extension/chrome/icon32.png +0 -0
  42. package/dist/browser-extension/chrome/icon48.png +0 -0
  43. package/dist/browser-extension/chrome/manifest.json +0 -57
  44. package/dist/browser-extension/chrome/popup.css +0 -519
  45. package/dist/browser-extension/chrome/popup.html +0 -242
  46. package/dist/browser-extension/chrome/popup.js +0 -278
  47. package/dist/browser-extension/chrome/sidebar.css +0 -1176
  48. package/dist/browser-extension/chrome/sidebar.html +0 -280
  49. package/dist/browser-extension/chrome/sidebar.js +0 -779
  50. package/dist/browser-extension/cli.js +0 -233
  51. package/dist/browser-extension/firefox/ai-worker.js +0 -571
  52. package/dist/browser-extension/firefox/background.js +0 -875
  53. package/dist/browser-extension/firefox/callback.html +0 -17
  54. package/dist/browser-extension/firefox/content-script.js +0 -1270
  55. package/dist/browser-extension/firefox/icon128.png +0 -0
  56. package/dist/browser-extension/firefox/icon16.png +0 -0
  57. package/dist/browser-extension/firefox/icon32.png +0 -0
  58. package/dist/browser-extension/firefox/icon48.png +0 -0
  59. package/dist/browser-extension/firefox/manifest.json +0 -66
  60. package/dist/browser-extension/firefox/popup.css +0 -519
  61. package/dist/browser-extension/firefox/popup.html +0 -242
  62. package/dist/browser-extension/firefox/popup.js +0 -278
  63. package/dist/browser-extension/firefox/sidebar.css +0 -1176
  64. package/dist/browser-extension/firefox/sidebar.html +0 -280
  65. package/dist/browser-extension/firefox/sidebar.js +0 -779
  66. package/dist/browser-extension/hanzo-browser-extension-firefox-1.1.0.zip +0 -0
  67. package/dist/browser-extension/icon128.png +0 -0
  68. package/dist/browser-extension/icon16.png +0 -0
  69. package/dist/browser-extension/icon32.png +0 -0
  70. package/dist/browser-extension/icon48.png +0 -0
  71. package/dist/browser-extension/manifest.json +0 -57
  72. package/dist/browser-extension/package.json +0 -41
  73. package/dist/browser-extension/popup.css +0 -468
  74. package/dist/browser-extension/popup.html +0 -191
  75. package/dist/browser-extension/popup.js +0 -278
  76. package/dist/browser-extension/safari/Info.plist +0 -21
  77. package/dist/browser-extension/safari/ai-worker.js +0 -571
  78. package/dist/browser-extension/safari/background.js +0 -2732
  79. package/dist/browser-extension/safari/callback.html +0 -17
  80. package/dist/browser-extension/safari/content-script.js +0 -1270
  81. package/dist/browser-extension/safari/icon128.png +0 -0
  82. package/dist/browser-extension/safari/icon16.png +0 -0
  83. package/dist/browser-extension/safari/icon32.png +0 -0
  84. package/dist/browser-extension/safari/icon48.png +0 -0
  85. package/dist/browser-extension/safari/popup.css +0 -519
  86. package/dist/browser-extension/safari/popup.html +0 -242
  87. package/dist/browser-extension/safari/popup.js +0 -278
  88. package/dist/browser-extension/safari/sidebar.css +0 -1176
  89. package/dist/browser-extension/safari/sidebar.html +0 -280
  90. package/dist/browser-extension/safari/sidebar.js +0 -779
  91. package/dist/browser-extension/sidebar.css +0 -718
  92. package/dist/browser-extension/sidebar.html +0 -184
  93. package/dist/browser-extension/sidebar.js +0 -779
  94. package/dist/browser-extension/webgpu-ai.js +0 -495
  95. package/dist/cdp-bridge-server.js +0 -411
  96. package/dist/hanzo-ai-chrome-1.7.2.zip +0 -0
  97. package/dist/hanzo-ai-chrome-1.7.3.zip +0 -0
  98. package/dist/hanzo-ai-firefox-1.7.2.zip +0 -0
  99. package/dist/hanzo-ai-firefox-1.7.3.zip +0 -0
  100. package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.pbxproj +0 -1047
  101. package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  102. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json +0 -11
  103. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -98
  104. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png +0 -0
  105. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png +0 -0
  106. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png +0 -0
  107. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png +0 -0
  108. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png +0 -0
  109. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png +0 -0
  110. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png +0 -0
  111. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png +0 -0
  112. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png +0 -0
  113. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png +0 -0
  114. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/universal-icon-1024@1x.png +0 -0
  115. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/Contents.json +0 -6
  116. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json +0 -21
  117. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon128.png +0 -0
  118. package/dist/safari/Hanzo AI/Shared (App)/Resources/Base.lproj/Main.html +0 -20
  119. package/dist/safari/Hanzo AI/Shared (App)/Resources/Icon.png +0 -0
  120. package/dist/safari/Hanzo AI/Shared (App)/Resources/Script.js +0 -24
  121. package/dist/safari/Hanzo AI/Shared (App)/Resources/Style.css +0 -61
  122. package/dist/safari/Hanzo AI/Shared (App)/ViewController.swift +0 -81
  123. package/dist/safari/Hanzo AI/Shared (Extension)/SafariWebExtensionHandler.swift +0 -42
  124. package/dist/safari/Hanzo AI/iOS (App)/AppDelegate.swift +0 -24
  125. package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/LaunchScreen.storyboard +0 -36
  126. package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/Main.storyboard +0 -38
  127. package/dist/safari/Hanzo AI/iOS (App)/Info.plist +0 -27
  128. package/dist/safari/Hanzo AI/iOS (App)/SceneDelegate.swift +0 -18
  129. package/dist/safari/Hanzo AI/iOS (Extension)/Info.plist +0 -13
  130. package/dist/safari/Hanzo AI/macOS (App)/AppDelegate.swift +0 -21
  131. package/dist/safari/Hanzo AI/macOS (App)/Base.lproj/Main.storyboard +0 -124
  132. package/dist/safari/Hanzo AI/macOS (App)/Info.plist +0 -8
  133. package/dist/safari/Hanzo AI/macOS (Extension)/Info.plist +0 -13
  134. package/docs/_config.yml +0 -13
  135. package/docs/_layouts/default.html +0 -435
  136. package/docs/index.md +0 -54
  137. package/hanzo-ai-chrome-v1.7.6.zip +0 -0
  138. package/hanzo-ai-firefox-v1.7.6.zip +0 -0
  139. package/install-extension.sh +0 -75
  140. package/test/claude-integration.test.ts +0 -251
  141. package/test/server-frameworks-edge-cases.test.ts +0 -145
  142. package/test/server-frameworks.test.ts +0 -395
  143. package/test-cdp-connection.js +0 -140
  144. 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();