bangonit 0.4.3 → 0.5.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 (76) hide show
  1. package/README.md +2 -1
  2. package/app/desktopapp/dist/main/index.js +10 -4
  3. package/app/desktopapp/dist/main/ipc.js +9 -24
  4. package/app/desktopapp/dist/main/preload.js +0 -7
  5. package/app/desktopapp/dist/main/tabs.js +10 -5
  6. package/app/desktopapp/package.json +0 -1
  7. package/app/replay/dist/replay.js +2 -2
  8. package/app/webapp/.next/standalone/app/webapp/.next/BUILD_ID +1 -1
  9. package/app/webapp/.next/standalone/app/webapp/.next/app-build-manifest.json +11 -11
  10. package/app/webapp/.next/standalone/app/webapp/.next/app-path-routes-manifest.json +1 -1
  11. package/app/webapp/.next/standalone/app/webapp/.next/build-manifest.json +3 -3
  12. package/app/webapp/.next/standalone/app/webapp/.next/prerender-manifest.json +1 -1
  13. package/app/webapp/.next/standalone/app/webapp/.next/required-server-files.json +1 -1
  14. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found/page.js +1 -1
  15. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.html +1 -1
  17. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.rsc +1 -1
  18. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js +1 -1
  19. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js +1 -1
  20. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app/page.js +6 -6
  21. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app/page_client-reference-manifest.js +1 -1
  22. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.html +1 -1
  23. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.rsc +2 -2
  24. package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.html +1 -1
  25. package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.rsc +1 -1
  26. package/app/webapp/.next/standalone/app/webapp/.next/server/app/page.js +1 -1
  27. package/app/webapp/.next/standalone/app/webapp/.next/server/app/page_client-reference-manifest.js +1 -1
  28. package/app/webapp/.next/standalone/app/webapp/.next/server/app-paths-manifest.json +1 -1
  29. package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/679.js +1 -1
  30. package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/708.js +3 -3
  31. package/app/webapp/.next/standalone/app/webapp/.next/server/middleware-build-manifest.js +1 -1
  32. package/app/webapp/.next/standalone/app/webapp/.next/server/pages/404.html +1 -1
  33. package/app/webapp/.next/standalone/app/webapp/.next/server/pages/500.html +1 -1
  34. package/app/webapp/.next/standalone/app/webapp/.next/server/server-reference-manifest.json +1 -1
  35. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +1 -0
  36. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +1 -0
  37. package/app/webapp/.next/{static/chunks/main-app-106dd83f859b9dfa.js → standalone/app/webapp/.next/static/chunks/main-app-76384b941f0b51cb.js} +1 -1
  38. package/app/webapp/.next/standalone/app/webapp/package.json +2 -6
  39. package/app/webapp/.next/standalone/app/webapp/server.js +1 -1
  40. package/app/webapp/.next/standalone/package.json +18 -6
  41. package/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +1 -0
  42. package/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +1 -0
  43. package/app/webapp/.next/{standalone/app/webapp/.next/static/chunks/main-app-106dd83f859b9dfa.js → static/chunks/main-app-76384b941f0b51cb.js} +1 -1
  44. package/app/webapp/package.json +2 -6
  45. package/app/webapp/skills/document-review.md +1 -0
  46. package/app/webapp/skills/gmail.md +2 -0
  47. package/app/webapp/src/app/globals.css +8 -3
  48. package/app/webapp/src/app/layout.tsx +2 -8
  49. package/app/webapp/src/shared/api/chat.ts +49 -25
  50. package/app/webapp/src/shared/api/screenshot.ts +11 -10
  51. package/app/webapp/src/shared/components/AppShell.tsx +80 -109
  52. package/app/webapp/src/shared/components/SessionView.tsx +335 -248
  53. package/app/webapp/src/shared/components/VirtualCursor.tsx +13 -14
  54. package/app/webapp/src/shared/lib/browser/cursor.ts +2 -7
  55. package/app/webapp/src/shared/lib/browser/index.ts +56 -36
  56. package/app/webapp/src/shared/lib/browser/mouse.ts +86 -21
  57. package/app/webapp/src/shared/lib/browser/navigate.ts +1 -4
  58. package/app/webapp/src/shared/lib/browser/recorder.ts +12 -5
  59. package/app/webapp/src/shared/lib/browser/screenshot.ts +4 -4
  60. package/app/webapp/src/shared/lib/browser/snapshot.ts +9 -5
  61. package/app/webapp/src/shared/lib/browser/tabs.ts +1 -1
  62. package/app/webapp/src/shared/lib/browser/types.ts +3 -2
  63. package/app/webapp/src/shared/lib/browser/wait.ts +1 -1
  64. package/app/webapp/src/shared/lib/recorder/session-recorder.ts +1 -1
  65. package/app/webapp/src/shared/types/global.d.ts +8 -19
  66. package/app/webapp/tailwind.config.js +1 -3
  67. package/bin/src/cli/bangonit.js +270 -177
  68. package/package.json +18 -6
  69. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-03dbc2fc67c26b74.js +0 -1
  70. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +0 -1
  71. package/app/webapp/.next/static/chunks/app/app/page-03dbc2fc67c26b74.js +0 -1
  72. package/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +0 -1
  73. /package/app/webapp/.next/standalone/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_buildManifest.js +0 -0
  74. /package/app/webapp/.next/standalone/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_ssgManifest.js +0 -0
  75. /package/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_buildManifest.js +0 -0
  76. /package/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_ssgManifest.js +0 -0
