diff --git a/packages/skills/skills/argent-device-interact/SKILL.md b/packages/skills/skills/argent-device-interact/SKILL.md index 4f992277..b3d7edc0 100644 --- a/packages/skills/skills/argent-device-interact/SKILL.md +++ b/packages/skills/skills/argent-device-interact/SKILL.md @@ -108,7 +108,7 @@ Read the exact error and choose the action that matches it: Coordinates: `0.0` = left/top, `1.0` = right/bottom. -Before tapping near the bottom of the screen in React Native apps, check that "Open Debugger to View Warnings" banners are not visible — tapping them breaks the debugger connection. Close them with the X icon if present. +When working with a React Native app, call the `dismiss-logbox` tool once near the start of your interaction session — this hides the bottom-screen LogBox banner (yellow warnings + red non-fatal errors) and keeps it hidden across JS reloads, without affecting the fullscreen redbox for fatal errors. **Never tap the banner or its X icon** — accidental hits open the fullscreen debugger overlay. If `describe` still shows an `AXGroup` near the bottom of the screen with a label that starts with `"! "`, call `dismiss-logbox` again (it's idempotent). ### gesture-swipe — Straight-line gesture diff --git a/packages/skills/skills/argent-metro-debugger/SKILL.md b/packages/skills/skills/argent-metro-debugger/SKILL.md index cf17f9d6..0c3ca796 100644 --- a/packages/skills/skills/argent-metro-debugger/SKILL.md +++ b/packages/skills/skills/argent-metro-debugger/SKILL.md @@ -17,6 +17,8 @@ adb -s reverse tcp:8081 tcp:8081 `` is the Android `serial` from `list-devices`. Once reversed, the app on the device connects to Metro just like an iOS simulator does, and all `debugger-*` / `network-*` / `react-profiler-*` tools work unchanged. If the device restarts or adb drops, re-run the command. A failing Metro connection on Android almost always means `adb reverse` has not been done or has been lost. +**LogBox banner**: when working with a React Native app, call `dismiss-logbox` once at the start of your session. The tool resolves the same `JsRuntimeDebugger` service every `debugger-*` tool uses (auto-connecting if needed) and hides the bottom-screen LogBox banner for the rest of the session. JS reloads do not bring it back. The fullscreen redbox shown for fatal/uncaught errors is unaffected. + ## 2. Tool Overview All tools accept `port` (default 8081) AND `device_id` (the iOS Simulator UDID or Android serial, a.k.a. `logicalDeviceId` — the CDP-reported id that matches the device). Always make sure you target the correct app on the correct device. @@ -36,6 +38,7 @@ One Metro port can serve multiple connected devices (e.g. two simulators on `loc | ----------------------- | ---------------------------------------------------------------------------------------- | | `debugger-reload-metro` | Reload all connected apps (like pressing "r" in Metro terminal). Needs a CDP target. | | `restart-app` | Terminate and relaunch the app by UDID and bundleId. Use when app lost Metro connection. | +| `dismiss-logbox` | Hide the bottom-screen LogBox banner (warnings + non-fatal errors) without affecting the fullscreen redbox for fatal errors. Auto-connects the debugger if not already connected. Idempotent. The disable script is re-injected automatically on every JS reload via a CDP `executionContextCreated` listener inside the debugger blueprint — so calling once per session is enough. | ### Inspection & console diff --git a/packages/skills/skills/argent-react-native-app-workflow/SKILL.md b/packages/skills/skills/argent-react-native-app-workflow/SKILL.md index c7f992ee..c8150dfd 100644 --- a/packages/skills/skills/argent-react-native-app-workflow/SKILL.md +++ b/packages/skills/skills/argent-react-native-app-workflow/SKILL.md @@ -61,6 +61,14 @@ Optional: specify the target device, e.g. `npx react-native run-ios --simulator= - [ ] If the device isn't booted: use `boot-device` with the iOS `udid` or Android `avdName`. Refer to the `argent-ios-simulator-setup` / `argent-android-emulator-setup` skill. - [ ] Android: `adb -s reverse tcp:8081 tcp:8081` done. +### 1.4 Hide the LogBox banner + +After the app is running and Metro is connected, call `dismiss-logbox` once. This connects the JS runtime debugger if it isn't already connected and hides the bottom-screen LogBox banner (yellow warnings + red non-fatal errors) for the rest of the session, including across JS reloads. The fullscreen redbox shown for fatal/uncaught errors is **not** affected — fatal errors remain visible. + +- Pass `port` if Metro isn't on 8081 (look it up from the `argent-environment-inspector` result). +- Idempotent: re-call after a reload only if you have a specific reason to suspect the banner returned (rare). +- Never tap the banner directly — the X target overlaps the bottom tab bar in most apps and missed taps open the fullscreen debugger overlay. + --- ## 2. Ensuring / Debugging Metro @@ -225,6 +233,7 @@ If the user's intent is ambiguous (run existing tests, write new tests, or find | Check Metro status | `debugger-status` tool | | Inspect React component tree | `debugger-component-tree` tool | | Run JS in app | `debugger-evaluate` tool | +| Hide LogBox banner | `dismiss-logbox` tool (call once per RN session; auto-connects the debugger; never affects fullscreen fatal redbox) | | iOS native logs | `npx react-native log-ios` | | Android native logs | `npx react-native log-android` or `adb -s logcat` | | Clean + reinstall (nuclear) | See §3.1 step 3 | diff --git a/packages/tool-server/src/blueprints/js-runtime-debugger.ts b/packages/tool-server/src/blueprints/js-runtime-debugger.ts index 27a4596f..d9a07ca0 100644 --- a/packages/tool-server/src/blueprints/js-runtime-debugger.ts +++ b/packages/tool-server/src/blueprints/js-runtime-debugger.ts @@ -153,6 +153,32 @@ export const jsRuntimeDebuggerBlueprint: ServiceBlueprint>(); + cdp.events.on("executionContextCreated", () => { + for (const t of pendingDisables) clearTimeout(t); + pendingDisables.clear(); + for (const delayMs of [1500, 3500]) { + const timer = setTimeout(() => { + pendingDisables.delete(timer); + cdp.evaluate(DISABLE_LOGBOX_SCRIPT).catch(warnOnError("DISABLE_LOGBOX_SCRIPT[ctx]")); + }, delayMs); + pendingDisables.add(timer); + } + }); + await sourceMaps.waitForPending(); const sourceResolver = createSourceResolver(port, metro.projectRoot); diff --git a/packages/tool-server/src/tools/debugger/dismiss-logbox.ts b/packages/tool-server/src/tools/debugger/dismiss-logbox.ts new file mode 100644 index 00000000..7862ba6a --- /dev/null +++ b/packages/tool-server/src/tools/debugger/dismiss-logbox.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; +import type { ToolDefinition } from "@argent/registry"; +import type { JsRuntimeDebuggerApi } from "../../blueprints/js-runtime-debugger"; +import { DISABLE_LOGBOX_SCRIPT } from "../../utils/debugger/scripts/disable-logbox"; + +const zodSchema = z.object({ + port: z.coerce.number().default(8081).describe("Metro server port"), + device_id: z + .string() + .describe( + "Device logicalDeviceId from debugger-connect (iOS simulator UDID or Android logicalDeviceId). " + + "Auto-connects the JS runtime debugger if not already connected." + ), +}); + +export const dismissLogboxTool: ToolDefinition< + z.infer, + { dismissed: boolean; deviceName: string; appName: string; logicalDeviceId: string | undefined } +> = { + id: "dismiss-logbox", + description: + "Hide the React Native LogBox banner (yellow warnings + red non-fatal errors) at the " + + "bottom of the screen. Does NOT affect the fullscreen LogBox shown for fatal/uncaught " + + "errors — those remain visible. Idempotent and safe to call repeatedly. Auto-connects " + + "the JS runtime debugger if needed; once connected, the banner stays hidden across JS " + + "reloads for the rest of the session. Call once near the start of any RN session, " + + "and never tap the banner directly (the X target overlaps the bottom tab bar in most apps).", + alwaysLoad: true, + searchHint: "logbox banner warning error dismiss hide notification redbox yellow", + zodSchema, + services: (params) => ({ + debugger: `JsRuntimeDebugger:${params.port}:${params.device_id}`, + }), + async execute(services) { + const api = services.debugger as JsRuntimeDebuggerApi; + await api.cdp.evaluate(DISABLE_LOGBOX_SCRIPT); + return { + dismissed: true, + deviceName: api.deviceName, + appName: api.appName, + logicalDeviceId: api.logicalDeviceId, + }; + }, +}; diff --git a/packages/tool-server/src/utils/debugger/cdp-client.ts b/packages/tool-server/src/utils/debugger/cdp-client.ts index 8e085a5b..903421cd 100644 --- a/packages/tool-server/src/utils/debugger/cdp-client.ts +++ b/packages/tool-server/src/utils/debugger/cdp-client.ts @@ -32,6 +32,7 @@ export type CDPClientEvents = { scriptParsed: (script: ScriptInfo) => void; paused: (params: Record) => void; consoleAPICalled: (params: ConsoleAPICalledParams) => void; + executionContextCreated: (params: Record) => void; }; interface CDPExceptionDetails { @@ -323,6 +324,10 @@ export class CDPClient { this.events.emit("paused", params); } + if (method === "Runtime.executionContextCreated") { + this.events.emit("executionContextCreated", params); + } + this.events.emit("event", method, params); } diff --git a/packages/tool-server/src/utils/debugger/scripts/disable-logbox.ts b/packages/tool-server/src/utils/debugger/scripts/disable-logbox.ts index c0602a0b..dddc15bf 100644 --- a/packages/tool-server/src/utils/debugger/scripts/disable-logbox.ts +++ b/packages/tool-server/src/utils/debugger/scripts/disable-logbox.ts @@ -1,7 +1,13 @@ /** * IIFE that scans the Metro module registry for the LogBox module - * and calls `ignoreAllLogs(true)` to suppress the yellow/red overlay, - * then clears any already-queued LogBox entries (e.g. SegmentFetcher). + * and calls `ignoreAllLogs(true)` to suppress the bottom-screen LogBox + * banner (yellow warnings + red non-fatal errors). + * + * The fullscreen LogBox shown for fatal/uncaught errors is **not** + * affected — `ignoreAllLogs` gates only the banner via `_isDisabled`, + * not the redbox. Leftover warning logs already in the LogBox registry + * remain in memory but stay invisible because `_isDisabled === true`; + * RN deduplicates by category so growth is bounded. * * Uses `__r.getModules()` (available in DEV) to iterate only * already-initialized modules, avoiding forced evaluation of unloaded @@ -66,7 +72,8 @@ export const DISABLE_LOGBOX_SCRIPT = `(function() { if (LB && typeof LB.ignoreAllLogs === 'function') { LB.ignoreAllLogs(true); } - if (LBData) { - LBData.clear(); - } + // Intentionally NOT clearing the LogBox data store — that path routes + // through setSelectedLog(-1) and would dismiss an open fullscreen + // redbox. The banner is gated by _isDisabled directly, so leftover + // warning logs stay invisible. })()`; diff --git a/packages/tool-server/src/utils/setup-registry.ts b/packages/tool-server/src/utils/setup-registry.ts index 324d2534..ee48857b 100644 --- a/packages/tool-server/src/utils/setup-registry.ts +++ b/packages/tool-server/src/utils/setup-registry.ts @@ -35,6 +35,7 @@ import { debuggerReloadMetroTool } from "../tools/debugger/debugger-reload-metro import { debuggerComponentTreeTool } from "../tools/debugger/debugger-component-tree"; import { debuggerInspectElementTool } from "../tools/debugger/debugger-inspect-element"; import { debuggerLogRegistryTool } from "../tools/debugger/debugger-log-registry"; +import { dismissLogboxTool } from "../tools/debugger/dismiss-logbox"; import { networkLogsTool } from "../tools/network/network-logs"; import { networkRequestTool } from "../tools/network/network-request"; import { createDescribeTool } from "../tools/describe"; @@ -102,6 +103,7 @@ export function createRegistry(): Registry { registry.registerTool(debuggerComponentTreeTool); registry.registerTool(debuggerInspectElementTool); registry.registerTool(debuggerLogRegistryTool); + registry.registerTool(dismissLogboxTool); registry.registerTool(networkLogsTool); registry.registerTool(networkRequestTool); registry.registerTool(createDescribeTool(registry)); diff --git a/packages/tool-server/test/blueprints/disable-logbox-script.test.ts b/packages/tool-server/test/blueprints/disable-logbox-script.test.ts new file mode 100644 index 00000000..056ab6bf --- /dev/null +++ b/packages/tool-server/test/blueprints/disable-logbox-script.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from "vitest"; +import { DISABLE_LOGBOX_SCRIPT } from "../../src/utils/debugger/scripts/disable-logbox"; + +describe("DISABLE_LOGBOX_SCRIPT", () => { + it("calls LogBox.ignoreAllLogs(true) to gate the banner", () => { + expect(DISABLE_LOGBOX_SCRIPT).toContain("ignoreAllLogs(true)"); + }); + + it("does NOT call LBData.clear() — would dismiss an open fullscreen redbox", () => { + expect(DISABLE_LOGBOX_SCRIPT).not.toContain(".clear()"); + expect(DISABLE_LOGBOX_SCRIPT).not.toContain("LBData.clear"); + }); + + it("is wrapped in an IIFE", () => { + expect(DISABLE_LOGBOX_SCRIPT.trim().startsWith("(function()")).toBe(true); + expect(DISABLE_LOGBOX_SCRIPT.trim().endsWith("})()")).toBe(true); + }); +}); diff --git a/packages/tool-server/test/debugger/cdp-execution-context-event.test.ts b/packages/tool-server/test/debugger/cdp-execution-context-event.test.ts new file mode 100644 index 00000000..396335b8 --- /dev/null +++ b/packages/tool-server/test/debugger/cdp-execution-context-event.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { WebSocketServer, WebSocket } from "ws"; +import { CDPClient } from "../../src/utils/debugger/cdp-client"; + +let wss: WebSocketServer; +let port: number; +let serverWs: WebSocket | null = null; + +beforeEach(async () => { + serverWs = null; + await new Promise((resolve) => { + wss = new WebSocketServer({ port: 0 }, () => { + port = (wss.address() as { port: number }).port; + resolve(); + }); + }); + wss.on("connection", (ws) => { + serverWs = ws; + }); +}); + +afterEach(async () => { + if (serverWs) serverWs.close(); + await new Promise((resolve) => wss.close(() => resolve())); +}); + +function waitForServer(): Promise { + return new Promise((resolve) => { + if (serverWs) return resolve(serverWs); + wss.once("connection", (ws) => resolve(ws)); + }); +} + +describe("CDPClient executionContextCreated event", () => { + it("emits typed executionContextCreated when Runtime.executionContextCreated arrives", async () => { + const client = new CDPClient(`ws://127.0.0.1:${port}`); + const connectPromise = client.connect(); + const ws = await waitForServer(); + await connectPromise; + + const received: Record[] = []; + client.events.on("executionContextCreated", (params) => { + received.push(params); + }); + + const ctxPayload = { + context: { id: 1, name: "main", origin: "" }, + }; + ws.send( + JSON.stringify({ + method: "Runtime.executionContextCreated", + params: ctxPayload, + }) + ); + + // Give the WS message a tick to be dispatched. + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(received).toHaveLength(1); + expect(received[0]).toEqual(ctxPayload); + + await client.disconnect(); + }); + + it("still emits the generic event after executionContextCreated (no regression)", async () => { + const client = new CDPClient(`ws://127.0.0.1:${port}`); + const connectPromise = client.connect(); + const ws = await waitForServer(); + await connectPromise; + + const genericMethods: string[] = []; + client.events.on("event", (method) => { + genericMethods.push(method); + }); + + ws.send( + JSON.stringify({ + method: "Runtime.executionContextCreated", + params: { context: { id: 2 } }, + }) + ); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(genericMethods).toContain("Runtime.executionContextCreated"); + + await client.disconnect(); + }); +}); diff --git a/packages/tool-server/test/debugger/dismiss-logbox.test.ts b/packages/tool-server/test/debugger/dismiss-logbox.test.ts new file mode 100644 index 00000000..df55fdee --- /dev/null +++ b/packages/tool-server/test/debugger/dismiss-logbox.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import { dismissLogboxTool } from "../../src/tools/debugger/dismiss-logbox"; + +describe("dismiss-logbox tool definition", () => { + it("has the expected id", () => { + expect(dismissLogboxTool.id).toBe("dismiss-logbox"); + }); + + it("is marked alwaysLoad", () => { + expect(dismissLogboxTool.alwaysLoad).toBe(true); + }); + + it("defaults port to 8081", () => { + const parsed = dismissLogboxTool.zodSchema.parse({ device_id: "abc-123" }); + expect(parsed.port).toBe(8081); + }); + + it("requires device_id", () => { + expect(() => dismissLogboxTool.zodSchema.parse({})).toThrow(); + }); + + it("resolves services to the JsRuntimeDebugger URN", () => { + const services = dismissLogboxTool.services!({ port: 8081, device_id: "device-xyz" }); + expect(services).toEqual({ + debugger: "JsRuntimeDebugger:8081:device-xyz", + }); + }); + + it("uses the provided port when not default", () => { + const services = dismissLogboxTool.services!({ port: 9090, device_id: "device-xyz" }); + expect(services).toEqual({ + debugger: "JsRuntimeDebugger:9090:device-xyz", + }); + }); +});