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
@@ -64,8 +64,8 @@ Actions:
64
64
  close — close current page
65
65
  wait --timeout=<seconds> — pause execution (use sparingly — only when you must wait for an animation or timer, not for page loads which are handled automatically)
66
66
 
67
- Observation — the "observe" field:
68
- Set "observe" to observe the page after all actions execute. Two modes:
67
+ Observation — the "observe" field (REQUIRED):
68
+ Every browser tool call must include "observe". Two modes:
69
69
  - "snapshot" — takes an accessibility snapshot (fast). Returns a text tree of all visible elements with refs (e.g. ref=e5) for precise clicking. This is your default — use it almost always.
70
70
  - "snapshot_and_screenshot" — takes a snapshot AND a screenshot analyzed by a vision subagent (slower). Use only when the snapshot alone doesn't have the info you need (e.g. visual content like images, charts, colors, spatial layout, or sites using canvas/non-semantic HTML).
71
71
  When using "snapshot_and_screenshot", pass "prompts" (top-level, alongside "observe") — an array of questions about the screenshot. Ask multiple questions per call to minimize screenshot usage.
@@ -143,19 +143,22 @@ export function createChatHandler() {
143
143
 
144
144
  const tools = {
145
145
  browser: tool({
146
- description: "Control the browser. Pass an array of actions to execute sequentially. Set 'observe' to observe the page after actions complete. The system waits for network idle before observation.",
146
+ description:
147
+ "Control the browser. Pass an array of actions to execute sequentially. Set 'observe' to observe the page after actions complete. The system waits for network idle before observation.",
147
148
  inputSchema: jsonSchema({
148
149
  type: "object",
149
150
  properties: {
150
151
  observe: {
151
152
  type: "string",
152
153
  enum: ["snapshot", "snapshot_and_screenshot"],
153
- description: "How to observe the page after actions. 'snapshot' takes a fast accessibility snapshot with element refs. 'snapshot_and_screenshot' also takes a screenshot analyzed by a vision subagent (slower — use only when the snapshot doesn't have the info you need).",
154
+ description:
155
+ "How to observe the page after actions. 'snapshot' takes a fast accessibility snapshot with element refs. 'snapshot_and_screenshot' also takes a screenshot analyzed by a vision subagent (slower — use only when the snapshot doesn't have the info you need).",
154
156
  },
155
157
  prompts: {
156
158
  type: "array",
157
159
  items: { type: "string" },
158
- description: "Questions for screenshot vision analysis (only used with observe: 'snapshot_and_screenshot')",
160
+ description:
161
+ "Questions for screenshot vision analysis (only used with observe: 'snapshot_and_screenshot')",
159
162
  },
160
163
  actions: {
161
164
  type: "array",
@@ -164,7 +167,18 @@ export function createChatHandler() {
164
167
  properties: {
165
168
  action: {
166
169
  type: "string",
167
- enum: ["navigate", "back", "forward", "mouse", "type", "press", "upload", "tabs", "wait", "close"],
170
+ enum: [
171
+ "navigate",
172
+ "back",
173
+ "forward",
174
+ "mouse",
175
+ "type",
176
+ "press",
177
+ "upload",
178
+ "tabs",
179
+ "wait",
180
+ "close",
181
+ ],
168
182
  description: "The browser action to perform",
169
183
  },
170
184
  url: { type: "string", description: "URL for navigate or new tab" },
@@ -175,7 +189,10 @@ export function createChatHandler() {
175
189
  items: {
176
190
  type: "object",
177
191
  properties: {
178
- action: { type: "string", enum: ["click", "dblclick", "down", "move", "up", "wheel", "wait"] },
192
+ action: {
193
+ type: "string",
194
+ enum: ["click", "dblclick", "down", "move", "up", "wheel", "wait"],
195
+ },
179
196
  ref: { type: "string", description: "Element ref from snapshot" },
180
197
  x: { type: "number" },
181
198
  y: { type: "number" },
@@ -198,38 +215,40 @@ export function createChatHandler() {
198
215
  },
199
216
  },
200
217
  },
201
- required: ["actions"],
218
+ required: ["actions", "observe"],
202
219
  }),
203
- toModelOutput({output}: {output: string}) {
220
+ toModelOutput({ output }: { output: string }) {
204
221
  const parsedOutput = browserToolOutputSchema.parse(JSON.parse(output));
205
222
  if (!parsedOutput.imageOutput) {
206
223
  return {
207
224
  type: "content",
208
- value: [
209
- {type: "text", text: parsedOutput.textOutput},
210
- ]
211
- }
225
+ value: [{ type: "text", text: parsedOutput.textOutput }],
226
+ };
212
227
  }
213
228
  if (parsedOutput.imageOutput.type === "not_changed") {
214
229
  return {
215
230
  type: "content",
216
231
  value: [
217
- {type: "text", text: parsedOutput.textOutput + "\n\n[screenshot did not change since last screenshot]"},
218
- ]
219
- }
232
+ {
233
+ type: "text",
234
+ text: parsedOutput.textOutput + "\n\n[screenshot did not change since last screenshot]",
235
+ },
236
+ ],
237
+ };
220
238
  }
221
239
 
222
240
  return {
223
241
  type: "content",
224
242
  value: [
225
- {type: "text", text: parsedOutput.textOutput},
226
- {type: "image-data", mediaType: "image/jpeg", data: parsedOutput.imageOutput.base64}
227
- ]
228
- }
229
- }
243
+ { type: "text", text: parsedOutput.textOutput },
244
+ { type: "image-data", mediaType: "image/jpeg", data: parsedOutput.imageOutput.base64 },
245
+ ],
246
+ };
247
+ },
230
248
  }),
231
249
  todos: tool({
232
- description: "Track progress on multi-step tasks. Call with the FULL list of todos to update, or omit 'todos' to read the current list.",
250
+ description:
251
+ "Track progress on multi-step tasks. Call with the FULL list of todos to update, or omit 'todos' to read the current list.",
233
252
  inputSchema: jsonSchema({
234
253
  type: "object",
235
254
  properties: {
@@ -248,12 +267,16 @@ export function createChatHandler() {
248
267
  }),
249
268
  }),
250
269
  report_result: tool({
251
- description: "Report the final test result. You MUST call this exactly once at the end of every test run. Pass 'pass' if all test steps succeeded, or 'fail' with a summary of what went wrong.",
270
+ description:
271
+ "Report the final test result. You MUST call this exactly once at the end of every test run. Pass 'pass' if all test steps succeeded, or 'fail' with a summary of what went wrong.",
252
272
  inputSchema: jsonSchema({
253
273
  type: "object",
254
274
  properties: {
255
275
  result: { type: "string", enum: ["pass", "fail"], description: "Whether the test passed or failed" },
256
- summary: { type: "string", description: "Brief summary of test results. For failures, describe what went wrong." },
276
+ summary: {
277
+ type: "string",
278
+ description: "Brief summary of test results. For failures, describe what went wrong.",
279
+ },
257
280
  },
258
281
  required: ["result", "summary"],
259
282
  }),
@@ -273,7 +296,8 @@ export function createChatHandler() {
273
296
  {
274
297
  type: "compact_20260112",
275
298
  trigger: { type: "input_tokens", value: 120000 },
276
- instructions: "Summarize the conversation concisely. Preserve: the test plan, all URLs visited, current page/tab state, actions completed, current test state (done/in-progress/remaining), errors and resolutions, key decisions made. Include exact URLs, filenames, and values.",
299
+ instructions:
300
+ "Summarize the conversation concisely. Preserve: the test plan, all URLs visited, current page/tab state, actions completed, current test state (done/in-progress/remaining), errors and resolutions, key decisions made. Include exact URLs, filenames, and values.",
277
301
  },
278
302
  ],
279
303
  },
@@ -30,13 +30,15 @@ async function analyzeWithClaude(image: string, prompt: string): Promise<string>
30
30
  model: "claude-haiku-4-5",
31
31
  max_tokens: 4096,
32
32
  system: SYSTEM_PROMPT,
33
- messages: [{
34
- role: "user",
35
- content: [
36
- { type: "image", source: { type: "base64", media_type: "image/jpeg", data: image } },
37
- { type: "text", text: prompt },
38
- ],
39
- }],
33
+ messages: [
34
+ {
35
+ role: "user",
36
+ content: [
37
+ { type: "image", source: { type: "base64", media_type: "image/jpeg", data: image } },
38
+ { type: "text", text: prompt },
39
+ ],
40
+ },
41
+ ],
40
42
  });
41
43
 
42
44
  return result.content
@@ -57,9 +59,8 @@ export function createScreenshotHandler() {
57
59
  headers: { "Content-Type": "application/json", ...CORS_HEADERS },
58
60
  });
59
61
  }
60
- const prompt = prompts.length === 1
61
- ? prompts[0]
62
- : prompts.map((p: string, i: number) => `${i + 1}. ${p}`).join("\n");
62
+ const prompt =
63
+ prompts.length === 1 ? prompts[0] : prompts.map((p: string, i: number) => `${i + 1}. ${p}`).join("\n");
63
64
 
64
65
  try {
65
66
  const text = await analyzeWithClaude(image, prompt);
@@ -5,19 +5,6 @@ import SessionView from "./SessionView";
5
5
  import { AgentConfig, AgentStatus } from "..";
6
6
  import { SessionRecorder } from "../lib/recorder/session-recorder";
7
7
 
8
- function patchIncompleteToolCalls(messages: any[]): any[] {
9
- return messages.map((msg) => {
10
- if (msg.role !== "assistant" || !msg.parts) return msg;
11
- const patched = msg.parts.map((part: any) => {
12
- if (part.type === "tool-invocation" && part.state !== "result") {
13
- return { ...part, state: "result", output: "[Interrupted — app was restarted]" };
14
- }
15
- return part;
16
- });
17
- return { ...msg, parts: patched };
18
- });
19
- }
20
-
21
8
  interface TestPlanMessage {
22
9
  agentIndex: number;
23
10
  testPlan: string;
@@ -31,7 +18,19 @@ interface TestPlanMessage {
31
18
  export default function AppShell() {
32
19
  const [agents, setAgents] = useState<AgentConfig[]>([]);
33
20
  const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentStatus>>({});
34
- const [agentSessions, setAgentSessions] = useState<Record<string, { initialPrompt: string; initialMessages?: any[]; initialTabs?: { id: number; url: string; title: string }[]; initialActiveTabId?: number; initialTodos?: { content: string; status: "pending" | "in_progress" | "completed" }[]; planDir?: string }>>({});
21
+ const [agentSessions, setAgentSessions] = useState<
22
+ Record<
23
+ string,
24
+ {
25
+ initialPrompt: string;
26
+ initialMessages?: any[];
27
+ initialTabs?: { id: number; url: string; title: string }[];
28
+ initialActiveTabId?: number;
29
+ initialTodos?: { content: string; status: "pending" | "in_progress" | "completed" }[];
30
+ planDir?: string;
31
+ }
32
+ >
33
+ >({});
35
34
  const [loaded, setLoaded] = useState(false);
36
35
  const [retryKeys, setRetryKeys] = useState<Record<string, number>>({});
37
36
  const retryConfigRef = useRef<Map<string, { maxRetries: number; attempt: number; testPlan: string }>>(new Map());
@@ -41,68 +40,44 @@ export default function AppShell() {
41
40
  const sessionRecorderRef = useRef<SessionRecorder | null>(null);
42
41
  const browserRecordersRef = useRef<Map<string, import("../lib/browser/recorder").BrowserRecorder>>(new Map());
43
42
 
44
- // Load persisted agents on mount
43
+ // Initialize with a single agent on mount
45
44
  useEffect(() => {
46
- (async () => {
47
- const savedAgents: AgentConfig[] = (await window.bangonit?.getAgents()) || [];
48
- if (savedAgents.length === 0) {
49
- const first: AgentConfig = {
50
- id: crypto.randomUUID(),
51
- name: "Test 1",
52
- createdAt: Date.now(),
53
- };
54
- savedAgents.push(first);
55
- await window.bangonit?.setAgents(savedAgents);
56
- }
57
- setAgents(savedAgents);
58
-
59
- const sessions: typeof agentSessions = {};
60
- const statuses: Record<string, AgentStatus> = {};
61
- for (const agent of savedAgents) {
62
- const session = await window.bangonit?.getAgentSession(agent.id);
63
- if (session && session.messages?.length > 0) {
64
- sessions[agent.id] = {
65
- initialPrompt: session.initialPrompt,
66
- initialMessages: patchIncompleteToolCalls(session.messages),
67
- initialTabs: session.tabs,
68
- initialActiveTabId: session.activeTabId,
69
- initialTodos: session.todos,
70
- };
71
- statuses[agent.id] = "idle";
72
- }
73
- }
74
- setAgentSessions(sessions);
75
- setAgentStatuses(statuses);
76
- setActiveAgentId(savedAgents[0]?.id || null);
77
- setLoaded(true);
78
- })();
45
+ const first: AgentConfig = {
46
+ id: crypto.randomUUID(),
47
+ name: "Test 1",
48
+ createdAt: Date.now(),
49
+ };
50
+ setAgents([first]);
51
+ setActiveAgentId(first.id);
52
+ setLoaded(true);
79
53
  }, []);
80
54
 
81
- // Persist agents
55
+ // Ref for async access to current agents list
82
56
  const agentsRef = useRef(agents);
83
57
  agentsRef.current = agents;
84
- useEffect(() => {
85
- if (loaded && agents.length > 0) {
86
- window.bangonit?.setAgents(agents);
87
- }
88
- }, [agents, loaded]);
89
58
 
90
59
  // Rerun a test — clears partition, bumps key to remount SessionView
91
- const rerunAgent = useCallback(async (agentId: string) => {
92
- await window.bangonit?.clearPartition?.(agentId);
93
- const session = agentSessions[agentId];
94
- if (!session) return;
95
- setAgentStatuses((prev) => ({ ...prev, [agentId]: "running" }));
96
- setRetryKeys((prev) => ({ ...prev, [agentId]: (prev[agentId] || 0) + 1 }));
97
- setAgentSessions((prev) => ({
98
- ...prev,
99
- [agentId]: { initialPrompt: session.initialPrompt },
100
- }));
101
- }, [agentSessions]);
60
+ const rerunAgent = useCallback(
61
+ async (agentId: string) => {
62
+ await window.bangonit?.clearPartition?.(agentId);
63
+ const session = agentSessions[agentId];
64
+ if (!session) return;
65
+ setAgentStatuses((prev) => ({ ...prev, [agentId]: "running" }));
66
+ setRetryKeys((prev) => ({ ...prev, [agentId]: (prev[agentId] || 0) + 1 }));
67
+ setAgentSessions((prev) => ({
68
+ ...prev,
69
+ [agentId]: { initialPrompt: session.initialPrompt },
70
+ }));
71
+ },
72
+ [agentSessions],
73
+ );
102
74
 
103
- const registerBrowserRecorder = useCallback((agentId: string, recorder: import("../lib/browser/recorder").BrowserRecorder) => {
104
- browserRecordersRef.current.set(agentId, recorder);
105
- }, []);
75
+ const registerBrowserRecorder = useCallback(
76
+ (agentId: string, recorder: import("../lib/browser/recorder").BrowserRecorder) => {
77
+ browserRecordersRef.current.set(agentId, recorder);
78
+ },
79
+ [],
80
+ );
106
81
 
107
82
  // Track statuses in a ref for async access
108
83
  const agentStatusesRef = useRef(agentStatuses);
@@ -126,7 +101,7 @@ export default function AppShell() {
126
101
  return {
127
102
  id: a.id,
128
103
  name: a.name,
129
- result: st === "completed" ? "pass" as const : st === "failed" ? "fail" as const : undefined,
104
+ result: st === "completed" ? ("pass" as const) : st === "failed" ? ("fail" as const) : undefined,
130
105
  };
131
106
  });
132
107
 
@@ -134,32 +109,35 @@ export default function AppShell() {
134
109
  await window.bangonit?.generateReplayHtml?.({ runDir, data: JSON.stringify(data) });
135
110
  }, []);
136
111
 
137
- const handleStatusChange = useCallback((agentId: string, status: "running" | "idle", result?: "pass" | "fail") => {
138
- let mapped: AgentStatus;
139
- if (status === "running") mapped = "running";
140
- else if (result === "pass") mapped = "completed";
141
- else if (result === "fail") mapped = "failed";
142
- else mapped = "idle";
143
- setAgentStatuses((prev) => ({ ...prev, [agentId]: mapped }));
144
-
145
- if (recording && (result === "pass" || result === "fail")) {
146
- saveReplayData(agentId, mapped);
147
- }
112
+ const handleStatusChange = useCallback(
113
+ (agentId: string, status: "running" | "idle", result?: "pass" | "fail") => {
114
+ let mapped: AgentStatus;
115
+ if (status === "running") mapped = "running";
116
+ else if (result === "pass") mapped = "completed";
117
+ else if (result === "fail") mapped = "failed";
118
+ else mapped = "idle";
119
+ setAgentStatuses((prev) => ({ ...prev, [agentId]: mapped }));
120
+
121
+ if (recording && (result === "pass" || result === "fail")) {
122
+ saveReplayData(agentId, mapped);
123
+ }
148
124
 
149
- if (result === "fail") {
150
- const config = retryConfigRef.current.get(agentId);
151
- if (config && config.attempt < config.maxRetries) {
152
- config.attempt++;
153
- window.bangonit?.emitTestRetry?.({ agentId, attempt: config.attempt, maxRetries: config.maxRetries });
154
- setTimeout(() => rerunAgent(agentId), 1000);
125
+ if (result === "fail") {
126
+ const config = retryConfigRef.current.get(agentId);
127
+ if (config && config.attempt < config.maxRetries) {
128
+ config.attempt++;
129
+ window.bangonit?.emitTestRetry?.({ agentId, attempt: config.attempt, maxRetries: config.maxRetries });
130
+ setTimeout(() => rerunAgent(agentId), 1000);
131
+ }
155
132
  }
156
- }
157
- }, [rerunAgent, recording, saveReplayData]);
133
+ },
134
+ [rerunAgent, recording, saveReplayData],
135
+ );
158
136
 
159
137
  // Start a test from the UI
160
138
  const startTest = useCallback((agentId: string, prompt: string, name?: string) => {
161
139
  if (name) {
162
- setAgents((prev) => prev.map((a) => a.id === agentId ? { ...a, name } : a));
140
+ setAgents((prev) => prev.map((a) => (a.id === agentId ? { ...a, name } : a)));
163
141
  }
164
142
  setAgentSessions((prev) => ({
165
143
  ...prev,
@@ -201,7 +179,7 @@ export default function AppShell() {
201
179
  });
202
180
  }
203
181
  if (name) {
204
- next = next.map((a, i) => i === agentIndex ? { ...a, name } : a);
182
+ next = next.map((a, i) => (i === agentIndex ? { ...a, name } : a));
205
183
  }
206
184
  return next;
207
185
  });
@@ -211,9 +189,7 @@ export default function AppShell() {
211
189
  const agent = currentAgents[agentIndex];
212
190
  if (!agent) return currentAgents;
213
191
 
214
- const prompt = extraPrompt
215
- ? `${testPlan}\n\n## Additional Instructions\n${extraPrompt}`
216
- : testPlan;
192
+ const prompt = extraPrompt ? `${testPlan}\n\n## Additional Instructions\n${extraPrompt}` : testPlan;
217
193
 
218
194
  setAgentSessions((prev) => ({
219
195
  ...prev,
@@ -244,7 +220,6 @@ export default function AppShell() {
244
220
  return <div className="flex h-screen bg-zinc-950" />;
245
221
  }
246
222
 
247
- const hasAnySessions = agents.some((a) => agentSessions[a.id]);
248
223
  const activeAgent = agents.find((a) => a.id === activeAgentId);
249
224
  const activeHasSession = activeAgentId ? !!agentSessions[activeAgentId] : false;
250
225
 
@@ -265,11 +240,14 @@ export default function AppShell() {
265
240
  </div>
266
241
  {agents.map((agent) => {
267
242
  const status = agentStatuses[agent.id];
268
- const hasSession = !!agentSessions[agent.id];
269
- const dotClass = status === "running" ? "bg-blue-500 animate-pulse" :
270
- status === "completed" ? "bg-green-500" :
271
- status === "failed" ? "bg-red-500" :
272
- "bg-zinc-600";
243
+ const dotClass =
244
+ status === "running"
245
+ ? "bg-blue-500 animate-pulse"
246
+ : status === "completed"
247
+ ? "bg-green-500"
248
+ : status === "failed"
249
+ ? "bg-red-500"
250
+ : "bg-zinc-600";
273
251
  return (
274
252
  <button
275
253
  key={agent.id}
@@ -316,16 +294,12 @@ export default function AppShell() {
316
294
  planDir={agentSessions[agent.id].planDir}
317
295
  />
318
296
  </div>
319
- ) : null
297
+ ) : null,
320
298
  )}
321
299
 
322
300
  {/* Prompt screen — shown for the active agent if it has no session yet (hidden in CLI mode) */}
323
301
  {activeAgent && !activeHasSession && !cliMode && (
324
- <TestPrompt
325
- agentId={activeAgent.id}
326
- agentName={activeAgent.name}
327
- onSubmit={startTest}
328
- />
302
+ <TestPrompt agentId={activeAgent.id} agentName={activeAgent.name} onSubmit={startTest} />
329
303
  )}
330
304
  </div>
331
305
  </div>
@@ -334,7 +308,6 @@ export default function AppShell() {
334
308
 
335
309
  function TestPrompt({
336
310
  agentId,
337
- agentName,
338
311
  onSubmit,
339
312
  }: {
340
313
  agentId: string;
@@ -389,9 +362,7 @@ function TestPrompt({
389
362
  Run
390
363
  </button>
391
364
  </div>
392
- <p className="text-xs text-zinc-600 mt-2">
393
- Press Enter to run. Shift+Enter for a new line.
394
- </p>
365
+ <p className="text-xs text-zinc-600 mt-2">Press Enter to run. Shift+Enter for a new line.</p>
395
366
  <div className="mt-4">
396
367
  <p className="text-xs text-zinc-600 mb-2">Examples:</p>
397
368
  <div className="flex flex-col gap-1.5">