@@ -50,7 +50,7 @@ export default function SessionView({
50
50
  const [tabs, setTabs] = useState<Tab[]>(
51
51
  initialTabs?.map((t) => ({ ...t, initialUrl: t.url })) || [
52
52
  { id: 0, url: "about:blank", title: "", initialUrl: "about:blank" },
53
- ]
53
+ ],
54
54
  );
55
55
  const [activeTabId, setActiveTabId] = useState(initialActiveTabId ?? 0);
56
56
  const [urlInput, setUrlInput] = useState(() => {
@@ -62,7 +62,9 @@ export default function SessionView({
62
62
  const [devtoolsOpen, setDevtoolsOpen] = useState(false);
63
63
  const webviewRefs = useRef<Map<number, any>>(new Map());
64
64
  // Browser is always view-only — block all user input
65
- const [todos, setTodos] = useState<{ content: string; status: "pending" | "in_progress" | "completed" }[]>(initialTodos || []);
65
+ const [todos, setTodos] = useState<{ content: string; status: "pending" | "in_progress" | "completed" }[]>(
66
+ initialTodos || [],
67
+ );
66
68
  const messagesEndRef = useRef<HTMLDivElement>(null);
67
69
  const hasSubmittedRef = useRef(false);
68
70
  const nextTabId = useRef(initialTabs ? Math.max(...initialTabs.map((t) => t.id)) + 1 : 1);
@@ -91,17 +93,20 @@ export default function SessionView({
91
93
  "X-Client-Platform": navigator.platform,
92
94
  }),
93
95
  }),
94
- []
96
+ [],
95
97
  );
96
98
 
97
99
  const stoppedRef = useRef(false);
98
100
 
99
101
  // Stream agent output to main process for CLI
100
- const emit = useCallback((type: string, text: string) => {
101
- window.bangonit?.emitAgentOutput?.({ agentId, type, text, name: agentName });
102
- }, [agentId, agentName]);
102
+ const emit = useCallback(
103
+ (type: string, text: string) => {
104
+ window.bangonit?.emitAgentOutput?.({ agentId, type, text, name: agentName });
105
+ },
106
+ [agentId, agentName],
107
+ );
103
108
 
104
- const { messages, setMessages, sendMessage, addToolOutput, stop: rawStop, status, error } = useChat({
109
+ const { messages, sendMessage, addToolOutput, status, error } = useChat({
105
110
  transport,
106
111
  messages: initialMessages,
107
112
  sendAutomaticallyWhen: ({ messages }) =>
@@ -157,22 +162,37 @@ export default function SessionView({
157
162
  if (tc.toolName === "browser") {
158
163
  try {
159
164
  const input = tc.input as any;
160
- const label = (input?.actions || []).map((a: any) => {
161
- if (a.action === "navigate") return `navigate ${a.url}`;
162
- if (a.action === "type") return `type "${(a.text || "").slice(0, 30)}"`;
163
- if (a.action === "press") return `press ${a.key}`;
164
- if (a.action === "mouse") return `mouse ${(a.mouseActions || []).map((s: any) => `${s.action} ${s.ref || `${s.x},${s.y}`}`).join(" > ")}`;
165
- return a.action;
166
- }).join(", ");
165
+ const label = (input?.actions || [])
166
+ .map((a: any) => {
167
+ if (a.action === "navigate") return `navigate ${a.url}`;
168
+ if (a.action === "type") return `type "${(a.text || "").slice(0, 30)}"`;
169
+ if (a.action === "press") return `press ${a.key}`;
170
+ if (a.action === "mouse")
171
+ return `mouse ${(a.mouseActions || []).map((s: any) => `${s.action} ${s.ref || `${s.x},${s.y}`}`).join(" > ")}`;
172
+ return a.action;
173
+ })
174
+ .join(", ");
167
175
  emit("tool", `browser: ${label}${input?.observe ? ` [${input.observe}]` : ""}`);
168
- sessionRecorderRef.current?.addEvent({ ts: Date.now(), agentId, type: "tool-start", data: { tool: "browser", label } });
176
+ sessionRecorderRef.current?.addEvent({
177
+ ts: Date.now(),
178
+ agentId,
179
+ type: "tool-start",
180
+ data: { tool: "browser", label },
181
+ });
169
182
  const bt = browserToolsRef.current;
170
- const raw = bt ? await bt.exec({
171
- actions: input?.actions || [],
172
- observe: input?.observe,
173
- prompts: input?.prompts,
174
- }) : { error: "Error: browser tools not available" };
175
- sessionRecorderRef.current?.addEvent({ ts: Date.now(), agentId, type: "tool-end", data: { tool: "browser" } });
183
+ const raw = bt
184
+ ? await bt.exec({
185
+ actions: input?.actions || [],
186
+ observe: input?.observe || "snapshot",
187
+ prompts: input?.prompts,
188
+ })
189
+ : { error: "Error: browser tools not available" };
190
+ sessionRecorderRef.current?.addEvent({
191
+ ts: Date.now(),
192
+ agentId,
193
+ type: "tool-end",
194
+ data: { tool: "browser" },
195
+ });
176
196
  addToolOutput({
177
197
  tool: tc.toolName,
178
198
  toolCallId: tc.toolCallId,
@@ -192,11 +212,6 @@ export default function SessionView({
192
212
  },
193
213
  });
194
214
 
195
- const stop = useCallback(() => {
196
- stoppedRef.current = true;
197
- rawStop();
198
- }, [rawStop]);
199
-
200
215
  const isLoading = status === "submitted" || status === "streaming";
201
216
 
202
217
  // Clear stopped flag when a new request starts
@@ -228,10 +243,11 @@ export default function SessionView({
228
243
  if (messages.length === 0) return;
229
244
  const lastMsg = messages[messages.length - 1];
230
245
  if (lastMsg.role !== "assistant") return;
231
- const text = lastMsg.parts
232
- ?.filter((p: any) => p.type === "text")
233
- .map((p: any) => p.text)
234
- .join("") || "";
246
+ const text =
247
+ lastMsg.parts
248
+ ?.filter((p: any) => p.type === "text")
249
+ .map((p: any) => p.text)
250
+ .join("") || "";
235
251
  if (lastMsg.id !== lastStreamedMsgIdRef.current) {
236
252
  lastStreamedMsgIdRef.current = lastMsg.id;
237
253
  lastStreamedLenRef.current = 0;
@@ -273,7 +289,9 @@ export default function SessionView({
273
289
  nudgeTimerRef.current = null;
274
290
  if (!nudgedRef.current && !isDone) {
275
291
  nudgedRef.current = true;
276
- sendMessage({ text: "You must call the report_result tool to finalize this test run. Call it now with the result (pass or fail) and a summary." });
292
+ sendMessage({
293
+ text: "You must call the report_result tool to finalize this test run. Call it now with the result (pass or fail) and a summary.",
294
+ });
277
295
  }
278
296
  }, 5000);
279
297
  return () => {
@@ -293,59 +311,55 @@ export default function SessionView({
293
311
  }
294
312
  }, [messages, agentId]);
295
313
 
296
- // Persist messages, tabs, and todos on change
297
- useEffect(() => {
298
- if (messages.length > 0) {
299
- window.bangonit?.setAgentSession(agentId, {
300
- messages,
301
- initialPrompt,
302
- tabs: tabs.filter((t) => !t.isPopup).map(({ id, url, title }) => ({ id, url, title })),
303
- activeTabId: tabs.find((t) => t.id === activeTabId)?.isPopup ? tabs.find((t) => !t.isPopup)?.id ?? 0 : activeTabId,
304
- todos,
305
- });
306
- }
307
- }, [messages, tabs, activeTabId, agentId, initialPrompt, todos]);
308
-
309
314
  // Create a new tab
310
- const createTab = useCallback((url: string) => {
311
- const id = nextTabId.current++;
312
- setTabs((prev) => [...prev, { id, url: "about:blank", title: "", initialUrl: url }]);
313
- setActiveTabId(id);
314
- setUrlInput(url === "about:blank" ? "" : url);
315
- window.bangonit?.setActiveTab(agentId, id);
316
- }, [agentId]);
315
+ const createTab = useCallback(
316
+ (url: string) => {
317
+ const id = nextTabId.current++;
318
+ setTabs((prev) => [...prev, { id, url: "about:blank", title: "", initialUrl: url }]);
319
+ setActiveTabId(id);
320
+ setUrlInput(url === "about:blank" ? "" : url);
321
+ window.bangonit?.setActiveTab(agentId, id);
322
+ },
323
+ [agentId],
324
+ );
317
325
 
318
326
  // Close a tab by its ID
319
- const closeTab = useCallback((tabId: number) => {
320
- setTabs((prev) => {
321
- const idx = prev.findIndex((t) => t.id === tabId);
322
- if (idx === -1) return prev;
323
- const remaining = prev.filter((t) => t.id !== tabId);
324
- if (remaining.length === 0) return prev;
325
- setActiveTabId((current) => {
326
- if (current === tabId) {
327
- const newActive = remaining[Math.max(0, idx - 1)].id;
328
- const newTab = remaining.find((t) => t.id === newActive);
329
- if (newTab) setUrlInput(newTab.url === "about:blank" ? "" : newTab.url);
330
- window.bangonit?.setActiveTab(agentId, newActive);
331
- return newActive;
332
- }
333
- return current;
327
+ const closeTab = useCallback(
328
+ (tabId: number) => {
329
+ setTabs((prev) => {
330
+ const idx = prev.findIndex((t) => t.id === tabId);
331
+ if (idx === -1) return prev;
332
+ const remaining = prev.filter((t) => t.id !== tabId);
333
+ if (remaining.length === 0) return prev;
334
+ setActiveTabId((current) => {
335
+ if (current === tabId) {
336
+ const newActive = remaining[Math.max(0, idx - 1)].id;
337
+ const newTab = remaining.find((t) => t.id === newActive);
338
+ if (newTab) setUrlInput(newTab.url === "about:blank" ? "" : newTab.url);
339
+ window.bangonit?.setActiveTab(agentId, newActive);
340
+ return newActive;
341
+ }
342
+ return current;
343
+ });
344
+ return remaining;
334
345
  });
335
- return remaining;
336
- });
337
- }, [agentId]);
346
+ },
347
+ [agentId],
348
+ );
338
349
 
339
350
  // Switch active tab
340
- const selectTab = useCallback((tabId: number) => {
341
- setActiveTabId(tabId);
342
- window.bangonit?.setActiveTab(agentId, tabId);
343
- setTabs((prev) => {
344
- const tab = prev.find((t) => t.id === tabId);
345
- if (tab) setUrlInput(tab.url === "about:blank" ? "" : tab.url);
346
- return prev;
347
- });
348
- }, [agentId]);
351
+ const selectTab = useCallback(
352
+ (tabId: number) => {
353
+ setActiveTabId(tabId);
354
+ window.bangonit?.setActiveTab(agentId, tabId);
355
+ setTabs((prev) => {
356
+ const tab = prev.find((t) => t.id === tabId);
357
+ if (tab) setUrlInput(tab.url === "about:blank" ? "" : tab.url);
358
+ return prev;
359
+ });
360
+ },
361
+ [agentId],
362
+ );
349
363
 
350
364
  // Stable refs for callbacks
351
365
  const createTabRef = useRef(createTab);
@@ -357,47 +371,68 @@ export default function SessionView({
357
371
 
358
372
  useEffect(() => {
359
373
  const cleanups: (() => void)[] = [];
360
- const push = (fn: (() => void) | undefined) => { if (fn) cleanups.push(fn); };
361
-
362
- push(window.bangonit?.onTabUpdated((data: any) => {
363
- if (data.agentId !== agentId) return;
364
- const isBlank = data.url === "about:blank";
365
- const isBlankTitle = data.title === "about:blank";
366
- setTabs((prev) => prev.map((t) =>
367
- t.id === data.tabId
368
- ? { ...t, ...(!isBlank && data.url !== undefined && { url: data.url }), ...(!isBlankTitle && data.title !== undefined && { title: data.title }) }
369
- : t
370
- ));
371
- if (data.url !== undefined && !isBlank) {
372
- setActiveTabId((current) => {
373
- if (current === data.tabId) setUrlInput(data.url);
374
- return current;
375
- });
376
- }
377
- }));
378
-
379
- push(window.bangonit?.onOpenNewTab((data: any) => {
380
- if (data.agentId !== agentId) return;
381
- createTabRef.current(data.url);
382
- }));
383
- push(window.bangonit?.onPopupOpened((data: any) => {
384
- if (data.agentId !== agentId) return;
385
- setTabs((prev) => [...prev, {
386
- id: data.tabId,
387
- url: data.url || "about:blank",
388
- title: "",
389
- initialUrl: data.url || "about:blank",
390
- isPopup: true,
391
- }]);
392
- }));
393
- push(window.bangonit?.onCloseTab((data: any) => {
394
- if (data.agentId !== agentId) return;
395
- closeTabRef.current(data.tabId);
396
- }));
397
- push(window.bangonit?.onSelectTab((data: any) => {
398
- if (data.agentId !== agentId) return;
399
- selectTabRef.current(data.tabId);
400
- }));
374
+ const push = (fn: (() => void) | undefined) => {
375
+ if (fn) cleanups.push(fn);
376
+ };
377
+
378
+ push(
379
+ window.bangonit?.onTabUpdated((data: any) => {
380
+ if (data.agentId !== agentId) return;
381
+ const isBlank = data.url === "about:blank";
382
+ const isBlankTitle = data.title === "about:blank";
383
+ setTabs((prev) =>
384
+ prev.map((t) =>
385
+ t.id === data.tabId
386
+ ? {
387
+ ...t,
388
+ ...(!isBlank && data.url !== undefined && { url: data.url }),
389
+ ...(!isBlankTitle && data.title !== undefined && { title: data.title }),
390
+ }
391
+ : t,
392
+ ),
393
+ );
394
+ if (data.url !== undefined && !isBlank) {
395
+ setActiveTabId((current) => {
396
+ if (current === data.tabId) setUrlInput(data.url);
397
+ return current;
398
+ });
399
+ }
400
+ }),
401
+ );
402
+
403
+ push(
404
+ window.bangonit?.onOpenNewTab((data: any) => {
405
+ if (data.agentId !== agentId) return;
406
+ createTabRef.current(data.url);
407
+ }),
408
+ );
409
+ push(
410
+ window.bangonit?.onPopupOpened((data: any) => {
411
+ if (data.agentId !== agentId) return;
412
+ setTabs((prev) => [
413
+ ...prev,
414
+ {
415
+ id: data.tabId,
416
+ url: data.url || "about:blank",
417
+ title: "",
418
+ initialUrl: data.url || "about:blank",
419
+ isPopup: true,
420
+ },
421
+ ]);
422
+ }),
423
+ );
424
+ push(
425
+ window.bangonit?.onCloseTab((data: any) => {
426
+ if (data.agentId !== agentId) return;
427
+ closeTabRef.current(data.tabId);
428
+ }),
429
+ );
430
+ push(
431
+ window.bangonit?.onSelectTab((data: any) => {
432
+ if (data.agentId !== agentId) return;
433
+ selectTabRef.current(data.tabId);
434
+ }),
435
+ );
401
436
  // Mouse lock/unlock events unused — input blocked while test is running
402
437
 
403
438
  return () => cleanups.forEach((fn) => fn?.());
@@ -417,40 +452,45 @@ export default function SessionView({
417
452
  }, [messages]);
418
453
 
419
454
  // Register a webview's webContents with the main process
420
- const handleWebviewRef = useCallback((el: any, tabId: number, initialUrl: string) => {
421
- if (!el) return;
422
- webviewRefs.current.set(tabId, el);
423
- const register = () => {
424
- if (registeredTabsRef.current.has(tabId)) return;
425
- try {
426
- const wcId = el.getWebContentsId?.();
427
- if (wcId) {
428
- registeredTabsRef.current.add(tabId);
429
- window.bangonit?.registerTab(agentId, tabId, wcId, initialUrl, planDir);
430
- if (tabId === activeTabId) {
431
- window.bangonit?.setActiveTab(agentId, tabId);
455
+ const handleWebviewRef = useCallback(
456
+ (el: any, tabId: number, initialUrl: string) => {
457
+ if (!el) return;
458
+ webviewRefs.current.set(tabId, el);
459
+ const register = () => {
460
+ if (registeredTabsRef.current.has(tabId)) return;
461
+ try {
462
+ const wcId = el.getWebContentsId?.();
463
+ if (wcId) {
464
+ registeredTabsRef.current.add(tabId);
465
+ window.bangonit?.registerTab(agentId, tabId, wcId, initialUrl, planDir);
466
+ if (tabId === activeTabId) {
467
+ window.bangonit?.setActiveTab(agentId, tabId);
468
+ }
432
469
  }
470
+ } catch (e) {
471
+ console.error(e);
433
472
  }
434
- } catch (e) { console.error(e); }
435
- };
436
- register();
437
- el.addEventListener("did-attach", register);
438
- el.addEventListener("close", () => closeTabRef.current(tabId));
439
- // Forward browser console messages to main process + session recorder (once per webview)
440
- if (!el._consoleListenerAttached) {
441
- el._consoleListenerAttached = true;
442
- el.addEventListener("console-message", (e: any) => {
443
- window.bangonit?.emitConsoleMessage?.({
444
- agentId,
445
- level: e.level,
446
- message: e.message,
447
- url: e.sourceId || "",
448
- line: e.lineNumber || 0,
473
+ };
474
+ register();
475
+ el.addEventListener("did-attach", register);
476
+ el.addEventListener("close", () => closeTabRef.current(tabId));
477
+ // Forward browser console messages to main process + session recorder (once per webview)
478
+ if (!el._consoleListenerAttached) {
479
+ el._consoleListenerAttached = true;
480
+ el.addEventListener("console-message", (e: any) => {
481
+ window.bangonit?.emitConsoleMessage?.({
482
+ agentId,
483
+ level: e.level,
484
+ message: e.message,
485
+ url: e.sourceId || "",
486
+ line: e.lineNumber || 0,
487
+ });
488
+ sessionRecorderRef.current?.addConsoleLog(agentId, e.level, e.message, e.sourceId || "", e.lineNumber || 0);
449
489
  });
450
- sessionRecorderRef.current?.addConsoleLog(agentId, e.level, e.message, e.sourceId || "", e.lineNumber || 0);
451
- });
452
- }
453
- }, [agentId, activeTabId]);
490
+ }
491
+ },
492
+ [agentId, activeTabId],
493
+ );
454
494
 
455
495
  const formatTime = (s: number) => `${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, "0")}`;
456
496
 
@@ -467,7 +507,10 @@ export default function SessionView({
467
507
  className={`flex items-center gap-1.5 px-3 py-2 text-xs border-r border-zinc-800 min-w-0 max-w-[200px] shrink-0
468
508
  ${tab.id === activeTabId ? "bg-zinc-800 text-zinc-200" : "text-zinc-500 hover:text-zinc-300 hover:bg-zinc-850"}`}
469
509
  >
470
- <span className="truncate">{tab.isPopup ? "↗ " : ""}{tab.title || (tab.url && tab.url !== "about:blank" ? tab.url : "New Tab")}</span>
510
+ <span className="truncate">
511
+ {tab.isPopup ? "↗ " : ""}
512
+ {tab.title || (tab.url && tab.url !== "about:blank" ? tab.url : "New Tab")}
513
+ </span>
471
514
  </button>
472
515
  ))}
473
516
  </div>
@@ -481,35 +524,38 @@ export default function SessionView({
481
524
 
482
525
  {/* Webviews */}
483
526
  <div className="flex-1 relative bg-white overflow-hidden" onWheel={(e) => e.stopPropagation()}>
484
- {tabs.map((tab) => tab.isPopup ? (
485
- tab.id === activeTabId && (
486
- <div key={tab.id} className="absolute inset-0 flex items-center justify-center bg-zinc-900 text-zinc-400 text-sm z-[1]">
487
- Popup window open — {tab.title || tab.url}
488
- </div>
489
- )
490
- ) : (
491
- <webview
492
- key={tab.id}
493
- ref={(el: any) => handleWebviewRef(el, tab.id, tab.initialUrl)}
494
- src="about:blank"
495
- partition={`persist:agent-${agentId}`}
496
- allowpopups=""
497
- style={{
498
- width: "100%",
499
- height: "100%",
500
- position: "absolute",
501
- backgroundColor: "white",
502
- top: 0,
503
- left: 0,
504
- pointerEvents: tab.id === activeTabId ? "auto" : "none",
505
- zIndex: tab.id === activeTabId ? 1 : 0,
506
- }}
507
- />
508
- ))}
509
- {!tabs.find((t) => t.id === activeTabId)?.isPopup && <VirtualCursor agentId={agentId} />}
510
- {!isDone && (
511
- <div className="absolute inset-0 z-10 cursor-not-allowed" />
527
+ {tabs.map((tab) =>
528
+ tab.isPopup ? (
529
+ tab.id === activeTabId && (
530
+ <div
531
+ key={tab.id}
532
+ className="absolute inset-0 flex items-center justify-center bg-zinc-900 text-zinc-400 text-sm z-[1]"
533
+ >
534
+ Popup window open — {tab.title || tab.url}
535
+ </div>
536
+ )
537
+ ) : (
538
+ <webview
539
+ key={tab.id}
540
+ ref={(el: any) => handleWebviewRef(el, tab.id, tab.initialUrl)}
541
+ src="about:blank"
542
+ partition={`agent-${agentId}`}
543
+ allowpopups=""
544
+ style={{
545
+ width: "100%",
546
+ height: "100%",
547
+ position: "absolute",
548
+ backgroundColor: "white",
549
+ top: 0,
550
+ left: 0,
551
+ pointerEvents: tab.id === activeTabId ? "auto" : "none",
552
+ zIndex: tab.id === activeTabId ? 1 : 0,
553
+ }}
554
+ />
555
+ ),
512
556
  )}
557
+ {!tabs.find((t) => t.id === activeTabId)?.isPopup && <VirtualCursor agentId={agentId} />}
558
+ {!isDone && <div className="absolute inset-0 z-10 cursor-not-allowed" />}
513
559
  </div>
514
560
  </div>
515
561
 
@@ -523,17 +569,16 @@ export default function SessionView({
523
569
 
524
570
  {/* Test result banner */}
525
571
  {testResult && (
526
- <div className={`px-4 py-2 text-sm border-b flex items-center gap-3 ${testResult.result === "pass"
527
- ? "bg-green-950/30 border-green-900/50"
528
- : "bg-red-950/30 border-red-900/50"
529
- }`}>
572
+ <div
573
+ className={`px-4 py-2 text-sm border-b flex items-center gap-3 ${
574
+ testResult.result === "pass" ? "bg-green-950/30 border-green-900/50" : "bg-red-950/30 border-red-900/50"
575
+ }`}
576
+ >
530
577
  <span className={`font-medium ${testResult.result === "pass" ? "text-green-400" : "text-red-400"}`}>
531
578
  {testResult.result === "pass" ? "PASSED" : "FAILED"}
532
579
  </span>
533
580
  <span className="text-zinc-500 text-xs">{formatTime(elapsedSeconds)}</span>
534
- {testResult.summary && (
535
- <span className="text-zinc-400 text-xs truncate flex-1">{testResult.summary}</span>
536
- )}
581
+ {testResult.summary && <span className="text-zinc-400 text-xs truncate flex-1">{testResult.summary}</span>}
537
582
  </div>
538
583
  )}
539
584
 
@@ -541,11 +586,18 @@ export default function SessionView({
541
586
  {todos.length > 0 && (
542
587
  <div className="text-xs space-y-1 px-4 py-2 border-b border-zinc-800 bg-zinc-900/80 shrink-0 max-h-48 overflow-y-auto">
543
588
  {todos.map((todo, i) => (
544
- <div key={i} className={`flex items-center gap-2 ${todo.status === "completed" ? "text-zinc-500" : "text-zinc-300"}`}>
589
+ <div
590
+ key={i}
591
+ className={`flex items-center gap-2 ${todo.status === "completed" ? "text-zinc-500" : "text-zinc-300"}`}
592
+ >
545
593
  <span className="shrink-0">
546
- {todo.status === "completed" ? "✓" : todo.status === "in_progress" ? (
594
+ {todo.status === "completed" ? (
595
+ "✓"
596
+ ) : todo.status === "in_progress" ? (
547
597
  <span className="inline-block w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
548
- ) : "○"}
598
+ ) : (
599
+ "○"
600
+ )}
549
601
  </span>
550
602
  <span className={todo.status === "completed" ? "line-through" : ""}>{todo.content}</span>
551
603
  </div>
@@ -570,8 +622,12 @@ export default function SessionView({
570
622
  <div className="px-4 py-2 border-t border-zinc-800 flex items-center gap-3 shrink-0">
571
623
  {isDone ? (
572
624
  <>
573
- <span className={`inline-block w-2 h-2 rounded-full shrink-0 ${testResult!.result === "pass" ? "bg-green-500" : "bg-red-500"}`} />
574
- <span className={`text-xs font-medium ${testResult!.result === "pass" ? "text-green-400" : "text-red-400"}`}>
625
+ <span
626
+ className={`inline-block w-2 h-2 rounded-full shrink-0 ${testResult!.result === "pass" ? "bg-green-500" : "bg-red-500"}`}
627
+ />
628
+ <span
629
+ className={`text-xs font-medium ${testResult!.result === "pass" ? "text-green-400" : "text-red-400"}`}
630
+ >
575
631
  {testResult!.result === "pass" ? "Passed" : "Failed"} in {formatTime(elapsedSeconds)}
576
632
  </span>
577
633
  <div className="flex-1" />
@@ -584,10 +640,11 @@ export default function SessionView({
584
640
  setDevtoolsOpen(!devtoolsOpen);
585
641
  }
586
642
  }}
587
- className={`text-xs px-2 py-1 rounded border transition-colors ${devtoolsOpen
588
- ? "border-blue-600 text-blue-400 bg-blue-950/30"
589
- : "border-zinc-700 hover:border-zinc-500 text-zinc-500 hover:text-zinc-300"
590
- }`}
643
+ className={`text-xs px-2 py-1 rounded border transition-colors ${
644
+ devtoolsOpen
645
+ ? "border-blue-600 text-blue-400 bg-blue-950/30"
646
+ : "border-zinc-700 hover:border-zinc-500 text-zinc-500 hover:text-zinc-300"
647
+ }`}
591
648
  title="Toggle DevTools"
592
649
  >
593
650
  DevTools
@@ -604,8 +661,12 @@ export default function SessionView({
604
661
  </>
605
662
  ) : hasStarted ? (
606
663
  <>
607
- <span className={`inline-block w-2 h-2 rounded-full shrink-0 ${paused ? "bg-amber-500" : "bg-blue-500 animate-pulse"}`} />
608
- <span className="text-xs text-zinc-400">{paused ? "Paused" : "Working"} {formatTime(elapsedSeconds)}</span>
664
+ <span
665
+ className={`inline-block w-2 h-2 rounded-full shrink-0 ${paused ? "bg-amber-500" : "bg-blue-500 animate-pulse"}`}
666
+ />
667
+ <span className="text-xs text-zinc-400">
668
+ {paused ? "Paused" : "Working"} {formatTime(elapsedSeconds)}
669
+ </span>
609
670
  <div className="flex-1" />
610
671
  <button
611
672
  onClick={() => {
@@ -616,10 +677,11 @@ export default function SessionView({
616
677
  setDevtoolsOpen(!devtoolsOpen);
617
678
  }
618
679
  }}
619
- className={`text-xs px-2 py-1 rounded border transition-colors ${devtoolsOpen
620
- ? "border-blue-600 text-blue-400 bg-blue-950/30"
621
- : "border-zinc-700 hover:border-zinc-500 text-zinc-500 hover:text-zinc-300"
622
- }`}
680
+ className={`text-xs px-2 py-1 rounded border transition-colors ${
681
+ devtoolsOpen
682
+ ? "border-blue-600 text-blue-400 bg-blue-950/30"
683
+ : "border-zinc-700 hover:border-zinc-500 text-zinc-500 hover:text-zinc-300"
684
+ }`}
623
685
  title="Toggle DevTools"
624
686
  >
625
687
  DevTools
@@ -695,30 +757,41 @@ function ToolCallView({ part }: { part: any }) {
695
757
  const isRunning = state === "input-streaming" || state === "input-available";
696
758
  const toolName = part.toolName || (typeof part.type === "string" ? part.type.replace(/^tool-/, "") : "");
697
759
 
698
- const command = state === "input-streaming" ? ""
699
- : toolName === "browser"
700
- ? (() => {
701
- const parts = (input.actions || []).map((a: any) => {
702
- switch (a.action) {
703
- case "navigate": return `navigate ${a.url || ""}`;
704
- case "mouse": {
705
- const summary = (a.mouseActions || []).map((s: any) =>
706
- s.action === "wait" ? `wait ${s.ms}ms` :
707
- s.action === "wheel" ? `wheel ${s.ref || `${s.x},${s.y}`}` :
708
- `${s.action} ${s.ref || `${s.x},${s.y}`}`
709
- ).join(" ");
710
- return `mouse ${summary}`;
711
- }
712
- case "type": return `type "${(a.text || "").slice(0, 40)}"`;
713
- case "press": return `press ${a.key || ""}`;
714
- case "tabs": return `tabs ${a.tabAction || ""}${a.tabId ? " " + a.tabId : ""}`;
715
- default: return a.action || "";
716
- }
717
- });
718
- if (input.observe) parts.push(input.observe);
719
- return parts.join(", ");
720
- })()
721
- : input.command || (Object.keys(input).length > 0 ? JSON.stringify(input) : "");
760
+ const command =
761
+ state === "input-streaming"
762
+ ? ""
763
+ : toolName === "browser"
764
+ ? (() => {
765
+ const parts = (input.actions || []).map((a: any) => {
766
+ switch (a.action) {
767
+ case "navigate":
768
+ return `navigate ${a.url || ""}`;
769
+ case "mouse": {
770
+ const summary = (a.mouseActions || [])
771
+ .map((s: any) =>
772
+ s.action === "wait"
773
+ ? `wait ${s.ms}ms`
774
+ : s.action === "wheel"
775
+ ? `wheel ${s.ref || `${s.x},${s.y}`}`
776
+ : `${s.action} ${s.ref || `${s.x},${s.y}`}`,
777
+ )
778
+ .join(" → ");
779
+ return `mouse ${summary}`;
780
+ }
781
+ case "type":
782
+ return `type "${(a.text || "").slice(0, 40)}"`;
783
+ case "press":
784
+ return `press ${a.key || ""}`;
785
+ case "tabs":
786
+ return `tabs ${a.tabAction || ""}${a.tabId ? " " + a.tabId : ""}`;
787
+ default:
788
+ return a.action || "";
789
+ }
790
+ });
791
+ if (input.observe) parts.push(input.observe);
792
+ return parts.join(", ");
793
+ })()
794
+ : input.command || (Object.keys(input).length > 0 ? JSON.stringify(input) : "");
722
795
 
723
796
  return (
724
797
  <div className="text-sm">
@@ -738,30 +811,44 @@ function ToolCallView({ part }: { part: any }) {
738
811
  </button>
739
812
  {expanded && (
740
813
  <pre className="mt-1 px-3 py-2 bg-zinc-900/50 rounded-lg border border-zinc-800 text-xs whitespace-pre-wrap overflow-x-auto max-h-60 overflow-y-auto">
741
- {command && <><span className="text-zinc-300 select-all">$ {command}</span>{"\n"}</>}
742
- {result != null && (() => {
743
- if (toolName === "browser" && typeof result === "string") {
744
- try {
745
- const parsed = JSON.parse(result);
746
- if (parsed.textOutput !== undefined) {
747
- return (
748
- <>
749
- <span className="text-zinc-500">{parsed.textOutput}</span>
750
- {parsed.imageOutput && (
751
- <>
752
- {"\n"}
753
- <span className={parsed.imageOutput.type === "changed" ? "text-blue-400" : "text-zinc-600"}>
754
- {parsed.imageOutput.type === "changed" ? "[screenshot: changed]" : "[screenshot: unchanged]"}
755
- </span>
756
- </>
757
- )}
758
- </>
759
- );
814
+ {command && (
815
+ <>
816
+ <span className="text-zinc-300 select-all">$ {command}</span>
817
+ {"\n"}
818
+ </>
819
+ )}
820
+ {result != null &&
821
+ (() => {
822
+ if (toolName === "browser" && typeof result === "string") {
823
+ try {
824
+ const parsed = JSON.parse(result);
825
+ if (parsed.textOutput !== undefined) {
826
+ return (
827
+ <>
828
+ <span className="text-zinc-500">{parsed.textOutput}</span>
829
+ {parsed.imageOutput && (
830
+ <>
831
+ {"\n"}
832
+ <span className={parsed.imageOutput.type === "changed" ? "text-blue-400" : "text-zinc-600"}>
833
+ {parsed.imageOutput.type === "changed"
834
+ ? "[screenshot: changed]"
835
+ : "[screenshot: unchanged]"}
836
+ </span>
837
+ </>
838
+ )}
839
+ </>
840
+ );
841
+ }
842
+ } catch (e) {
843
+ console.error(e);
760
844
  }
761
- } catch (e) { console.error(e); }
762
- }
763
- return <span className="text-zinc-500">{typeof result === "string" ? result : JSON.stringify(result, null, 2)}</span>;
764
- })()}
845
+ }
846
+ return (
847
+ <span className="text-zinc-500">
848
+ {typeof result === "string" ? result : JSON.stringify(result, null, 2)}
849
+ </span>
850
+ );
851
+ })()}
765
852
  </pre>
766
853
  )}
767
854
  </div>