Skip to content

feat(electron): give the bridged coding agent xNet's workspace tools - #638

Merged
crs48 merged 1 commit into
mainfrom
claude/clawed-electron-prototype-9c4f15
Jul 28, 2026
Merged

feat(electron): give the bridged coding agent xNet's workspace tools#638
crs48 merged 1 commit into
mainfrom
claude/clawed-electron-prototype-9c4f15

Conversation

@crs48

@crs48 crs48 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

What was broken

The desktop app already ran an agent bridge that drives your own claude CLI as the chat model. But the agent had no path to your data.

The MCP wiring sat behind XNET_BRIDGE_MCP + XNET_BRIDGE_MCP_CLI, and the CLI entry pointed at xnet mcp serve — except @xnetjs/cli is not an Electron dependency, so that path could never resolve in a packaged app. Even had it resolved, it passed no token, so the local API would have returned 401. Chat could talk about your workspace but not read or write it.

What this does

Hosts the MCP server in the Electron main process, over the renderer store proxy, and points the agent at it over Streamable HTTP on an ephemeral loopback port. No CLI to spawn, no local-API hop, no per-session token to plumb across processes. Withhold the tools with XNET_BRIDGE_MCP=0.

Also replaces the local API's hardcoded four-schema stub (Schema/Task/Project/Note) with the renderer's real schemaRegistry, served over the same IPC channel that already carries store operations. Anything reading that stub was getting a fiction.

A subtlety worth reviewing

getAllIRIs() is synchronous in SchemaRegistryAPI, but the registry lives an IPC hop away in the renderer — so the proxy caches it. Both subscribers start before createWindow(), so priming must be background-with-retry and must never be awaited at boot; awaiting it deadlocks startup (hit and backed out during development). While unprimed it throws rather than returning [], so "renderer not ready" cannot read as "this workspace has no schemas".

Verification

Verified in the live desktop app over CDP, not just unit-tested.

Bridge status from inside the renderer:

{ running: true, agent: "claude", url: "http://127.0.0.1:31416", workspaceTools: true }

The spawned process, wired as designed:

claude -p "..." --output-format stream-json --include-partial-messages --verbose
  --mcp-config "~/Library/Application Support/xnet-desktop/agent-bridge-mcp.json"
  --allowedTools mcp__xnet__*

A real chat turn through the bridge returned "Schema count: 194 · New page ID: M6_jJ2XVxc5aKIx45SX3V", and the page was really in the store:

{ id: "M6_jJ2XVxc5aKIx45SX3V", schemaId: "xnet://xnet.fyi/Page@1.0.0",
  title: "Agent Smoke Test 0406" }
pageCount: 29 -> 30

194 schemas is the proof the registry fix works — the old stub would have reported 4. The test page was deleted afterwards.

Also confirmed empirically: createMcpHttpServer answers POST with plain application/json (no SSE, no mcp-session-id), and Claude Code 2.1.220 accepts it as a {"type":"http"} MCP server. That was the load-bearing unknown in this design, so it was probed against the real CLI before building on it.

Checks: turbo typecheck clean, 146 electron + 166 devkit tests green (6 new covering the priming logic), lint 0 errors.

Notes

🤖 Generated with Claude Code

The desktop app already ran an agent bridge that drove the user's own
`claude` CLI as the chat model, but the agent had no way to reach the
workspace: the MCP wiring sat behind XNET_BRIDGE_MCP + XNET_BRIDGE_MCP_CLI,
and @xnetjs/cli is not an Electron dependency, so that path could never
resolve in a packaged app. Chat could talk about your data but not read or
write it.

Host the MCP server in the main process instead, over the renderer store
proxy, and point the agent at it over Streamable HTTP. No CLI to spawn, no
local-API hop, no per-session token to plumb across processes. Withhold the
tools with XNET_BRIDGE_MCP=0.

Also replace the local API's hardcoded four-schema stub with the renderer's
real registry, served over the same IPC channel that already carries store
operations. Priming retries in the background because both subscribers start
before createWindow(); until it succeeds getAllIRIs() throws rather than
returning [], so "not ready yet" cannot read as "this workspace has no
schemas".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-638 July 28, 2026 05:44 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

No visual differences detected in the changed UI.

CI run

github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #638.

github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@crs48
crs48 merged commit 90c7a67 into main Jul 28, 2026
21 checks passed
@crs48
crs48 deleted the claude/clawed-electron-prototype-9c4f15 branch July 28, 2026 05:53
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
crs48 added a commit that referenced this pull request Jul 28, 2026
… a face (0406) (#648)

Continues exploration **0406** (13/33 after this) — and closes the loop
this whole arc opened: **#638** gave desktop Claude Code the workspace
tools; this PR finally gives it a UI.

## What this does

Moves the AI chat cluster (`AiChatPanel` + nine `ai-*` modules) into
`@xnetjs/workbench` under a new subpath export — `@xnetjs/workbench/ai`,
kept off the core barrel so hosts opt into the brain/WebLLM weight — and
mounts it in the desktop shell: an `assistant` `ShellState` kind, a
system-menu entry, and an **Open Assistant** palette command, following
the settings/stories pattern exactly.

The move was cheap because the cluster was already app-clean (every
dependency an `@xnetjs` package) and the panel was written host-aware
from the start: it probes `window.xnetAgentBridge` and auto-pairs over
IPC. It had simply never been mounted on the one surface that has the
bridge.

## Verified in the running desktop app

- **Zero-setup pairing**: bridge tier auto-selected, "Running claude ·
v3.0.0", pairing code auto-filled over IPC.
- **The tools arrive**: asked to create a page, the agent declined,
*naming `xnet_create_page` as callable* but honoring the read-only tier
— 0391's consent posture holding exactly as designed.
- **Real reads**: "how many pages titled 'Untitled Page'" → a count plus
a genuine node id, verified against the store (`exampleIdReal: true`;
the count was one off exact-title equality, consistent with fuzzy search
semantics).

Approval-gated writes from the panel remain the explicit next step (0394
phase 2) — the panel's own header comment says so.

## Checks

992 web + 89 workbench + 149 electron tests green; repo-wide typecheck
clean; changelog fragment included (user-visible feature).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

1 participant