Skip to content

feat(tauri): cross-language IPC bridge resolver (#772)#878

Open
vdavid wants to merge 1 commit into
colbymchenry:mainfrom
vdavid:david/tauri-ipc-resolver
Open

feat(tauri): cross-language IPC bridge resolver (#772)#878
vdavid wants to merge 1 commit into
colbymchenry:mainfrom
vdavid:david/tauri-ipc-resolver

Conversation

@vdavid

@vdavid vdavid commented Jun 14, 2026

Copy link
Copy Markdown

Closes #772.

Why

I build Cmdr, a keyboard-first, two-pane file manager in Rust + Tauri 2 + Svelte 5 (source at github.com/vdavid/cmdr), and CodeGraph has become how my agents navigate it. The one place it kept coming up short was the IPC seam: ask "what calls this command?" and you get nothing back, even though the entire frontend calls it. That's exactly where a Tauri app's refactors are scariest ("if I change this command, what in the UI breaks?"), so closing it felt worth doing. Huge fan of what you've built, and your native-bridge resolvers (React Native / Fabric / Swift↔ObjC) made this a pleasure to write: the shape was already there, I just taught it Tauri's dialect.

What

CodeGraph is blind across Tauri's IPC boundary today: callers/impact on a #[tauri::command] come back empty even when the whole frontend calls it, because the call is a runtime IPC hop between two languages whose names don't match (tauri-specta mangles get_mcp_portgetMcpPort).

This PR adds frameworks/tauri.ts, a cross-language bridge in the vein of the React Native / Fabric / Swift↔ObjC resolvers:

  • Commands: commands.fooBar() and raw invoke('foo_bar') → Rust #[tauri::command] fn foo_bar.
  • Events: events.fooBar.listen(...) and raw listen('foo-bar')/once(...) → Rust #[derive(…Event)] struct FooBar.

Handles tauri-specta's full name-conversion set (snake/camel for commands; Pascal/camel/kebab for events), stacked attributes (#[specta::specta] + #[tauri::command]), bare Event derives with use tauri_specta::Event in scope, and #[tauri_specta(event_name = "…")] overrides.

How it resolves

Two TS shapes. Typed commands.*/events.* are member accesses the JS extractor already emits a ref for, joined in resolve() (call-site granularity). Raw invoke('x')/listen('y') carry the wire name as a string literal the extractor drops, so a small extract() surfaces it as a ref attributed to the file node (file granularity, since a fn-node id is a hash of path:kind:name:line a string scan can't rebuild). Like the RN/Fabric bridges, both only redirect JS/TS callers (Rust-side refs use the normal Rust extractor); edges carry metadata.resolvedBy:'framework', confidence 0.7 (commands) / 0.6 (events), so exact name-match wins a tie. I kept these off the provenance:'heuristic' column, which is callback-synthesizer's territory for dispatch with no statically-resolvable name, and Tauri names are statically joinable. Happy to tag them heuristic if you'd rather.

Detection

Any of: tauri.conf.json[5] (root or nested in a monorepo), an @tauri-apps/api dependency, or any tauri::command in tracked Rust.

Validation

I followed what looked like this repo's standard (the dynamic-dispatch playbook: deterministic probes + an agent A/B), across three real apps:

  • Cmdr (tauri-specta, Tauri 2 + Svelte 5, 25k nodes / 102k edges): callers(get_mcp_port) went "no callers" → 4; 291 distinct commands gained framework-tagged TS callers; impact/explore now span the wire; events bridge across the case change.
  • clash-verge-rev (raw invoke): 76 of 79 commands bridge through its cmds.ts wrapper, all spot-checked as real #[tauri::command]s.
  • pot-desktop (raw invoke): 3/3 of its literal invokes resolve.

Precision held: framework edges only hit real commands/events and are cleanly separable from CodeGraph's existing exact-match matching (the generic-name noise like describe/filter is exact-match, not this).

Honest note on the agent A/B: the graph correctness is solid and MCP-verified (codegraph_callers returns the real cross-IPC consumers). But I couldn't show a free-choice read-reduction: Sonnet greps these "who-calls-X" questions by reflex, and Tauri's wire name is the same string on both sides (or paired in the generated bindings.ts), so grep already bridges it. The win shows up under codegraph-only/impact/explore use, not against an agent that's free to grep. So I'm claiming graph correctness, not a read-reduction number.

Limits

  • Raw invoke/listen edges are file-granular (which file uses the command), vs the typed path's call-site granularity.
  • Runtime-built names (invoke(varName), app.emit(var), templated viewer:file-changed:<id>) are skipped: no literal to read.

Test plan

  • 23 tests (__tests__/tauri-ipc-bridge.test.ts): unit for resolve() across commands/events, JS-only redirection, name-conversion + event_name override, and extract() (incl. dynamic-name skipping); plus three end-to-end tests that index a minimal Tauri project through the real CodeGraph pipeline and assert the cross-language edges in SQLite (typed command, typed event, raw invoke). Full suite green (1558 passed, 2 skipped).
  • Real-app validation as above (Cmdr / clash-verge-rev / pot-desktop), indexing each with the patched build and checking the cross-IPC queries via both the CLI and the MCP tool.

Joins Tauri's TS<->Rust IPC boundary that tree-sitter can't see, for both
conventions:

- Typed (tauri-specta): `commands.fooBar()` / `events.fooBar.listen()` are
  member accesses the JS extractor already emits a ref for, joined in `resolve()`
  to the Rust `#[tauri::command] fn foo_bar` / `#[derive(...Event)] struct FooBar`
  (call-site granularity).
- Raw: `invoke('foo_bar')` / `listen('foo-bar')` / `once(...)` carry the wire
  name as a string literal the extractor drops, so `extract()` surfaces it as a
  `calls` ref attributed to the file node (file granularity — a fn-node id is a
  sha256 of path:kind:name:line a string scan can't rebuild).

Across tauri-specta's snake/camel/Pascal/kebab name conversions. Edges marked
`metadata.resolvedBy:'framework'`, confidence 0.7 commands / 0.6 events, exact
name-match wins a tie; over-matching is harmless (resolve() only joins names that
hit a real command/event). Detected via `tauri.conf.json[5]` (root or nested
monorepo), an `@tauri-apps/api` dependency, or any `tauri::command` in tracked
Rust.

Validated on three real apps: Cmdr (tauri-specta, Tauri 2 + Svelte 5, 25k nodes)
-> `callers(get_mcp_port)` "no callers" -> 4, 291 distinct commands gain TS
callers, events bridge across the case change; clash-verge-rev (raw invoke) ->
76 of 79 commands bridge via its cmds.ts wrapper, all spot-checked real commands;
pot-desktop (raw invoke) -> 3/3 literal invokes. `impact` and `explore` span the
wire too. 23 unit + fixture-integration tests (incl. a raw-invoke end-to-end);
full suite green (1558).

- src/resolution/frameworks/tauri.ts        the resolver (resolve + extract)
- src/resolution/frameworks/index.ts        registration
- __tests__/tauri-ipc-bridge.test.ts        unit + end-to-end fixture tests
- CHANGELOG.md, docs/design/dynamic-dispatch-coverage-playbook.md  records
@vdavid

vdavid commented Jun 14, 2026

Copy link
Copy Markdown
Author

@colbymchenry, not sure if you get notified about PRs, so here is a quick ping. If you agree that this is a good contrib, this is meant to be good to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Would you want a Tauri IPC resolver?

1 participant