Commit Graph

15 Commits

Author SHA1 Message Date
filip131311 4975ff490c fix(debugger): cut debugger tool fail rate — structured not-connected results, classified CDP failures, narrow recovery (#610)
## Why

PostHog (30d) shows the debugger tool family is argent's least reliable
surface:

| tool | fail rate | dominant causes |
|---|---|---|
| `debugger-status` | **37%** | METRO_NO_TARGETS /
SERVICE_INITIALIZATION_FAILED / zod |
| `debugger-connect` | **24%** | SERVICE_INITIALIZATION_FAILED (~7k, >1k
users), METRO_NO_TARGETS |
| `debugger-log-registry` | 41.7% raw / 4.2% excl. storms | one user
retry-looped it **12,742×** on NO_TARGETS |

Three design issues drive this: (1) "app/Metro not ready yet" — an
expected precondition — is reported as a tool *error*, which both
inflates fail rates and invites agent retry storms; (2) every CDP
init/lifecycle fault lands in telemetry as the catch-all
`REGISTRY_SERVICE_INITIALIZATION_FAILED` with `error_kind=unknown`; (3)
~600 users/30d hit zod validation failures with zero visibility into
which parameter was wrong.

## What

**1. Classified failures** (`d71efa43`) — six former plain-`Error` sites
in `cdp-client.ts` / `target-selection.ts` now carry dedicated
`DEBUGGER_CDP_*` / `DEBUGGER_TARGET_DEVICE_MISMATCH` signals (messages
byte-identical). The registry's two causeless "terminating window"
`ServiceInitializationError`s get their own
`REGISTRY_SERVICE_TERMINATING` code.

**2. Narrow recovery** (`ff9c1955`) — `recoverable()` on both debugger
blueprints, scoped to windows where retry is provably safe: Metro
recovers only `DEBUGGER_CDP_NOT_CONNECTED` (request never left the host;
a detected socket death already self-heals for the *next* call via the
terminated cascade — recovering `CONNECTION_CLOSED` there would be dead
code, see the blueprint comment). Chromium also recovers
`CONNECTION_CLOSED` for the tab-switch reconnect window.
`REQUEST_TIMEOUT` stays unrecoverable (double-execution risk). A
reachability test proves the retry actually fires for a RUNNING node.

**3. Structured not-connected results** (`48b91ffd`) — `debugger-status`
and `debugger-log-registry` become factories that return `{ status:
"not_connected", reason, detail, guidance }` for classified
preconditions (`metro_not_running | no_app_connected | device_mismatch |
cdp_unreachable | stale_connection | reconnecting`) and still throw on
unexpected faults. `detail` preserves the original error text for
string-matching agents. `debugger-status` gains a socket-state gate
(Metro: dispose stale node; Chromium: report transient reconnect, no
dispose — the client belongs to the ChromiumCdp dependency).
log-registry keeps **no** gate on purpose: captured logs are readable
over a dead socket, and disposal would destroy the post-crash log file.
Flows/run-sequence map a not_connected result to a **failed step**
(mirrors `isUnmetUiWaitResult`) so recorded connectivity gates don't
silently pass. `debugger-evaluate` re-codes agent-expression throws as
`DEBUGGER_EVALUATE_EXPRESSION_THREW`.

**4. Telemetry + guidance + docs** (`7602a70d`) —
`tool:fail.invalid_params` (schema-declared names only; unknown keys →
literal `"unrecognized_keys"`; capped emit-side at 16), new
`debugger:tool_outcome` event (coded enum; joins
`tool:invoke`/`complete` via `tool_invocation_id` —
`ai_client`/`duration` live on the joined row), anti-retry-loop guidance
appended to the Metro discovery errors (first sentences verbatim —
skills match on them), six skills updated, Telemetry.md bumped to v1.03
with the new coded diagnostics disclosed.

## Dashboard migration note

`debugger-status` / `debugger-log-registry` precondition failures stop
emitting `tool:fail` — track them via `debugger:tool_outcome` (`outcome
!= 'connected'`). Per-AI-client breakdowns of those outcomes need a join
on `tool_invocation_id` against `tool:complete`.

## Testing

- Full suites green: tool-server 3,142, telemetry 290, registry 87. New
coverage: recoverable truth tables (incl. non-Error inputs), status
not-connected matrix
(Metro/Chromium/terminating-window/stale/dispose-side-effect/fall-through-rethrow),
log-registry rows, zod `invalid_params` derivation + emission-level
guard, flow gate, evaluate rewrap, sanitize-level enum checks.
- Live-tested against a booted iOS simulator + RN app and an Electron
target (details in PR comments).

Plan hardened through 3 adversarial review rounds (46 confirmed findings
addressed) before implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-10 11:51:45 +02:00
Ignacy Łątka d4b9818d64 fix(debugger): deliver component-tree errors via binding + stop truncating log levels (#438)
Fixes two confirmed defects in the JS-runtime debugger subsystem.

## Defect A — `debugger-component-tree` hangs ~15s and throws on every
error condition

**File:**
`packages/tool-server/src/utils/debugger/scripts/component-tree.ts`

The injected fiber-walk script delivers its success result through the
`__argent_callback` CDP binding, but every error path instead did
`return JSON.stringify({ error: ... })` from the async IIFE — `'No
DevTools hook'`, `'No fiber roots'`, `'Could not find UIManager'`.

The consumer (`evaluateWithBinding`, `cdp-client.ts`) runs
`Runtime.evaluate` with `returnByValue:false`/`awaitPromise:false`, so
the IIFE's return value is discarded. The promise only settles when the
binding fires or the 15s timeout elapses. So on a release/production
build, before the first render, or on a non-RN runtime,
`debugger-component-tree` blocked for ~15s and then threw a generic
`DEBUGGER_CDP_BINDING_TIMEOUT` instead of returning the intended
diagnostic immediately.

**Observed:** callback never invoked (0 calls) on the error paths; the
tool hangs to the binding timeout.
**Expected:** the error is delivered through the binding right away and
surfaces as `Error: <message>` via the consumer's
`JSON.parse(response.result).error` path.

**Fix:** mirror the sibling `inspect-at-point.ts`. Added an
`__argent_fail(msg)` helper that routes the error through
`__argent_callback` in the exact same wire shape as the success path (`{
requestId, result: JSON.stringify({ error: msg }) }`), and converted all
three error paths to call it and `return`.

## Defect B — `LogFileWriter` truncates console levels longer than 5
chars

**File:** `packages/tool-server/src/utils/debugger/log-file-writer.ts`

The flat-line level was rendered as `LEVEL_DISPLAY[entry.level] ??
entry.level.toUpperCase().padEnd(5).slice(0, 5)`. The `.slice(0,5)`
truncated any level not in the 5-entry `LEVEL_DISPLAY` map. CDP's
`Runtime.consoleAPICalled.type` includes `"warning"` (`console.warn`)
and `"assert"` (`console.assert`) — neither is in the map — so
`readAll()`/`readFiltered()` returned `"warni"`/`"asser"`, and
`readFiltered({ level: "warning" })` matched nothing.

**Observed:** writing level `"warning"` → `readAll()[0].level ===
"warni"`; `readFiltered({ level: "assert" }).total === 0`.
**Expected:** the canonical level round-trips exactly for levels of any
length.

**Fix:** drop `.slice(0,5)` — keep `padEnd(5)` for column alignment but
never cut. `LINE_RE` captures the level as `\S+`, so a non-truncated,
whitespace-free level round-trips through `parseFlatLine` unchanged.
Short levels (log/info/warn/error/debug) are unaffected.

## Tests

- `packages/tool-server/test/debugger/comptree-error-channel.test.ts` —
asserts `__argent_callback` is invoked on the error paths and the error
is encoded as `parsed.error`.
- `packages/tool-server/test/debugger/log-file-writer-level.test.ts` —
asserts `"warning"`/`"assert"` round-trip and filter correctly, and
short levels still work.

Both new files fail before the fix and pass after. Full `tool-server`
suite green (1627 tests); repo-root `tsc --build` clean; eslint clean on
changed files.
2026-07-03 16:19:28 +02:00
Ignacy Łątka 9c43da3b56 fix(tool-server): fall back to raw locations when inspect-element symbolication fails (#284)
## Problem

`debugger-inspect-element`, called with the default `resolveSourceMaps:
true`, returned `source: null` **and** `code: null` for **every** item
whenever Metro symbolication did not yield a usable original location.
This made the tool effectively useless in its default mode — the caller
got a list of component names with no file:line and no code fragment to
edit.

## Root cause

Two independent issues compounded:

1. **No raw fallback in the tool.** In `debugger-inspect-element.ts`,
the `resolveSourceMaps` branch only assigned `source`/`code` when
`symbolicate()` returned a result. When it returned `null`
(symbolication failed, or was filtered out — see below), the item was
left with `source: null` / `code: null`, **discarding the raw bundled
frame the tool already had** in `item.frame`.

2. **Over-broad `node_modules` rejection in the resolver.** In
`source-resolver.ts`, `symbolicateFrame` did `if
(frame.file.includes("node_modules")) return null;`. That was meant to
drop frames that symbolication couldn't map (where Metro echoes the
bundle URL back). But it also threw away **genuinely-mapped** sources
whose real path legitimately lives in `node_modules` — e.g.
`expo-router` / `react-navigation` route components — turning a
perfectly good resolved location into `null`, which then hit issue (1)
and nulled the whole item.

## Fix

Two minimal, localized changes:

- **`debugger-inspect-element.ts`** — add an `else` to the
`resolveSourceMaps` branch that falls back to the raw bundled frame `{
file: item.frame.file, line: item.frame.line, column: item.frame.col }`,
mirroring the exact shape produced by the `resolveSourceMaps: false`
branch. When symbolication can't map a frame, the caller now still gets
the bundled location instead of `null`.

- **`source-resolver.ts`** (line ~93) — replace the
`includes("node_modules")` guard with `if
(/^https?:\/\//.test(frame.file)) return null;`. A successful
symbolication yields a real file path (kept, including legitimate
`node_modules` route components); a failed one yields the original
`http(s)` bundle URL echoed back (rejected). This keeps the diff to a
single line + comment at line ~93 to stay easy to reconcile.

## Testing

`cd packages/tool-server && vitest run
test/metro/source-resolver.test.ts
test/debugger/inspect-element-filter.test.ts`

Added fail-before / pass-after coverage at two layers:

- **Resolver level** (`test/metro/source-resolver.test.ts`): mocks
`fetch` for `/symbolicate` and asserts that
- a frame mapping to a `node_modules/.../*.js` path is **kept**
(returned that mapped, project-relative location),
  - an echoed-back `http(s)` bundle URL is **rejected** (`null`),
  - a real app-source path is mapped relative to the project root,
  - a thrown request returns `null`.

- **Tool level** (`test/debugger/inspect-element-filter.test.ts`):
drives `debuggerInspectElementTool.execute()` with a fake
`JsRuntimeDebuggerApi` (no live CDP) and asserts that when
`symbolicate()` returns `null`, the item retains the raw `{ file, line,
column }` (and reads no source fragment); when it returns a mapped
location, that location and its code fragment are used.

Verified the fail-before / pass-after cycle by checking out
`origin/main` for the two source files only (keeping the new tests): the
3 new assertions fail on `origin/main` (`source` is `null` for the
raw-fallback case; the node_modules-mapped source is `null`; the echoed
bundle URL is wrongly accepted), then pass once the fixes are restored.
All 34 tests pass with the fix; the 31 pre-existing tests are
unaffected.

`tsc --noEmit -p tsconfig.test.json` reports no errors in any changed
file (the only errors are pre-existing module-resolution artifacts from
the symlinked `node_modules`: `pngjs`,
`@argent/native-devtools-android`, `@argent/update-core`).

## Known limitation / follow-up

User-defined and route component frames that lack `_debugStack` under
**React 19** (where `_debugSource` was removed) are still `null` even in
raw mode, because there is no bundled frame to fall back to in the first
place. That is a separate, riskier change (it requires a different
source for the frame entirely) and is intentionally **out of scope**
here.

## Note for reconcilers

There are other open branches touching `source-resolver.ts`
(`fix/source-resolver-traversal`, `fix/source-map-ssrf`). This change is
deliberately confined to the single guard at line ~93 (plus an
explanatory comment) to keep it trivially mergeable alongside them.
2026-06-18 12:53:21 +02:00
Ignacy Łątka b5fc1f2a50 fix(tool-server): key Fabric component-tree measurement by nativeTag so tap coords don't collapse to 0.5,0.5 (#286)
## Problem

`debugger-component-tree` returned **every** element with the same tap
coordinate — `(tap: 0.50,0.50)` — on the New Architecture
(Fabric/bridgeless), making the tool's primary output (per-element tap
coordinates) unusable. Reproduced live on an RN 0.81.5 Fabric app; the
tree walk, skip stats, and screen size were all correct, only the
coordinates collapsed.

## Root cause

The injected script (`utils/debugger/scripts/component-tree.ts`)
batch-measures host views and caches each rect under a per-host key
built as `(hi.f ? 'f' : 'p') + hi.n`. On **Fabric**, `getHostInfo`
returns the shadow **node object** as `hi.n` (`{ f: true, n:
fiber.stateNode.node }`), so the key stringifies to `"f[object Object]"`
for *every* host. All hosts collapsed to a single cache entry → only the
first (root) view was measured → every component inherited the root's
full-screen rect → `(x + w/2)/screenW, (y + h/2)/screenH = 0.5, 0.5` for
all. Paper/old-arch was unaffected because there `hi.n` is already a
numeric `nativeTag`.

## Fix

Key the cache by a primitive `nativeTag` on Fabric
(`fiber.stateNode.canonical.nativeTag`, the same field the Paper branch
already uses), with a `WeakMap`-by-identity fallback so distinct shadow
nodes can never share a key even if `nativeTag` were ever absent.
`getHostInfo` now carries an explicit `key`, and the three cache-key
derivations use it. Paper/old-arch behavior is unchanged.

## Testing

**Live (first-hand), on a running RN 0.81 Fabric app** — walking the
live fiber tree:

| | Fabric host nodes | distinct measure keys |
|---|---|---|
| old key (`'f' + node`) | 2140 | **1** (`"f[object Object]"`) |
| new key (`'f' + nativeTag`) | 2140 | **2140** |

**Unit** — adds `test/debugger/component-tree-script.test.ts`, which
evaluates the injected script against a mocked Fabric tree (the existing
`component-tree.test.ts` only covered the pure post-processor with
pre-filled rects, which is why this shipped). The collapse is exercised
on both Fabric measure paths with **tagless** hosts (no numeric
`nativeTag`), so the cache key is forced onto the new
`WeakMap`-by-identity fallback — with a numeric tag the buggy `'f' +
node` key was already distinct and never collapsed. `vitest run` → 6/6
pass with the fix; reverting `component-tree.ts` to `main` fails both
collapse tests with `{ y: 100 }` where `{ y: 300 }` is expected (the
second component inheriting the first's rect — exactly the collapse).
2026-06-18 11:49:53 +02:00
Ignacy Łątka acfe80ab6b chore: add ESLint with typescript-eslint (#349)
## What

Adds type-aware linting to the workspace. Until now the only static gate
on JS/TS was Prettier (formatting) and `tsc` — there was no linter, so
the whole class of bugs `tsc` doesn't catch (floating promises, misused
promises, throwing non-Errors, unsafe stringification, dead code) went
unguarded across ~450 TS files.

- **`eslint.config.mjs`** — flat config. `recommendedTypeChecked` runs
on the TS sources via an explicit `project` list (each package keeps
tests in a separate `tsconfig.test.json` that `projectService` can't
auto-discover, so it's listed explicitly). Type-checked rules are
disabled for the loosely-typed plain-JS build/dev scripts. The
`argent-private` submodule and build artifacts are ignored (mirrors
`.prettierignore`).
- **`lint` / `lint:fix`** npm scripts.
- **`.github/workflows/lint.yml`** — builds the workspace (type-aware
rules need the referenced projects' declarations) then runs `eslint .`.
Node 24 (ESLint 10 requires `^20.19 || ^22.13 || >=24`).

## Rule calibration

This is an adoption PR, so it's deliberately calibrated rather than
maximal:

- **Kept on (bug-catchers):** `no-floating-promises`,
`no-misused-promises` (with `checksVoidReturn: false` to drop the noisy
callback variant), `only-throw-error`, `no-base-to-string`,
`prefer-promise-reject-errors`, `no-unused-vars` (`_`-prefix escape
hatch), plus the ESLint recommended set.
- **Parked off as documented debt:** the high-volume style/`any`-family
rules — `no-unsafe-*`, `require-await`, `no-unnecessary-type-assertion`,
`restrict-template-expressions`, `unbound-method`, `no-explicit-any`,
etc. These flag broad classes of pre-existing code and are out of scope
here; each is commented as a ratchet target for follow-up passes.

All findings from the kept-on rules are fixed in this PR (dead
code/imports, error-cause chaining, **3 genuine floating promises**,
non-Error throws, unsafe stringification). Stale unused `eslint-disable`
directives left over in the tree were removed by `--fix`.

## Verification

Locally, all green: `eslint .` (0 errors), `tsc --build`,
`typecheck:tests`/`typecheck:scripts`, the full `vitest` suite (no
behavior change from the fixes), and `prettier --check`.

## Not included (intentionally)

Other static-analysis gates discussed (CodeQL, dependency-audit,
ShellCheck, actionlint) are out of scope for this PR.
2026-06-17 12:20:55 +02:00
Ignacy Łątka ff49143d03 fix(debugger): make inspect-element work on old-arch RN (RN 0.81 bridge) (#344)
## Problem

`debugger-inspect-element` returned `{"error":"no host fiber"}` at every
coordinate on a React Native 0.81.5 app (Hermes, old architecture /
legacy bridge, React Compiler). Verified live on a real device.

## Root causes (three layers, each verified live)

1. **Host-fiber resolution** - `findHostFiber` only recognised old-arch
host fibers via `stateNode.canonical`, but RN 0.81 bridge host fibers
expose `stateNode._nativeTag` with no `canonical`, so it found nothing
and threw "no host fiber". Fixed by also matching `_nativeTag` (mirrors
the working `component-tree.ts` getHostInfo). On the real tree: 87/87
host fibers had `_nativeTag` and 0 had `canonical`.
2. **Wrong anchor** - even with the host fiber resolved, the first host
fiber is a sibling subtree that does not contain the touch point, so the
renderer's native `findSubviewIn` resolved nothing for *every*
coordinate (touchedViewTag undefined). Fixed by anchoring at the
**FiberRoot container** (its `containerTag`), the universal ancestor.
Fabric and older Paper (no containerTag) keep the host-fiber anchor.
3. **Changed data shape** - RN 0.81 no longer returns
`data.closestInstance`/`hierarchy`; it returns `data.componentStack` (a
React stack string). The old extraction produced empty results. Now
parsed into items; bundle source locations are symbolicated by the tool
layer exactly like `_debugStack` frames.

## Result

Live on a real RN 0.81 app, inspecting a point now returns the real
component hierarchy (170 frames: `SplashScreen`, `ErrorBoundary`,
`LoggedOut`, `NativeStackNavigator`, `HomeTabNavigator`, `SceneView`,
`NavigationProvider`, ...) with source locations.

## Safety / scope

- Fabric (new-arch) and iOS paths unchanged; canonical-publicInstance
handling is byte-for-byte preserved (incl. the lazily-null Fabric case).
- Distinct from #284 (symbolication fallback - a different, later
failure mode).
- Unit tests added for: `_nativeTag` host-fiber resolution, container
anchoring, componentStack parsing, and the Fabric null-publicInstance
no-regression case. The test harness evaluates the generated script, so
it exercises the real emitted code.
- Validated end-to-end against a live emulator, not only unit tests.
2026-06-16 16:20:50 +02:00
Ignacy Łątka 4999102238 fix(profiler): verbose errors when target app cannot be profiled (#201)
If you point the profiler or inspect-element tool at an app that can't
actually be profiled (usually a release build with React DevTools
stripped out), the tools currently blow up with a cryptic `Cannot read
property 'X' of undefined` and leave the session in a weird half-started
state. This PR makes them say what went wrong and what to do about it
instead.

Profiler and inspector tools used to crash with generic "Cannot read
property 'X' of undefined" TypeErrors whenever they hit a runtime
without `__REACT_DEVTOOLS_GLOBAL_HOOK__` (release/production builds
where DevTools is stripped) or against a partial/malformed Hermes CPU
profile. Surface the diagnosis up front instead, and stop leaving a
half-initialised session for downstream tools to trip over.

# Technical details

- `inspect-at-point.ts`: guard the injected script for missing hook /
renderer / fiber root and report through the existing
`__argent_callback` error channel; export
`INSPECT_NO_DEVTOOLS_HOOK_ERROR`, `INSPECT_NO_RENDERER_ERROR`,
`INSPECT_NO_FIBER_ROOT_ERROR` so callers can recognise the same
diagnosis without string-matching bespoke phrasings.
- `react-profiler-start.ts`: replace the terse "production build"
one-liner with the verbose `NO_DEVTOOLS_HOOK_ERROR` (rebuild in dev
mode, three likely causes, concrete `run-ios` / Expo dev client hints).
Export `NO_RENDERERS_ATTACHED_ERROR` for the distinct "hook present but
no renderer registered" state.
- `react-profiler-stop.ts`: refuse to stop when `api.profilingActive` is
false (start failed before reaching the sampler), and validate the
Hermes profile shape (`samples` / `nodes` / `timeDeltas` arrays) before
handing it to the analyse pipeline.
- `dump.ts` (`readCpuProfile`): validate shape inline at load so
`profiler-load` of an older / partial dump fails fast with an actionable
message instead of crashing `buildCpuSampleIndex` later.
- `react-profiler-renders.ts` / `react-profiler-fiber-tree.ts`: branch
on the in-runtime error code so "no `__REACT_DEVTOOLS_GLOBAL_HOOK__`"
gets the rebuild-in-dev advice while "no renderers attached to hook"
gets the wait-for-render / let-`react-profiler-start`-bootstrap advice —
separating two failure modes that the old `HOOK_MISSING_MESSAGE`
collapsed into one misleading message.

### Tests

- New `inspect-at-point-guards.test.ts` — eval the injected IIFE against
a controlled `globalThis` to verify each guard fires the verbose
diagnostic and routes through `__argent_callback`.

---------

Co-authored-by: Filip131311 <contact@filipkaminski.com>
2026-06-12 10:39:16 +02:00
Alexander Brokking 232762ebd0 fix(tool-server): make debugger-component-tree work on New Arch / multi-renderer apps (#316)
## Summary

`debugger-component-tree` returns **"No visible components found on
screen"** on React Native **New Architecture (Fabric / bridgeless)**
apps that also use a secondary React reconciler such as
`react-native-skia` — even when the app is fully rendered and CDP is
connected. This PR fixes the two root causes so the tool returns the
real component tree with names, testIDs, and accurate tap coordinates.

## Root cause

The injected fiber-walk script
(`packages/tool-server/src/utils/debugger/scripts/component-tree.ts`)
made two assumptions that don't hold on modern RN:

1. **Hardcoded renderer id.** `hook.getFiberRoots(1)` assumes the React
Native renderer is DevTools renderer id `1`. Libraries that ship their
own React reconciler (`react-native-skia`, `react-native-svg`,
`react-three-fiber`, …) register a renderer too and frequently take id
`1`. On such apps `getFiberRoots(1)` returns only that library's roots
(e.g. Skia shapes), so the walk never reaches the app UI → `No visible
components`.

2. **`nativeFabricUIManager.measure` under bridgeless.** Layout was read
via `nativeFabricUIManager.measure(stateNode.node, …)`. Under
bridgeless, `globalThis.nativeFabricUIManager` is an **empty object**
(no `.measure`), so every measure threw and all rects became `null` —
components were then filtered out (and the few kept by testID reported
no usable rect).

## Fix

- **Renderer-agnostic root selection:** enumerate `hook.renderers`,
collect roots from every renderer, and walk the mounted root with the
largest fiber subtree (the real app UI) instead of hardcoding id 1.
- **Fabric layout via the public host instance:** measure through
`stateNode.canonical.publicInstance.measureInWindow()` (a
`ReactNativeElement`) — the **same approach this repo's
`inspect-at-point.ts` already uses** — falling back to
`nativeFabricUIManager.measure(shadowNode, …)` when that global is
functional (non-bridgeless).
- **Hang guard:** a per-measure 1200 ms timeout so `Promise.all` can't
stall on a measure callback that never fires (detached / off-screen
nodes).

No change to the output schema or the Paper (old-architecture) path.

## How to reproduce

On a Fabric + bridgeless RN app that also renders `react-native-skia`:

```
argent run debugger-connect       --device_id <udid> --port <metro>
argent run debugger-component-tree --device_id <udid> --port <metro>
# before: "No visible components found on screen."
```

The DevTools hook reports multiple renderers — e.g. `getFiberRoots(1)`
is the Skia reconciler (only canvas nodes), while the real app UI lives
in a different react-native renderer that the hardcoded id never
reaches.

## Testing

- Full `npm run build` (`tsc --build`, project references) passes; the
modified file typechecks under `strict`.
- Verified live against a **React 19.2.0 / Fabric (bridgeless)** app
whose DevTools hook exposes 3 renderers (id 1 = `react-native-skia`
`0.0.1`; id 3 = `react-native` `19.2.0` holding the real ~2,900-fiber UI
tree).

Before (unpatched): `No visible components found on screen.` — even with
`--includeSkipped` and after forcing a fresh commit.

After (this change) — names + testIDs + accurate coords (example
genericized):

```
Screen: 440x956
OnboardingQuestion "<question text>"            (tap: 0.50,0.17)
  SelectableCard [testID=onboarding.option.yes]  (tap: 0.50,0.84)
    Text "Yes"                                    (tap: 0.50,0.84)
  SelectableCard [testID=onboarding.option.no]    (tap: 0.50,0.93)
Pressable [testID=screen.back-button]             (tap: 0.09,0.09)   ← top-left ✓
TouchableOpacity [testID=dev-settings.trigger]    (tap: 0.06,0.97)   ← bottom-left ✓
```

Coordinates were spot-checked against the visible layout (back button
top-left, an off-screen/hidden trigger bottom-left).

## Notes

`inspect-at-point.ts` has the same hardcoded `getFiberRoots(1)` /
`renderers[0]` renderer assumption (its Fabric layout path already uses
`publicInstance`, which is what motivated this fix). I scoped this PR to
`component-tree`; happy to follow up on the inspect path separately.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Filip131311 <contact@filipkaminski.com>
Co-authored-by: filip131311 <159789821+filip131311@users.noreply.github.com>
2026-06-10 12:34:47 +02:00
Filip131311 9267940391 style: prettier formatting in inspect-at-point-script test 2026-06-10 12:32:36 +02:00
Alexander Brokking d2071fafc5 fix(tool-server): select the correct renderer in debugger-inspect-element (#317)
## Summary

Companion to #316. `debugger-inspect-element` has the same
hardcoded-renderer assumption as the component-tree script, so it
inspects the wrong React renderer on multi-renderer apps.

## Root cause

`makeInspectScript`
(`packages/tool-server/src/utils/debugger/scripts/inspect-at-point.ts`)
picks:

```js
var renderer = Array.from(hook.renderers.values())[0];
var roots = hook.getFiberRoots(1);
var root = Array.from(roots)[0];
```

On React Native **New Architecture** apps that also use a secondary
React reconciler (`react-native-skia`, `react-native-svg`,
`react-three-fiber`, …), that library registers its own renderer and
frequently takes id `1`. `renderer` then points at the Skia/SVG
reconciler and `getFiberRoots(1)` returns only its roots, so
`renderer.rendererConfig.getInspectorDataForViewAtPoint(...)` runs
against the wrong tree and returns no app component at the touch point.

## Fix

Enumerate `hook.renderers`, walk each renderer's roots, and select the
**(renderer, root)** pair with the largest fiber subtree (the real app
UI) before inspecting — then call `getInspectorDataForViewAtPoint` on
that renderer. Falls back to the previous first-renderer behavior if
enumeration yields nothing.

Unlike the component-tree script, the Fabric layout path here was
already correct (it uses
`hostFiber.stateNode.canonical.publicInstance`), so no measurement
change is needed — this PR is purely the renderer/root selection.

## Testing

- Full `npm run build` (`tsc --build`) passes; the modified file
typechecks under `strict`.
- Same environment as #316: a React 19.2 / Fabric (bridgeless) app whose
DevTools hook exposes a `react-native-skia` renderer at id 1 alongside
the real `react-native` UI renderer. With the fix, inspection resolves
against the app renderer instead of the Skia one.

No change to the output schema or the Paper path.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 12:27:52 +02:00
Paweł Fornagiel 42cc5e3c09 Enhance error handling in registry and tool server blueprints (#115)
* feat: enhance error handling in registry and tool server blueprints

* test: add tests for error handling in registry and tool server blueprints

* refactor: streamline exception detail formatting in CDP client

* format: run format
2026-04-13 17:17:18 +02:00
Filip131311 8c2a313c76 chore: format codebase with Prettier
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 14:53:58 +02:00
Paweł Fornagiel 3d1a5dbe08 feat: Enhance component tree filtering in debugger (#35) 2026-03-23 10:56:11 +01:00
Paweł Fornagiel 0071a09d99 feat: add log registry for debugger console logs (#30)
* feat: first log registry implementation

* feat: refactor skills to mention how to use new log registry

* docs: update SKILL.md to include guidance for handling large log files

* refactor: update log file handling, change to .log instead of JSONL
2026-03-18 18:48:49 +01:00
Paweł Fornagiel 2fa515d628 feat: component tree & element inspector improvements (#26)
* feat: include skiplists

* feat: allow for verbose logging in component tree and element-inspect

* feat: trim the skills related to PR from unneeded information
2026-03-17 16:56:14 +01:00