@hanzo/browser-extension 1.9.31 → 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 (62) hide show
  1. package/package.json +2 -1
  2. package/src/answer/AnswerEngine.tsx +553 -0
  3. package/src/manifest-firefox.json +10 -2
  4. package/src/manifest.json +17 -2
  5. package/src/newtab.css +277 -0
  6. package/src/newtab.html +13 -0
  7. package/src/popup.html +28 -0
  8. package/dist/browser-extension/README.md +0 -43
  9. package/dist/browser-extension/background-firefox.js +0 -4556
  10. package/dist/browser-extension/background.js +0 -7245
  11. package/dist/browser-extension/browser-control.js +0 -1317
  12. package/dist/browser-extension/chrome/ai-worker.js +0 -571
  13. package/dist/browser-extension/chrome/background.js +0 -7245
  14. package/dist/browser-extension/chrome/callback.html +0 -17
  15. package/dist/browser-extension/chrome/content-script.js +0 -2789
  16. package/dist/browser-extension/chrome/icon128.png +0 -0
  17. package/dist/browser-extension/chrome/icon16.png +0 -0
  18. package/dist/browser-extension/chrome/icon48.png +0 -0
  19. package/dist/browser-extension/chrome/manifest.json +0 -66
  20. package/dist/browser-extension/chrome/popup.css +0 -761
  21. package/dist/browser-extension/chrome/popup.html +0 -353
  22. package/dist/browser-extension/chrome/popup.js +0 -1650
  23. package/dist/browser-extension/chrome/sidebar.css +0 -1792
  24. package/dist/browser-extension/chrome/sidebar.html +0 -463
  25. package/dist/browser-extension/chrome/sidebar.js +0 -26541
  26. package/dist/browser-extension/cli.js +0 -233
  27. package/dist/browser-extension/firefox/ai-worker.js +0 -571
  28. package/dist/browser-extension/firefox/background.js +0 -4556
  29. package/dist/browser-extension/firefox/callback.html +0 -17
  30. package/dist/browser-extension/firefox/content-script.js +0 -2789
  31. package/dist/browser-extension/firefox/icon128.png +0 -0
  32. package/dist/browser-extension/firefox/icon16.png +0 -0
  33. package/dist/browser-extension/firefox/icon48.png +0 -0
  34. package/dist/browser-extension/firefox/manifest.json +0 -77
  35. package/dist/browser-extension/firefox/popup.css +0 -761
  36. package/dist/browser-extension/firefox/popup.html +0 -353
  37. package/dist/browser-extension/firefox/popup.js +0 -1650
  38. package/dist/browser-extension/firefox/sidebar.css +0 -1792
  39. package/dist/browser-extension/firefox/sidebar.html +0 -463
  40. package/dist/browser-extension/firefox/sidebar.js +0 -26541
  41. package/dist/browser-extension/icon128.png +0 -0
  42. package/dist/browser-extension/icon16.png +0 -0
  43. package/dist/browser-extension/icon48.png +0 -0
  44. package/dist/browser-extension/manifest.json +0 -66
  45. package/dist/browser-extension/package.json +0 -41
  46. package/dist/browser-extension/popup.js +0 -1650
  47. package/dist/browser-extension/safari/Info.plist +0 -21
  48. package/dist/browser-extension/safari/ai-worker.js +0 -571
  49. package/dist/browser-extension/safari/background.js +0 -7245
  50. package/dist/browser-extension/safari/callback.html +0 -17
  51. package/dist/browser-extension/safari/content-script.js +0 -2789
  52. package/dist/browser-extension/safari/icon128.png +0 -0
  53. package/dist/browser-extension/safari/icon16.png +0 -0
  54. package/dist/browser-extension/safari/icon48.png +0 -0
  55. package/dist/browser-extension/safari/popup.css +0 -761
  56. package/dist/browser-extension/safari/popup.html +0 -353
  57. package/dist/browser-extension/safari/popup.js +0 -1650
  58. package/dist/browser-extension/safari/sidebar.css +0 -1792
  59. package/dist/browser-extension/safari/sidebar.html +0 -463
  60. package/dist/browser-extension/safari/sidebar.js +0 -26541
  61. package/dist/browser-extension/sidebar.js +0 -26541
  62. package/dist/browser-extension/webgpu-ai.js +0 -568
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@hanzo/browser-extension",
3
- "version": "1.9.31",
3
+ "version": "1.9.33",
4
4
  "description": "Hanzo AI Browser Extension",
