Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/skills/skills/argent-device-interact/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions packages/skills/skills/argent-metro-debugger/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ adb -s <serial> reverse tcp:8081 tcp:8081

`<serial>` 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.
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <serial> 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
Expand Down Expand Up @@ -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 <serial> logcat` |
| Clean + reinstall (nuclear) | See §3.1 step 3 |
Expand Down
26 changes: 26 additions & 0 deletions packages/tool-server/src/blueprints/js-runtime-debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,32 @@ export const jsRuntimeDebuggerBlueprint: ServiceBlueprint<JsRuntimeDebuggerApi,

await cdp.evaluate(DISABLE_LOGBOX_SCRIPT).catch(warnOnError("DISABLE_LOGBOX_SCRIPT"));

// Re-inject the disable script whenever a new JS execution context is
// created. JS reloads (cmd-R, shake menu, fast-refresh full reload,
// DevSettings.reload, debugger-reload-metro via CDP Page.reload) all
// destroy and re-create the JS context, which resets LogBox._isDisabled
// back to false. The CDP connection survives the reload, so this
// listener fires reliably and re-disables the banner without requiring
// the agent to re-call dismiss-logbox.
//
// Two delayed evals run after each event: an early one to cover fast
// bundles, a later one to cover cold-boot / slow-device cases where the
// bundle isn't ready at 1500ms. The script is idempotent and bounded-cost,
// so redundant runs are free insurance. Failures are logged via
// warnOnError but do not break the debugger session.
const pendingDisables = new Set<ReturnType<typeof setTimeout>>();
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);
Expand Down
44 changes: 44 additions & 0 deletions packages/tool-server/src/tools/debugger/dismiss-logbox.ts
Original file line number Diff line number Diff line change
@@ -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<typeof zodSchema>,
{ 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,
};
},
};
5 changes: 5 additions & 0 deletions packages/tool-server/src/utils/debugger/cdp-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type CDPClientEvents = {
scriptParsed: (script: ScriptInfo) => void;
paused: (params: Record<string, unknown>) => void;
consoleAPICalled: (params: ConsoleAPICalledParams) => void;
executionContextCreated: (params: Record<string, unknown>) => void;
};

interface CDPExceptionDetails {
Expand Down Expand Up @@ -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);
}

Expand Down
17 changes: 12 additions & 5 deletions packages/tool-server/src/utils/debugger/scripts/disable-logbox.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
})()`;
2 changes: 2 additions & 0 deletions packages/tool-server/src/utils/setup-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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));
Expand Down
18 changes: 18 additions & 0 deletions packages/tool-server/test/blueprints/disable-logbox-script.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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<void>((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<void>((resolve) => wss.close(() => resolve()));
});

function waitForServer(): Promise<WebSocket> {
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<string, unknown>[] = [];
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();
});
});
35 changes: 35 additions & 0 deletions packages/tool-server/test/debugger/dismiss-logbox.test.ts
Original file line number Diff line number Diff line change
@@ -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",
});
});
});
Loading