5
5
  "main": "dist/background.js",
6
6
  "dependencies": {
7
+ "@hanzo/ai": "^0.2.3",
7
8
  "@supabase/supabase-js": "^2.50.3",
8
9
  "axios": "^1.10.0",
9
10
  "chalk": "^4.1.2",
@@ -0,0 +1,553 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { createAiClient, type SearchEvent, type SearchMode, type SearchSource } from '@hanzo/ai';
3
+ import { fetchNews, type NewsItem } from './news.js';
4
+ import { AUTO_MODEL, fetchModelCatalog, type ChatModel, type ModelGroup } from './model-catalog.js';
5
+ import { renderMarkdown } from './markdown.js';
6
+
7
+ /**
8
+ * Hanzo Answer Engine — the shared AI-answer surface (query → streamed answer +
9
+ * inline citations + sources + live news). Backend = api.hanzo.ai ONLY, reached
10
+ * through the @hanzo/ai `search`/`deepResearch` primitive (the one place the
11
+ * capability is defined). Rendered on the @hanzo/gui primitive surface; strict
12
+ * monochrome per the Hanzo brand.
13
+ */
14
+
15
+ export interface AnswerEngineProps {
16
+ apiBase: string;
17
+ /** Resolves the current IAM bearer, or null when signed out. */
18
+ getToken: () => Promise<string | null>;
19
+ /** Kicks off IAM sign-in (opens the OAuth tab). */
20
+ signIn: () => void;
21
+ /** Optional initial query (from the omnibox / address bar `?q=`). */
22
+ initialQuery?: string;
23
+ }
24
+
25
+ const MODES: { id: SearchMode; label: string; hint: string }[] = [
26
+ { id: 'search', label: 'Search', hint: 'Fast grounded answer' },
27
+ { id: 'news', label: 'News', hint: 'Recency-biased' },
28
+ { id: 'research', label: 'Research', hint: 'Multi-source synthesis' },
29
+ { id: 'deep', label: 'Deep', hint: 'Plan + wide research' },
30
+ ];
31
+
32
+ const SOURCE_CHIPS = ['web', 'news', 'academic', 'github', 'reddit', 'x'];
33
+
34
+ // Hanzo tiers mirror @hanzo/plans (the SoT for pricing); shown in the picker's
35
+ // upgrade banner. Full plan detail lives at console.hanzo.ai/pricing.
36
+ const PLAN_TIERS = [
37
+ { name: 'Pro', price: '$20' },
38
+ { name: 'Max', price: '$100' },
39
+ { name: 'Ultra', price: '$200' },
40
+ ];
41
+
42
+ function HanzoMark({ size = 28 }: { size?: number }) {
43
+ return (
44
+ <svg viewBox="0 0 67 67" width={size} height={size} xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
45
+ <path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor" />
46
+ <path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="currentColor" />
47
+ <path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor" />
48
+ <path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor" />
49
+ <path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor" />
50
+ </svg>
51
+ );
52
+ }
53
+
54
+ function ModelPicker({
55
+ groups,
56
+ current,
57
+ onPick,
58
+ }: {
59
+ groups: ModelGroup[];
60
+ current: ChatModel;
61
+ onPick: (m: ChatModel) => void;
62
+ }) {
63
+ const [open, setOpen] = useState(false);
64
+ const [filter, setFilter] = useState('');
65
+ const ref = useRef<HTMLDivElement>(null);
66
+
67
+ useEffect(() => {
68
+ if (!open) return;
69
+ const onDoc = (e: MouseEvent) => {
70
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
71
+ };
72
+ document.addEventListener('mousedown', onDoc);
73
+ return () => document.removeEventListener('mousedown', onDoc);
74
+ }, [open]);
75
+
76
+ const q = filter.trim().toLowerCase();
77
+ const shown = useMemo(
78
+ () =>
79
+ groups
80
+ .map((g) => ({
81
+ ...g,
82
+ models: g.models.filter(
83
+ (m) => !q || m.name.toLowerCase().includes(q) || m.id.toLowerCase().includes(q),
84
+ ),
85
+ }))
86
+ .filter((g) => g.models.length),
87
+ [groups, q],
88
+ );
89
+
90
+ return (
91
+ <div className="ae-picker" ref={ref}>
92
+ <button className="ae-picker-btn" onClick={() => setOpen((v) => !v)} title="Choose model">
93
+ <span className="ae-dot" />
94
+ {current.name}
95
+ <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
96
+ <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="1.5" />
97
+ </svg>
98
+ </button>
99
+ {open && (
100
+ <div className="ae-picker-menu">
101
+ <input
102
+ className="ae-picker-filter"
103
+ placeholder="Search models…"
104
+ value={filter}
105
+ autoFocus
106
+ onChange={(e) => setFilter(e.target.value)}
107
+ />
108
+ <div className="ae-picker-list">
109
+ <button
110
+ className={'ae-model-row' + (current.id === AUTO_MODEL.id ? ' active' : '')}
111
+ onClick={() => {
112
+ onPick(AUTO_MODEL);
113
+ setOpen(false);
114
+ }}
115
+ >
116
+ <span className="ae-model-name">Auto</span>
117
+ <span className="ae-model-desc">Routes to the best model</span>
118
+ </button>
119
+ {shown.map((g) => (
120
+ <div key={g.family} className="ae-model-group">
121
+ <div className="ae-model-group-label">{g.label}</div>
122
+ {g.models.map((m) => (
123
+ <button
124
+ key={m.id}
125
+ className={'ae-model-row' + (current.id === m.id ? ' active' : '')}
126
+ onClick={() => {
127
+ onPick(m);
128
+ setOpen(false);
129
+ }}
130
+ >
131
+ <span className="ae-model-name">
132
+ {m.name}
133
+ {m.tier && <span className="ae-badge">{m.tier}</span>}
134
+ </span>
135
+ {m.description && <span className="ae-model-desc">{m.description}</span>}
136
+ </button>
137
+ ))}
138
+ </div>
139
+ ))}
140
+ </div>
141
+ <a className="ae-plan-banner" href="https://console.hanzo.ai/pricing" target="_blank" rel="noreferrer">
142
+ <span>Unlock every model</span>
143
+ <span className="ae-plan-tiers">
144
+ {PLAN_TIERS.map((t) => (
145
+ <span key={t.name} className="ae-plan-tier">
146
+ {t.name} <b>{t.price}</b>
147
+ </span>
148
+ ))}
149
+ </span>
150
+ </a>
151
+ </div>
152
+ )}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ function SourceCard({ s, index }: { s: SearchSource; index: number }) {
158
+ let host = '';
159
+ try {
160
+ host = new URL(s.url).hostname.replace(/^www\./, '');
161
+ } catch {
162
+ host = s.url;
163
+ }
164
+ return (
165
+ <a className="ae-source" href={s.url} target="_blank" rel="noreferrer" title={s.title}>
166
+ <span className="ae-source-head">
167
+ {s.favicon ? <img src={s.favicon} alt="" width="14" height="14" /> : <span className="ae-dot" />}
168
+ <span className="ae-source-host">{host}</span>
169
+ <span className="ae-source-idx">{index + 1}</span>
170
+ </span>
171
+ <span className="ae-source-title">{s.title}</span>
172
+ </a>
173
+ );
174
+ }
175
+
176
+ function InputCard({
177
+ value,
178
+ onChange,
179
+ onSubmit,
180
+ mode,
181
+ setMode,
182
+ sources,
183
+ toggleSource,
184
+ models,
185
+ model,
186
+ setModel,
187
+ busy,
188
+ }: {
189
+ value: string;
190
+ onChange: (v: string) => void;
191
+ onSubmit: () => void;
192
+ mode: SearchMode;
193
+ setMode: (m: SearchMode) => void;
194
+ sources: Set<string>;
195
+ toggleSource: (s: string) => void;
196
+ models: ModelGroup[];
197
+ model: ChatModel;
198
+ setModel: (m: ChatModel) => void;
199
+ busy: boolean;
200
+ }) {
201
+ const taRef = useRef<HTMLTextAreaElement>(null);
202
+ useEffect(() => {
203
+ const ta = taRef.current;
204
+ if (!ta) return;
205
+ ta.style.height = 'auto';
206
+ ta.style.height = Math.min(ta.scrollHeight, 200) + 'px';
207
+ }, [value]);
208
+
209
+ return (
210
+ <div className="ae-card">
211
+ <textarea
212
+ ref={taRef}
213
+ className="ae-input"
214
+ placeholder="Type @ for sources or / for modes"
215
+ value={value}
216
+ rows={1}
217
+ onChange={(e) => onChange(e.target.value)}
218
+ onKeyDown={(e) => {
219
+ if (e.key === 'Enter' && !e.shiftKey) {
220
+ e.preventDefault();
221
+ onSubmit();
222
+ }
223
+ }}
224
+ />
225
+ <div className="ae-card-row">
226
+ <div className="ae-chips">
227
+ {SOURCE_CHIPS.map((s) => (
228
+ <button
229
+ key={s}
230
+ className={'ae-chip' + (sources.has(s) ? ' on' : '')}
231
+ onClick={() => toggleSource(s)}
232
+ title={`@${s}`}
233
+ >
234
+ @{s}
235
+ </button>
236
+ ))}
237
+ </div>
238
+ <div className="ae-card-right">
239
+ <div className="ae-modes">
240
+ {MODES.map((m) => (
241
+ <button
242
+ key={m.id}
243
+ className={'ae-mode' + (mode === m.id ? ' on' : '')}
244
+ onClick={() => setMode(m.id)}
245
+ title={m.hint}
246
+ >
247
+ {m.label}
248
+ </button>
249
+ ))}
250
+ </div>
251
+ <ModelPicker groups={models} current={model} onPick={setModel} />
252
+ <button className="ae-send" onClick={onSubmit} disabled={busy || !value.trim()} title="Search (Enter)">
253
+ {busy ? (
254
+ <span className="ae-spinner" />
255
+ ) : (
256
+ <svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true">
257
+ <path d="M3 13V3l10 5-10 5z" fill="currentColor" />
258
+ </svg>
259
+ )}
260
+ </button>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ );
265
+ }
266
+
267
+ export function AnswerEngine({ apiBase, getToken, signIn, initialQuery }: AnswerEngineProps) {
268
+ const [collapsed, setCollapsed] = useState(false);
269
+ const [query, setQuery] = useState('');
270
+ const [submitted, setSubmitted] = useState('');
271
+ const [mode, setMode] = useState<SearchMode>('search');
272
+ const [sources, setSources] = useState<Set<string>>(new Set());
273
+ const [modelGroups, setModelGroups] = useState<ModelGroup[]>([]);
274
+ const [model, setModel] = useState<ChatModel>(AUTO_MODEL);
275
+ const [news, setNews] = useState<NewsItem[]>([]);
276
+
277
+ const [phase, setPhase] = useState<'idle' | 'running' | 'done'>('idle');
278
+ const [status, setStatus] = useState('');
279
+ const [answer, setAnswer] = useState('');
280
+ const [answerSources, setAnswerSources] = useState<SearchSource[]>([]);
281
+ const [followUps, setFollowUps] = useState<string[]>([]);
282
+ const [error, setError] = useState('');
283
+ const [needAuth, setNeedAuth] = useState(false);
284
+ const abortRef = useRef<AbortController | null>(null);
285
+
286
+ // Load real model catalog + live news (both public — work signed out).
287
+ // Default stays on Auto ("routes to the best model"); the picker changes it.
288
+ useEffect(() => {
289
+ fetchModelCatalog(apiBase)
290
+ .then(({ groups }) => setModelGroups(groups))
291
+ .catch(() => {});
292
+ fetchNews().then(setNews).catch(() => {});
293
+ }, [apiBase]);
294
+
295
+ const client = useMemo(
296
+ () =>
297
+ createAiClient({
298
+ baseUrl: apiBase,
299
+ getToken: async () => {
300
+ const t = await getToken();
301
+ if (!t) throw new Error('__NO_TOKEN__');
302
+ return t;
303
+ },
304
+ }),
305
+ [apiBase, getToken],
306
+ );
307
+
308
+ const run = useCallback(
309
+ async (q: string) => {
310
+ const question = q.trim();
311
+ if (!question) return;
312
+ abortRef.current?.abort();
313
+ const ctrl = new AbortController();
314
+ abortRef.current = ctrl;
315
+
316
+ setSubmitted(question);
317
+ setPhase('running');
318
+ setStatus('Searching the web');
319
+ setAnswer('');
320
+ setAnswerSources([]);
321
+ setFollowUps([]);
322
+ setError('');
323
+ setNeedAuth(false);
324
+
325
+ const opts = {
326
+ mode,
327
+ model: model.id,
328
+ sources: [...sources],
329
+ signal: ctrl.signal,
330
+ };
331
+ try {
332
+ for await (const ev of client.search(question, opts) as AsyncGenerator<SearchEvent>) {
333
+ switch (ev.type) {
334
+ case 'status':
335
+ setStatus(
336
+ ev.stage === 'planning'
337
+ ? 'Planning research'
338
+ : ev.stage === 'searching'
339
+ ? 'Searching the web' + (ev.detail ? `: ${ev.detail}` : '')
340
+ : ev.stage === 'reading'
341
+ ? 'Reading sources'
342
+ : 'Writing the answer',
343
+ );
344
+ break;
345
+ case 'sources':
346
+ setAnswerSources(ev.sources);
347
+ break;
348
+ case 'text':
349
+ setAnswer((a) => a + ev.delta);
350
+ break;
351
+ case 'follow_ups':
352
+ setFollowUps(ev.questions);
353
+ break;
354
+ case 'done':
355
+ setPhase('done');
356
+ break;
357
+ case 'error':
358
+ if (ev.error.includes('__NO_TOKEN__') || /401|invalid api key/i.test(ev.error)) {
359
+ setNeedAuth(true);
360
+ } else {
361
+ setError(ev.error);
362
+ }
363
+ setPhase('done');
364
+ break;
365
+ }
366
+ }
367
+ } catch (e) {
368
+ const msg = e instanceof Error ? e.message : String(e);
369
+ if (msg.includes('__NO_TOKEN__') || /401|invalid api key/i.test(msg)) setNeedAuth(true);
370
+ else setError(msg);
371
+ setPhase('done');
372
+ }
373
+ },
374
+ [client, mode, model, sources],
375
+ );
376
+
377
+ // Fire the initial (address-bar) query once models are ready enough.
378
+ const didInit = useRef(false);
379
+ useEffect(() => {
380
+ if (didInit.current || !initialQuery) return;
381
+ didInit.current = true;
382
+ setQuery(initialQuery);
383
+ void run(initialQuery);
384
+ }, [initialQuery, run]);
385
+
386
+ const submit = useCallback(() => void run(query), [query, run]);
387
+ const toggleSource = useCallback((s: string) => {
388
+ setSources((prev) => {
389
+ const next = new Set(prev);
390
+ next.has(s) ? next.delete(s) : next.add(s);
391
+ return next;
392
+ });
393
+ }, []);
394
+
395
+ const newThread = useCallback(() => {
396
+ abortRef.current?.abort();
397
+ setPhase('idle');
398
+ setQuery('');
399
+ setSubmitted('');
400
+ setAnswer('');
401
+ setAnswerSources([]);
402
+ setFollowUps([]);
403
+ setError('');
404
+ setNeedAuth(false);
405
+ }, []);
406
+
407
+ return (
408
+ <div className={'ae-root' + (collapsed ? ' collapsed' : '')}>
409
+ <aside className="ae-sidebar">
410
+ <div className="ae-side-top">
411
+ <button className="ae-brand" onClick={newThread} title="Hanzo">
412
+ <HanzoMark size={22} />
413
+ {!collapsed && <span>Hanzo</span>}
414
+ </button>
415
+ <button className="ae-collapse" onClick={() => setCollapsed((v) => !v)} title="Toggle sidebar">
416
+ <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
417
+ <path d="M6 3v10M3 3h10v10H3z" fill="none" stroke="currentColor" strokeWidth="1.3" />
418
+ </svg>
419
+ </button>
420
+ </div>
421
+ <button className="ae-new" onClick={newThread}>
422
+ <svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true">
423
+ <path d="M8 3v10M3 8h10" fill="none" stroke="currentColor" strokeWidth="1.5" />
424
+ </svg>
425
+ {!collapsed && <span>New thread</span>}
426
+ </button>
427
+ <nav className="ae-nav">
428
+ <a href="https://hanzo.chat" target="_blank" rel="noreferrer">Threads</a>
429
+ <a href="https://docs.hanzo.ai" target="_blank" rel="noreferrer">Docs</a>
430
+ <a href="https://hanzo.ai" target="_blank" rel="noreferrer">About</a>
431
+ </nav>
432
+ <div className="ae-side-bottom">
433
+ <button className="ae-signin" onClick={signIn}>
434
+ {collapsed ? '↪' : 'Sign in — free to start'}
435
+ </button>
436
+ </div>
437
+ </aside>
438
+
439
+ <main className="ae-main">
440
+ {phase === 'idle' ? (
441
+ <div className="ae-hero">
442
+ <div className="ae-wordmark">
443
+ <HanzoMark size={44} />
444
+ <span>Hanzo</span>
445
+ </div>
446
+ <div className="ae-hero-card">
447
+ <InputCard
448
+ value={query}
449
+ onChange={setQuery}
450
+ onSubmit={submit}
451
+ mode={mode}
452
+ setMode={setMode}
453
+ sources={sources}
454
+ toggleSource={toggleSource}
455
+ models={modelGroups}
456
+ model={model}
457
+ setModel={setModel}
458
+ busy={false}
459
+ />
460
+ </div>
461
+ {news.length > 0 && (
462
+ <div className="ae-news">
463
+ <div className="ae-news-head">Top stories</div>
464
+ <div className="ae-news-grid">
465
+ {news.slice(0, 6).map((n) => (
466
+ <a key={n.url} className="ae-news-item" href={n.url} target="_blank" rel="noreferrer">
467
+ <span className="ae-news-title">{n.title}</span>
468
+ <span className="ae-news-meta">
469
+ {n.source}
470
+ {typeof n.points === 'number' ? ` · ${n.points} pts` : ''}
471
+ </span>
472
+ </a>
473
+ ))}
474
+ </div>
475
+ </div>
476
+ )}
477
+ </div>
478
+ ) : (
479
+ <div className="ae-answer-wrap">
480
+ <div className="ae-answer-scroll">
481
+ <h1 className="ae-question">{submitted}</h1>
482
+
483
+ {answerSources.length > 0 && (
484
+ <div className="ae-sources">
485
+ {answerSources.map((s, i) => (
486
+ <SourceCard key={s.url} s={s} index={i} />
487
+ ))}
488
+ </div>
489
+ )}
490
+
491
+ {phase === 'running' && !answer && (
492
+ <div className="ae-status">
493
+ <span className="ae-spinner" /> {status}…
494
+ </div>
495
+ )}
496
+
497
+ {needAuth ? (
498
+ <div className="ae-auth">
499
+ <p>Sign in with Hanzo to get AI answers.</p>
500
+ <button className="ae-signin big" onClick={signIn}>
501
+ Sign in — free to start
502
+ </button>
503
+ </div>
504
+ ) : error ? (
505
+ <div className="ae-error">{error}</div>
506
+ ) : (
507
+ <div
508
+ className="ae-answer markdown"
509
+ dangerouslySetInnerHTML={{ __html: renderMarkdown(answer) }}
510
+ />
511
+ )}
512
+
513
+ {followUps.length > 0 && (
514
+ <div className="ae-followups">
515
+ <div className="ae-followups-head">Follow-up</div>
516
+ {followUps.map((f) => (
517
+ <button
518
+ key={f}
519
+ className="ae-followup"
520
+ onClick={() => {
521
+ setQuery(f);
522
+ void run(f);
523
+ }}
524
+ >
525
+ {f}
526
+ <span className="ae-followup-arrow">→</span>
527
+ </button>
528
+ ))}
529
+ </div>
530
+ )}
531
+ </div>
532
+
533
+ <div className="ae-answer-composer">
534
+ <InputCard
535
+ value={query}
536
+ onChange={setQuery}
537
+ onSubmit={submit}
538
+ mode={mode}
539
+ setMode={setMode}
540
+ sources={sources}
541
+ toggleSource={toggleSource}
542
+ models={modelGroups}
543
+ model={model}
544
+ setModel={setModel}
545
+ busy={phase === 'running'}
546
+ />
547
+ </div>
548
+ </div>
549
+ )}
550
+ </main>
551
+ </div>
552
+ );
553
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Hanzo AI",
4
- "version": "1.9.22",
5
- "description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
4
+ "version": "1.9.33",
5
+ "description": "AI answer engine, search, and dev assistant — Hanzo AI in a new tab, address-bar search, source-cited answers, realtime news, WebGPU AI, and MCP.",
6
6
  "permissions": [
7
7
  "activeTab",
8
8
  "identity",
@@ -17,6 +17,8 @@
17
17
  "host_permissions": [
18
18
  "http://localhost/*",
19
19
  "https://localhost/*",
20
+ "https://claude.ai/*",
21
+ "https://chatgpt.com/*",
20
22
  "<all_urls>"
21
23
  ],
22
24
  "content_security_policy": {
@@ -47,6 +49,12 @@
47
49
  "128": "icon128.png"
48
50
  }
49
51
  },
52
+ "chrome_url_overrides": {
53
+ "newtab": "newtab.html"
54
+ },
55
+ "omnibox": {
56
+ "keyword": "hanzo"
57
+ },
50
58
  "sidebar_action": {
51
59
  "default_title": "Hanzo AI",
52
60
  "default_panel": "sidebar.html",
package/src/manifest.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Hanzo AI",
4
- "version": "1.9.25",
5
- "description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
4
+ "version": "1.9.33",
5
+ "description": "AI answer engine, search, and dev assistant — Hanzo AI in a new tab, address-bar search, source-cited answers, realtime news, WebGPU AI, and MCP.",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxfXGh0lPUT5m04GKfjUwrLsV6pLaK3VcZuFRPogqAir2tzyLYnQPRtHynue9yvDyguIVnlkwvcwfDOYZrvH76zbw4s6onPBG8HqTKO6LQ9K3kdO1qBBkMMjdOgULQ1MrWThEbpU7NSTiwLYpEta/jAvrKRCAeKIlQE8p6htZmPy9aRUZuae66JgLcAlzD2vviX9sVB1asFABJVswL1RgZ55/8IzZaUrFjzOo9OHK4hmEOtudzkML+5silsAYdC+1BZugph2x94ai17YmZTCL1XyUa5Ke4q80cj+i9rOTgzhZs+mruyhL/AvNVOXilsgqCdNqSz77naWzC3pVGbxOewIDAQAB",
7
7
  "permissions": [
8
8
  "activeTab",
@@ -19,6 +19,8 @@
19
19
  "host_permissions": [
20
20
  "http://localhost/*",
21
21
  "https://localhost/*",
22
+ "https://claude.ai/*",
23
+ "https://chatgpt.com/*",
22
24
  "<all_urls>"
23
25
  ],
24
26
  "content_security_policy": {
@@ -52,6 +54,19 @@
52
54
  "48": "icon48.png",
53
55
  "128": "icon128.png"
54
56
  },
57
+ "chrome_url_overrides": {
58
+ "newtab": "newtab.html"
59
+ },
60
+ "chrome_settings_overrides": {
61
+ "search_provider": {
62
+ "name": "Hanzo",
63
+ "keyword": "hanzo",
64
+ "search_url": "https://hanzo.chat/c/new?q={searchTerms}&submit=true",
65
+ "favicon_url": "https://hanzo.ai/favicon.ico",
66
+ "encoding": "UTF-8",
67
+ "is_default": true
68
+ }
69
+ },
55
70
  "web_accessible_resources": [
56
71
  {
57
72
  "resources": [