Skip to content

feat(desktop): wire WyStack server+client over Electron IPC (YW-69)#44

Closed
youhaowei wants to merge 4 commits into
mainfrom
claude/crazy-banzai-07aa05
Closed

feat(desktop): wire WyStack server+client over Electron IPC (YW-69)#44
youhaowei wants to merge 4 commits into
mainfrom
claude/crazy-banzai-07aa05

Conversation

@youhaowei

@youhaowei youhaowei commented May 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • Main process: buildWyStackApp() registers a projectInfo query backed by ProjectHandle.meta, then attachElectronTransport({ app, ipcMain }) mounts the WyStack server over the wystack:c2s / wystack:s2c IPC channels. No auth — trusted in-process transport.
  • Preload bridge: window.wysIpc exposes a minimal IpcRendererLike surface scoped to the wystack channels via contextBridge. A WeakMap bridges original callbacks through context isolation so removeListener correlates correctly.
  • Renderer client (apps/renderer/src/wystack.ts): custom WyStackClient over IPC using createElectronPipe + a hand-rolled call→result correlator (Map<id, {resolve,reject}>). No-op WsManager — non-reactive only (YW-62 gates subscriptions). useProjectInfo() convenience hook using useQuery.
  • Renderer entry (main.tsx): wraps app in QueryClientProvider + WyStackProvider.
  • Index route: calls useProjectInfo() and renders project name/version from the WyStack response.
  • Build system: desktop build:main now bundles @wystack/* and transitive deps (drizzle-orm, zod, hono) inline via esbuild, keeping only electron, @dashframe/*, @electric-sql/pglite, and postgres as externals. turbo.json per-package task overrides prevent turbo from re-running wystack builds through the dependency chain. New scripts/build-wystack.mjs + bun build:wystack builds the wystack package dists (excluding test files and serve-bun.ts).

Implementation note: The call→result correlator over IPC is hand-rolled in the renderer because @wystack/client has no built-in IPC call transport — the client engine only owns the reactive (subscribe/invalidate) tier. A @wystack/client-side IPC call transport should land as follow-up work.

Runtime evidence

Electron main process launched headless; full log:

[dashframe] main process started, waiting for app ready...
[dashframe] app ready, opening project...
[dashframe] project ready at /Users/youhaowei/.DashFrame/default-project
[dashframe] WyStack IPC transport mounted
[dashframe] creating window with DEV_URL=http://localhost:5173...
[dashframe] window created
(node:51161) electron: Failed to load URL: http://localhost:5173/ with error: ERR_CONNECTION_REFUSED

The ERR_CONNECTION_REFUSED is expected — no Vite dev server was running in the headless test environment. The WyStack transport mounts and the window opens successfully. Full app-launch verification with the renderer showing project info requires bun run dev:desktop with a display.

bun check result

Tasks:    59 successful, 59 total
Cached:    56 cached, 59 total

All 59 tasks pass.

Test plan

  • bun run dev:desktop — app launches, renderer shows project name/version in the index route
  • Chrome DevTools Network tab (CDP): wystack:c2s send visible on connect; wystack:s2c carries {type:"result",id:"call_1",data:{name:...,version:...}}
  • Console: no errors; [dashframe] WyStack IPC transport mounted visible in main process logs

Closes YW-69

🤖 Generated with Claude Code

Greptile Summary

This PR wires WyStack (the project's custom query engine) over Electron IPC, replacing the previous static "hello" screen with a live projectInfo query from the main process. The transport is three-layered: main process registers a WyStack server over wystack:c2s/wystack:s2c IPC channels, the preload bridges them through contextBridge, and the renderer runs a hand-rolled call/result correlator wrapped in a WyStackClient-compatible object.

  • Main + preload: buildWyStackApp serves projectInfo from ProjectHandle.meta in-memory; the preload wysIpc bridge uses a channel allowlist and a WeakMap to correlate removeListener calls correctly across context isolation.
  • Renderer client (wystack.ts): custom WyStackClient over IPC with a per-call correlator (Map<id, {resolve,reject}>); no-op WsManager stubs subscriptions until YW-62; useProjectInfo hook surfaced via TanStack Query.
  • Build system: build:main now bundles @wystack/* inline; a new scripts/build-wystack.mjs pre-builds wystack submodule dists excluding test files; Turbo overrides prevent re-running wystack builds through the dependency chain.

Confidence Score: 4/5

Safe to merge for the current smoke-test scope; the IPC wiring is correct and the contextBridge allowlist is properly scoped, but two areas around lifecycle cleanup and the build script are worth addressing before the codebase grows.

The IPC protocol is straightforward and the preload bridge is well-guarded. The main concerns are: detach() being called twice in the quit sequence (both before-quit handlers fire on the first quit attempt, and again on the second), the hand-rolled correlator having no timeout or send-failure cleanup path, and the build-wystack.mjs script silently skipping stale dist/ directories and using unquoted paths that would break on space-containing repo roots. None of these cause visible breakage in the happy path, but they are latent issues that could surface during development or on certain developer machines.

apps/desktop/src/main.ts (quit-sequence detach call ordering), apps/renderer/src/wystack.ts (correlator cleanup), and scripts/build-wystack.mjs (stale-dist skip + unquoted paths).

Important Files Changed

Filename Overview
apps/desktop/src/main.ts Adds WyStack IPC transport mount and buildWyStackApp; the detach() registration on before-quit fires twice during the quit sequence, and the projectInfo data shape is duplicated from registerIpc.
apps/desktop/src/preload.ts Adds window.wysIpc bridge via contextBridge; channel allowlist and WeakMap-based listener correlation are well-implemented. The null-event wrapper (listener(null, ...rest)) is correctly documented.
apps/renderer/src/wystack.ts New module wiring WyStack IPC client; hand-rolled correlator has no timeout or error cleanup on send failure, leaving pending entries that can never settle.
apps/renderer/src/main.tsx Wraps the app in QueryClientProvider + WyStackProvider; straightforward provider composition, no issues.
apps/renderer/src/routes/index.tsx Adds useProjectInfo() to render project name/version; loading/error states are handled correctly.
scripts/build-wystack.mjs New build script for wystack submodule dists; unquoted path variables in execSync break on space-containing paths, and the dist existence check silently skips stale builds.
turbo.json Adds per-package task overrides to prevent turbo from re-running wystack builds and to break circular typecheck dependencies; appears correct.
apps/desktop/package.json Switches build:main from --packages=external to explicit externals so @wystack/* and transitive deps are bundled; adds @wystack/server as a workspace dependency.

Sequence Diagram

sequenceDiagram
    participant R as Renderer (wystack.ts)
    participant PB as Preload Bridge (wysIpc)
    participant IM as ipcMain (Electron)
    participant WS as WyStack Server
    participant PH as ProjectHandle.meta

    R->>PB: "wysIpc.send("wystack:c2s", {type:"call", id:"call_1", path:"projectInfo"})"
    PB->>IM: ipcRenderer.send("wystack:c2s", msg)
    IM->>WS: attachElectronTransport routes call frame
    WS->>PH: "handler({ db: null }, args)"
    PH-->>WS: "{ projectId, name, version, ... }"
    WS-->>IM: "{type:"result", id:"call_1", data:{...}}"
    IM-->>PB: ipcRenderer "wystack:s2c" event
    PB-->>R: "listener(null, {type:"result", id:"call_1", data:{...}})"
    R->>R: pending.get("call_1").resolve(data)
    R->>R: useProjectInfo() renders project name/version
Loading

Fix All in Claude Code Fix All in Codex Fix All in Cursor

Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
apps/renderer/src/wystack.ts:98-107
**Pending calls never time out or clean up on send failure**

`ipcCall` stores a `{resolve, reject}` in `pending` before calling `pipe.send`. If the send throws synchronously (e.g., context-bridge serialization error), the Promise constructor catches it and rejects—but the `pending` entry for that `id` is never deleted, so it leaks. More critically, if the IPC response is silently dropped for any reason (renderer reload mid-call, transient IPC error, unexpected frame type), the promise hangs indefinitely and the `pending` entry accumulates. Adding a timeout (e.g., via `AbortSignal` or `setTimeout`+`pending.delete`) and a `try/finally` cleanup around `pipe.send` would make this more robust.

### Issue 2 of 5
apps/desktop/src/main.ts:150-156
**`detach()` is called twice during the quit sequence**

Both `closeProjectBeforeQuit` and `() => detach()` are registered on the same `before-quit` event. On the first quit, both fire: `closeProjectBeforeQuit` calls `event.preventDefault()` then asynchronously closes the project and calls `app.quit()` again; `detach()` runs in that same first-event pass. The second `app.quit()` fires `before-quit` again—`closeProjectBeforeQuit` exits early, and `() => detach()` runs a second time. If `attachElectronTransport`'s `detach` is not idempotent (e.g., it warns or throws when a handler is removed twice), the quit sequence would be noisy or broken. Consider guarding with a `let detached = false` flag, or move the `detach()` call inside `closeProjectBeforeQuit` so it's subject to the same `isClosingProject` guard.

### Issue 3 of 5
apps/desktop/src/main.ts:38-71
**Duplicate `projectInfo` data construction**

`buildWyStackApp` and the existing `registerIpc` both read the same six fields from `handle.meta` and construct an identical shape (`projectId`, `name`, `version`, `schemaVersion`, `createdAt.toISOString()`, `createdBy`). If `ProjectInfo` evolves (a new field added, a field renamed), the two sites will drift. Consider extracting a small helper like `projectInfoFromHandle(handle: ProjectHandle): ProjectInfo` and calling it from both places.

### Issue 4 of 5
scripts/build-wystack.mjs:97-104
Unquoted path variables in `execSync` template literals will break on any machine whose repo root contains spaces (common on macOS home directories like `/Users/John Doe/`). The `tsc` invocation and `rm -f` both interpolate `tmpFile` without quoting.

```suggestion
    execSync(`bun x tsc -p "${tmpFile}"`, { stdio: 'inherit' });
    console.log(`[build-wystack] ${pkg.name}: done`);
  } catch (err) {
    console.error(`[build-wystack] ${pkg.name}: build failed`);
    process.exit(1);
  } finally {
    try { execSync(`rm -f "${tmpFile}"`); } catch {}
  }
```

### Issue 5 of 5
scripts/build-wystack.mjs:74-79
**Stale `dist/` directory skips rebuild silently**

The script checks `existsSync(distDir)` and skips the package entirely if `dist/` is present. A partially-built or outdated `dist` left over from a different commit or a failed prior build will never be replaced unless it is manually deleted. During active development on the wystack submodule this can be confusing: `bun setup` appears to succeed while the renderer actually loads stale code. Consider adding a `--force` flag or comparing a checksum/mtimes against the source files.

Reviews (1): Last reviewed commit: "feat(desktop): wire WyStack server+clien..." | Re-trigger Greptile

Greptile also left 5 inline comments on this PR.

youhaowei and others added 4 commits May 29, 2026 09:26
…e foundation)

DashFrame's wystack pointer was 7 commits behind origin/main, missing the
T2a/T2b/T3a foundation that downstream v0.2 integration work builds on:

- ea4f20f @wystack/transport package + Pipe primitive (T2a / YW-55)
- 235183f server Engine: Session + Dispatch over Pipe (T2b / YW-56)
- 402bb3c client neutral Engine + WebSocket adapter (T3a / YW-58)

This was the actual blocker behind 'waiting on WyStack' — the work had
landed on the remote but was never consumed. Unblocks grooming of T2c
(routes.ts -> Hono adapter) and T3b (client WS adapter).

Verified: bun install clean, @wystack/transport 75/75 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…b WS relocation)

Consumes two merged WyStack v0.2 enablement tickets:
- T2c (YW-57, wystack#28): routes.ts WS path delegates to attachEngine over
  a Hono-socket Pipe; REST + invalidation kept inline. Includes the
  ack-failure -> 4002 regression test from review.
- T3b (YW-59, wystack#29): browser WS adapter relocated to
  packages/client/src/transport/websocket.ts; public surface preserved.

Verified: bun install clean, wystack server+client 130/130 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… T5+T6)

Consumes the merged WyStack Electron IPC transport:
- T5 (YW-67, wystack#33): @wystack/server/electron — attachElectronTransport
  binds ipcMain + webContents into a Pipe driving attachEngine. Lifecycle is
  __connect/__close-driven (model A); no did-finish-load.
- T6 (YW-68, wystack#32): @wystack/client/electron — createElectronPipe wraps
  ipcRenderer as the EnginePipe; __connect on construction signals reload.

This completes the WyStack transport spine. DashFrame can now mount a WyStack
server over IPC (next: T7 integration smoke).

Verified: bun install clean, wystack server+client 180/180 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the v0.2 integration milestone: mounts a WyStack server in the Electron
main process, bridges IPC channels through the preload, and fetches projectInfo in
the renderer via a WyStack IPC query instead of the hand-rolled dashframe:project:info
invoke path.

Key changes:
- main.ts: buildWyStackApp() registers a `projectInfo` query backed by
  ProjectHandle.meta, then calls attachElectronTransport({ app, ipcMain }) to mount
  the WyStack server over the wystack:c2s / wystack:s2c IPC channels. No auth
  (trusted in-process transport).
- preload.ts: exposes window.wysIpc — a minimal IpcRendererLike surface scoped to
  the wystack:c2s/s2c channels via contextBridge. A WeakMap bridges the original
  callback references through the context-isolation boundary so removeListener
  correlates correctly.
- apps/renderer/src/wystack.ts (new): custom WyStackClient over IPC. Uses
  createElectronPipe from @wystack/client/electron for the pipe, a hand-rolled
  call→result correlator (Map<id, {resolve,reject}>), and a no-op WsManager (non-
  reactive slice only — YW-62 gates subscriptions/invalidation). Also exports
  useProjectInfo() — a typed convenience hook wrapping useQuery.
- apps/renderer/src/main.tsx: wraps app in QueryClientProvider + WyStackProvider.
- apps/renderer/src/routes/index.tsx: calls useProjectInfo() and renders the
  project name/version from the WyStack response.
- desktop build:main: changed from --packages=external to explicitly bundle @wystack/*
  and its transitive deps (drizzle-orm, zod, hono) inline. postgres is kept external
  (dynamic import in @wystack/db that the desktop PGLite path never takes).
- turbo.json: per-package task overrides for @dashframe/desktop and @dashframe/renderer
  typecheck/test tasks so turbo does not try to rebuild @wystack/* packages through
  the dependency chain (their dists are managed by bun build:wystack, not turbo).
- scripts/build-wystack.mjs + package.json: new build:wystack script that builds
  @wystack/transport, @wystack/server, and @wystack/client dists, excluding test
  files and the bun-only serve-bun.ts entrypoint.

NOTE: The call→result correlator is hand-rolled in apps/renderer/src/wystack.ts
because @wystack/client has no built-in IPC call transport — the client engine only
owns the reactive tier. A proper @wystack/client IPC call transport should land as
follow-up work.

Runtime evidence: Electron main process starts, opens the project DB at
~/.DashFrame/default-project, prints "[dashframe] WyStack IPC transport mounted",
and creates the window. The only error when launched headless is ERR_CONNECTION_REFUSED
for the Vite URL (expected: no dev server running in the test env).

bun check: 59/59 tasks successful.

Closes YW-69

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@linear-code

linear-code Bot commented May 31, 2026

Copy link
Copy Markdown

YW-69

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Project metadata is now displayed in the desktop application, including project name, version, schema version, and project ID.
    • Implemented inter-process communication framework to support data fetching and real-time updates between the desktop shell and application interface.

Walkthrough

This PR integrates WyStack, a query framework, into DashFrame's Electron app. It adds build tooling for WyStack packages, mounts a WyStack server in the Electron main process that exposes project metadata queries, exposes an IPC bridge in the preload layer, implements a renderer-side IPC-backed client with React Query integration, and updates the home route to fetch and display project info over this new IPC channel.

Changes

WyStack IPC Integration

Layer / File(s) Summary
Build infrastructure and WyStack tooling
libs/wystack, scripts/build-wystack.mjs, package.json, turbo.json
Adds WyStack submodule, build script for pre-compiling @wystack/* packages with TypeScript, and Turbo task config for build outputs and typecheck/test dependencies.
Desktop main process: WyStack server mounting
apps/desktop/package.json, apps/desktop/src/main.ts
Updates desktop build config with explicit externals, adds @wystack/server dependency, defines buildWyStackApp to serve projectInfo queries from project metadata, and attaches WyStack IPC transport to ipcMain with before-quit cleanup.
Preload: IPC bridge exposure to renderer
apps/desktop/src/preload.ts
Exposes wysIpc interface in renderer context via contextBridge with channel filtering (wystack:c2s/wystack:s2c), listener wrapping via WeakMap, and send/on/removeListener methods.
Renderer types and app bootstrap
apps/renderer/src/dashframe-api.d.ts, apps/renderer/package.json, apps/renderer/src/main.tsx
Extends Window type with wysIpc: IpcRendererLike, adds @wystack/client and @tanstack/react-query dependencies, imports QueryClient/QueryClientProvider and WyStackProvider, and wraps app root with both providers.
Renderer: IPC-backed WyStack client
apps/renderer/src/wystack.ts
Implements ipcClient by defining typed query references, call/result correlator with pending promise map keyed by call id, ipcCall helper for promise-based IPC dispatch, and exports useProjectInfo hook for querying project metadata over IPC.
Renderer: UI integration with project data
apps/renderer/src/routes/index.tsx
Updates HelloView route to use useProjectInfo hook for fetching project metadata and renders loading/error/data states with project fields (name, version, schemaVersion, projectId).

Sequence Diagrams

sequenceDiagram
  participant Main as Electron Main
  participant WyStackApp
  participant ipcMain
  participant Renderer
  Main->>WyStackApp: buildWyStackApp(projectHandle)
  WyStackApp-->>Main: WyStackApp configured with projectInfo
  Main->>ipcMain: attachElectronTransport(wyStackApp)
  ipcMain-->>Main: Transport mounted
  Renderer->>ipcMain: wystack:c2s message
  ipcMain->>WyStackApp: route to handler
  WyStackApp-->>ipcMain: result frame
  ipcMain-->>Renderer: wystack:s2c response
Loading
sequenceDiagram
  participant Hook as useProjectInfo
  participant Client as ipcClient
  participant Correlator as Call/Result Map
  participant wysIpc
  Hook->>Client: query(projectInfo)
  Client->>Correlator: new call id, register handlers
  Correlator->>wysIpc: send wystack:c2s with id, path, args
  wysIpc-->>Correlator: wystack:s2c result frame with matching id
  Correlator-->>Client: resolve promise with result
  Client-->>Hook: return typed project data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: wiring a WyStack server and client over Electron IPC in the desktop app, directly matching the PR's core objective.
Description check ✅ Passed The description thoroughly documents the changes across main process, preload, renderer client/integration, build system, and includes runtime evidence and test plans, directly relating to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/desktop/src/main.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

apps/desktop/src/preload.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

apps/renderer/src/dashframe-api.d.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 4 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ac1ca067a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/build-wystack.mjs
Comment on lines +36 to +39
{
name: '@wystack/server',
dir: path.join(WYSTACK, 'packages/server'),
include: ['src'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Build @wystack/db before @wystack/server

On a clean checkout this script builds @wystack/server before any @wystack/db dist exists, but @wystack/server/src/create.ts imports @wystack/db and the package exports resolve types/imports from dist. That makes the new setup prerequisite fail during the server tsc run before the desktop IPC wiring can be built; add/build @wystack/db before @wystack/server or narrow the server build to files that do not import the db package.

Useful? React with 👍 / 👎.

Comment on lines +98 to +107
function ipcCall(
funcPath: string,
args: Record<string, unknown>,
): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = nextCallId();
pending.set(id, { resolve, reject });
pipe.send({ type: "call", id, path: funcPath, args });
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Pending calls never time out or clean up on send failure

ipcCall stores a {resolve, reject} in pending before calling pipe.send. If the send throws synchronously (e.g., context-bridge serialization error), the Promise constructor catches it and rejects—but the pending entry for that id is never deleted, so it leaks. More critically, if the IPC response is silently dropped for any reason (renderer reload mid-call, transient IPC error, unexpected frame type), the promise hangs indefinitely and the pending entry accumulates. Adding a timeout (e.g., via AbortSignal or setTimeout+pending.delete) and a try/finally cleanup around pipe.send would make this more robust.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/renderer/src/wystack.ts
Line: 98-107

Comment:
**Pending calls never time out or clean up on send failure**

`ipcCall` stores a `{resolve, reject}` in `pending` before calling `pipe.send`. If the send throws synchronously (e.g., context-bridge serialization error), the Promise constructor catches it and rejects—but the `pending` entry for that `id` is never deleted, so it leaks. More critically, if the IPC response is silently dropped for any reason (renderer reload mid-call, transient IPC error, unexpected frame type), the promise hangs indefinitely and the `pending` entry accumulates. Adding a timeout (e.g., via `AbortSignal` or `setTimeout`+`pending.delete`) and a `try/finally` cleanup around `pipe.send` would make this more robust.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex Fix in Cursor

Comment thread apps/desktop/src/main.ts
Comment on lines +150 to +156
const { detach } = attachElectronTransport({
app: wyStackApp,
ipcMain,
// No resolveContext — trusted in-process transport, no auth handshake.
});
app.on("before-quit", () => detach());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 detach() is called twice during the quit sequence

Both closeProjectBeforeQuit and () => detach() are registered on the same before-quit event. On the first quit, both fire: closeProjectBeforeQuit calls event.preventDefault() then asynchronously closes the project and calls app.quit() again; detach() runs in that same first-event pass. The second app.quit() fires before-quit again—closeProjectBeforeQuit exits early, and () => detach() runs a second time. If attachElectronTransport's detach is not idempotent (e.g., it warns or throws when a handler is removed twice), the quit sequence would be noisy or broken. Consider guarding with a let detached = false flag, or move the detach() call inside closeProjectBeforeQuit so it's subject to the same isClosingProject guard.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main.ts
Line: 150-156

Comment:
**`detach()` is called twice during the quit sequence**

Both `closeProjectBeforeQuit` and `() => detach()` are registered on the same `before-quit` event. On the first quit, both fire: `closeProjectBeforeQuit` calls `event.preventDefault()` then asynchronously closes the project and calls `app.quit()` again; `detach()` runs in that same first-event pass. The second `app.quit()` fires `before-quit` again—`closeProjectBeforeQuit` exits early, and `() => detach()` runs a second time. If `attachElectronTransport`'s `detach` is not idempotent (e.g., it warns or throws when a handler is removed twice), the quit sequence would be noisy or broken. Consider guarding with a `let detached = false` flag, or move the `detach()` call inside `closeProjectBeforeQuit` so it's subject to the same `isClosingProject` guard.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex Fix in Cursor

Comment thread apps/desktop/src/main.ts
Comment on lines +38 to +71
function buildWyStackApp(handle: ProjectHandle): WyStackApp {
return {
functions: new Map([
[
"projectInfo",
{
type: "query" as const,
path: "projectInfo",
args: {},
handler: async () => ({
projectId: handle.meta.projectId,
name: handle.meta.name,
version: handle.meta.version,
schemaVersion: handle.meta.schemaVersion,
createdAt: handle.meta.createdAt.toISOString(),
createdBy: handle.meta.createdBy,
}),
},
],
]),
subscriptions: createSubscriptionManager(),
async call(funcPath, args) {
const fn = this.functions.get(funcPath);
if (!fn) throw new Error(`Unknown function: ${funcPath}`);
// projectInfo has no args and doesn't use ctx.db — pass a minimal context
const result = await fn.handler({ db: null as never }, args);
return {
result,
tablesRead: new Set<string>(),
tablesWritten: new Set<string>(),
};
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate projectInfo data construction

buildWyStackApp and the existing registerIpc both read the same six fields from handle.meta and construct an identical shape (projectId, name, version, schemaVersion, createdAt.toISOString(), createdBy). If ProjectInfo evolves (a new field added, a field renamed), the two sites will drift. Consider extracting a small helper like projectInfoFromHandle(handle: ProjectHandle): ProjectInfo and calling it from both places.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main.ts
Line: 38-71

Comment:
**Duplicate `projectInfo` data construction**

`buildWyStackApp` and the existing `registerIpc` both read the same six fields from `handle.meta` and construct an identical shape (`projectId`, `name`, `version`, `schemaVersion`, `createdAt.toISOString()`, `createdBy`). If `ProjectInfo` evolves (a new field added, a field renamed), the two sites will drift. Consider extracting a small helper like `projectInfoFromHandle(handle: ProjectHandle): ProjectInfo` and calling it from both places.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex Fix in Cursor

Comment thread scripts/build-wystack.mjs
Comment on lines +97 to +104
execSync(`bun x tsc -p ${tmpFile}`, { stdio: 'inherit' });
console.log(`[build-wystack] ${pkg.name}: done`);
} catch (err) {
console.error(`[build-wystack] ${pkg.name}: build failed`);
process.exit(1);
} finally {
try { execSync(`rm -f ${tmpFile}`); } catch {}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unquoted path variables in execSync template literals will break on any machine whose repo root contains spaces (common on macOS home directories like /Users/John Doe/). The tsc invocation and rm -f both interpolate tmpFile without quoting.

Suggested change
execSync(`bun x tsc -p ${tmpFile}`, { stdio: 'inherit' });
console.log(`[build-wystack] ${pkg.name}: done`);
} catch (err) {
console.error(`[build-wystack] ${pkg.name}: build failed`);
process.exit(1);
} finally {
try { execSync(`rm -f ${tmpFile}`); } catch {}
}
execSync(`bun x tsc -p "${tmpFile}"`, { stdio: 'inherit' });
console.log(`[build-wystack] ${pkg.name}: done`);
} catch (err) {
console.error(`[build-wystack] ${pkg.name}: build failed`);
process.exit(1);
} finally {
try { execSync(`rm -f "${tmpFile}"`); } catch {}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/build-wystack.mjs
Line: 97-104

Comment:
Unquoted path variables in `execSync` template literals will break on any machine whose repo root contains spaces (common on macOS home directories like `/Users/John Doe/`). The `tsc` invocation and `rm -f` both interpolate `tmpFile` without quoting.

```suggestion
    execSync(`bun x tsc -p "${tmpFile}"`, { stdio: 'inherit' });
    console.log(`[build-wystack] ${pkg.name}: done`);
  } catch (err) {
    console.error(`[build-wystack] ${pkg.name}: build failed`);
    process.exit(1);
  } finally {
    try { execSync(`rm -f "${tmpFile}"`); } catch {}
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex Fix in Cursor

Comment thread scripts/build-wystack.mjs
Comment on lines +74 to +79
for (const pkg of packages) {
const distDir = path.join(pkg.dir, 'dist');
if (existsSync(distDir)) {
console.log(`[build-wystack] ${pkg.name}: dist already exists, skipping`);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale dist/ directory skips rebuild silently

The script checks existsSync(distDir) and skips the package entirely if dist/ is present. A partially-built or outdated dist left over from a different commit or a failed prior build will never be replaced unless it is manually deleted. During active development on the wystack submodule this can be confusing: bun setup appears to succeed while the renderer actually loads stale code. Consider adding a --force flag or comparing a checksum/mtimes against the source files.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/build-wystack.mjs
Line: 74-79

Comment:
**Stale `dist/` directory skips rebuild silently**

The script checks `existsSync(distDir)` and skips the package entirely if `dist/` is present. A partially-built or outdated `dist` left over from a different commit or a failed prior build will never be replaced unless it is manually deleted. During active development on the wystack submodule this can be confusing: `bun setup` appears to succeed while the renderer actually loads stale code. Consider adding a `--force` flag or comparing a checksum/mtimes against the source files.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex Fix in Cursor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
package.json (1)

26-50: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Invoke the build script with bun, not node.

Both setup (Line 26) and build:wystack (Line 50) shell out to node, but the repo standardizes on Bun as the runtime (packageManager: bun@1.3.5). Using bun keeps tooling consistent and avoids relying on a separate Node install.

♻️ Proposed fix
-    "setup": "git submodule update --init --recursive && bun install && node scripts/build-wystack.mjs",
+    "setup": "git submodule update --init --recursive && bun install && bun scripts/build-wystack.mjs",
@@
-    "build:wystack": "node scripts/build-wystack.mjs"
+    "build:wystack": "bun scripts/build-wystack.mjs"

As per coding guidelines: "Use bun instead of npm, yarn, or pnpm for package management and runtime".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 26 - 50, The package scripts "setup" and
"build:wystack" currently invoke node to run scripts (node
scripts/build-wystack.mjs); change both to use bun (bun
scripts/build-wystack.mjs) so the repo consistently uses Bun as the runtime per
packageManager and guidelines; update the "setup" and "build:wystack" script
entries to shell out to bun instead of node.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main.ts`:
- Around line 47-54: The project metadata object returned in the IPC handler
(the async handler that builds { projectId, name, version, schemaVersion,
createdAt, createdBy } from handle.meta) is duplicated in registerIpc; extract a
single helper function (e.g., buildProjectInfo or getProjectInfo) that accepts
the source meta (handle.meta or equivalent) and returns the unified shape
(ensure createdAt is converted with toISOString()); replace the inline object in
both the handler and the registerIpc usage to call that helper so both code
paths share the same implementation.

In `@apps/renderer/src/routes/index.tsx`:
- Around line 16-39: The page currently uses raw HTML tags (main, h1, p,
section, strong, small) in the routes component; replace them with the
stdui/@dashframe/ui equivalents (e.g., Container/Layout, Typography/Heading,
Text/Paragraph, Section/Card, Strong/Emphasis, and Caption/Small) by first
adding any missing components to the stdui or `@dashframe/ui` package and then
refactoring the JSX in this routes module to import and use those components
(look for the component rendering block that references isLoading/isError/data
and replace each raw element with the corresponding stdui/@dashframe/ui
component while preserving props and conditional logic).
- Line 19: Replace the inline style on the error paragraph so it uses the
semantic token class instead of hard-coded color: find the JSX that renders the
error (the expression using isError and error?.message) and remove style={{
color: "red" }} and apply the semantic token class (e.g.
className="text-palette-destructive") to that <p> so the error uses the stdui
semantic color token for destructive text.

In `@apps/renderer/src/wystack.ts`:
- Around line 77-107: ipcCall currently registers pending entries that can leak
and hang forever; update ipcCall to create a timeout (e.g., setTimeout) that
rejects the promise with a descriptive Error after a configurable duration and
removes the pending entry, ensure the timeout is cleared when the promise is
settled in the existing pipe.onMessage handlers (where pending.get(msg.id)
resolves/rejects) and also ensure pending.delete is performed if pipe.send
throws by wrapping pipe.send in try/catch and rejecting+cleaning up there;
reference the ipcCall function, the pending map, nextCallId(), pipe.send, and
the onMessage resolve/reject handling to implement the guaranteed cleanup and
timer lifecycle.

In `@scripts/build-wystack.mjs`:
- Around line 74-79: The current loop in build-wystack.mjs (iterating packages
and using distDir/existing guard) can silently reuse stale dist/, so change the
skip logic to compare file mtimes: for each pkg (pkg.dir), recursively compute
the newest mtime under the source tree (e.g., pkg.dir/src or
pkg.dir/**/*.{ts,js,jsx,tsx}) and compare it to the mtime of distDir (fs.stat).
Only skip when distDir exists AND its mtime is newer than the newest source
mtime; otherwise treat it as stale and rebuild; update any logging to indicate
when dist is skipped vs rebuilt. Ensure you reference the same variables used in
the loop (packages, pkg.dir, distDir) when implementing the check.
- Line 1: This file fails Prettier format checks; run the project's formatter to
fix it (e.g., execute the project's format script: "bun run format" or run
Prettier with --write on scripts/build-wystack.mjs) and commit the reformatted
file so CI's "Lint, Type Check & Format" check passes; ensure the shebang line
(#!/usr/bin/env node) remains intact after formatting.
- Around line 96-104: The finally block currently calls execSync(`rm -f
${tmpFile}`) which is POSIX-only and fails on Windows and the unquoted
${tmpFile} can break on paths with spaces; change that to use the imported fs
API: call fs.unlinkSync(tmpFile) inside the finally try/catch and ignore ENOENT
(or catch and no-op), and also ensure the earlier execSync invocation that runs
tsc includes the tmpFile path quoted (e.g., wrap ${tmpFile} in quotes in the
command string) so paths with spaces are handled safely; reference the
try/catch/finally block, tmpFile variable, and the execSync calls to locate and
update the code.
- Line 21: Replace the platform-unsafe ROOT assignment by using
fileURLToPath(import.meta.url) (import from 'url') to compute ROOT correctly;
replace shell-string execSync calls that build commands with unquoted paths
(e.g., execSync(`bun x tsc -p ${tmpFile}`) and execSync(`rm -f ${tmpFile}`)) by
using child_process.execFileSync or spawnSync with argument arrays so paths with
spaces are handled safely and remove the POSIX-only rm by calling fs.rmSync or
fs.unlinkSync for tmpFile; finally, change the simple “skip if dist/ exists”
logic so it doesn’t silently reuse stale builds (either always rebuild, add a
forced-rebuild flag, or compare timestamps/checksum of inputs before skipping)
and update references to ROOT, tmpFile, and the exec calls accordingly (look for
constants ROOT, variable tmpFile, and the execSync usages).

---

Outside diff comments:
In `@package.json`:
- Around line 26-50: The package scripts "setup" and "build:wystack" currently
invoke node to run scripts (node scripts/build-wystack.mjs); change both to use
bun (bun scripts/build-wystack.mjs) so the repo consistently uses Bun as the
runtime per packageManager and guidelines; update the "setup" and
"build:wystack" script entries to shell out to bun instead of node.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9b1b6796-578f-41eb-84a5-e25a26c6cd78

📥 Commits

Reviewing files that changed from the base of the PR and between 2fb8944 and 1ac1ca0.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • apps/desktop/package.json
  • apps/desktop/src/main.ts
  • apps/desktop/src/preload.ts
  • apps/renderer/package.json
  • apps/renderer/src/dashframe-api.d.ts
  • apps/renderer/src/main.tsx
  • apps/renderer/src/routes/index.tsx
  • apps/renderer/src/wystack.ts
  • libs/wystack
  • package.json
  • scripts/build-wystack.mjs
  • turbo.json

Comment thread apps/desktop/src/main.ts
Comment on lines +47 to +54
handler: async () => ({
projectId: handle.meta.projectId,
name: handle.meta.name,
version: handle.meta.version,
schemaVersion: handle.meta.schemaVersion,
createdAt: handle.meta.createdAt.toISOString(),
createdBy: handle.meta.createdBy,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Extract the projectInfo shape to avoid divergence.

This object is duplicated verbatim in registerIpc (Lines 103-110). Two code paths now expose the same project metadata and must be kept in sync by hand. Extract a single helper.

♻️ Proposed fix
+function toProjectInfo(handle: ProjectHandle) {
+  return {
+    projectId: handle.meta.projectId,
+    name: handle.meta.name,
+    version: handle.meta.version,
+    schemaVersion: handle.meta.schemaVersion,
+    createdAt: handle.meta.createdAt.toISOString(),
+    createdBy: handle.meta.createdBy,
+  };
+}
@@
-          handler: async () => ({
-            projectId: handle.meta.projectId,
-            name: handle.meta.name,
-            version: handle.meta.version,
-            schemaVersion: handle.meta.schemaVersion,
-            createdAt: handle.meta.createdAt.toISOString(),
-            createdBy: handle.meta.createdBy,
-          }),
+          handler: async () => toProjectInfo(handle),
@@
-  ipcMain.handle("dashframe:project:info", () => ({
-    projectId: handle.meta.projectId,
-    name: handle.meta.name,
-    version: handle.meta.version,
-    schemaVersion: handle.meta.schemaVersion,
-    createdAt: handle.meta.createdAt.toISOString(),
-    createdBy: handle.meta.createdBy,
-  }));
+  ipcMain.handle("dashframe:project:info", () => toProjectInfo(handle));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handler: async () => ({
projectId: handle.meta.projectId,
name: handle.meta.name,
version: handle.meta.version,
schemaVersion: handle.meta.schemaVersion,
createdAt: handle.meta.createdAt.toISOString(),
createdBy: handle.meta.createdBy,
}),
function toProjectInfo(handle: ProjectHandle) {
return {
projectId: handle.meta.projectId,
name: handle.meta.name,
version: handle.meta.version,
schemaVersion: handle.meta.schemaVersion,
createdAt: handle.meta.createdAt.toISOString(),
createdBy: handle.meta.createdBy,
};
}
handler: async () => toProjectInfo(handle),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main.ts` around lines 47 - 54, The project metadata object
returned in the IPC handler (the async handler that builds { projectId, name,
version, schemaVersion, createdAt, createdBy } from handle.meta) is duplicated
in registerIpc; extract a single helper function (e.g., buildProjectInfo or
getProjectInfo) that accepts the source meta (handle.meta or equivalent) and
returns the unified shape (ensure createdAt is converted with toISOString());
replace the inline object in both the handler and the registerIpc usage to call
that helper so both code paths share the same implementation.

Comment on lines 16 to 39
<main className="p-8 font-sans">
<h1>DashFrame v0.2</h1>
<p>hello - desktop shell boot</p>
{isLoading && <p>Loading project info…</p>}
{isError && <p style={{ color: "red" }}>Error: {error?.message}</p>}
{data && (
<section>
<p>
<strong>Project:</strong> {data.name}
</p>
<p>
<strong>Version:</strong> {data.version}
</p>
<p>
<strong>Schema version:</strong> {data.schemaVersion}
</p>
<p>
<strong>Project ID:</strong> {data.projectId}
</p>
<p>
<small>Fetched via WyStack IPC query over Electron transport</small>
</p>
</section>
)}
</main>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace raw HTML elements with stdui components.

The page uses raw HTML elements (<main>, <h1>, <p>, <section>, <strong>, <small>) instead of components from @stdui/react or @dashframe/ui. As per coding guidelines, all UI on pages must use stdui or @dashframe/ui components.

Add the necessary typography and layout components to the UI package first, then refactor this page to use them.

As per coding guidelines: "All UI on pages MUST use stdui or @dashframe/ui components; add missing components to stdui or the UI package first"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/routes/index.tsx` around lines 16 - 39, The page currently
uses raw HTML tags (main, h1, p, section, strong, small) in the routes
component; replace them with the stdui/@dashframe/ui equivalents (e.g.,
Container/Layout, Typography/Heading, Text/Paragraph, Section/Card,
Strong/Emphasis, and Caption/Small) by first adding any missing components to
the stdui or `@dashframe/ui` package and then refactoring the JSX in this routes
module to import and use those components (look for the component rendering
block that references isLoading/isError/data and replace each raw element with
the corresponding stdui/@dashframe/ui component while preserving props and
conditional logic).

<h1>DashFrame v0.2</h1>
<p>hello - desktop shell boot</p>
{isLoading && <p>Loading project info…</p>}
{isError && <p style={{ color: "red" }}>Error: {error?.message}</p>}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use semantic color token instead of inline style.

The error message uses an inline style={{ color: "red" }} instead of a semantic token. Replace with a semantic token like text-palette-destructive for theme consistency and accessibility.

🎨 Proposed fix
-      {isError && <p style={{ color: "red" }}>Error: {error?.message}</p>}
+      {isError && <p className="text-palette-destructive">Error: {error?.message}</p>}

As per coding guidelines: "Use semantic tokens from stdui (bg-neutral-bg, text-neutral-fg, bg-palette-primary), not shadcn naming"

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isError && <p style={{ color: "red" }}>Error: {error?.message}</p>}
{isError && <p className="text-palette-destructive">Error: {error?.message}</p>}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/routes/index.tsx` at line 19, Replace the inline style on
the error paragraph so it uses the semantic token class instead of hard-coded
color: find the JSX that renders the error (the expression using isError and
error?.message) and remove style={{ color: "red" }} and apply the semantic token
class (e.g. className="text-palette-destructive") to that <p> so the error uses
the stdui semantic color token for destructive text.

Comment on lines +77 to +107
pipe.onMessage((msg) => {
if (msg.type === "result") {
const p = pending.get(msg.id);
if (p) {
pending.delete(msg.id);
p.resolve(msg.data);
}
} else if (msg.type === "error" && msg.id) {
const p = pending.get(msg.id);
if (p) {
pending.delete(msg.id);
p.reject(new Error(msg.error));
}
}
// authenticated / subscribed / invalidate frames are no-ops in this slice.
});

// ---------------------------------------------------------------------------
// Call helper — sends a `call` frame and awaits the `result` response
// ---------------------------------------------------------------------------

function ipcCall(
funcPath: string,
args: Record<string, unknown>,
): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = nextCallId();
pending.set(id, { resolve, reject });
pipe.send({ type: "call", id, path: funcPath, args });
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pending calls can leak and hang forever — add a timeout and failure cleanup.

ipcCall registers an entry in pending and resolves/rejects it only when a matching result/error frame arrives. There is no timeout and no cleanup if a response never comes (main process crash, dropped frame, malformed id). Consequences:

  • The pending map grows unbounded across the session — a memory leak.
  • The returned promise never settles, so React Query stays stuck in isLoading with no error surfaced to the UI.

Additionally, if pipe.send throws, the executor rejects the promise but the pending entry is left dangling.

🛡️ Proposed fix: timeout + guaranteed cleanup
 function ipcCall(
   funcPath: string,
   args: Record<string, unknown>,
 ): Promise<unknown> {
   return new Promise((resolve, reject) => {
     const id = nextCallId();
-    pending.set(id, { resolve, reject });
-    pipe.send({ type: "call", id, path: funcPath, args });
+    const timer = setTimeout(() => {
+      if (pending.delete(id)) {
+        reject(new Error(`WyStack IPC call '${funcPath}' timed out`));
+      }
+    }, 30_000);
+    pending.set(id, {
+      resolve: (data) => {
+        clearTimeout(timer);
+        resolve(data);
+      },
+      reject: (err) => {
+        clearTimeout(timer);
+        reject(err);
+      },
+    });
+    try {
+      pipe.send({ type: "call", id, path: funcPath, args });
+    } catch (err) {
+      pending.delete(id);
+      clearTimeout(timer);
+      reject(err instanceof Error ? err : new Error(String(err)));
+    }
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pipe.onMessage((msg) => {
if (msg.type === "result") {
const p = pending.get(msg.id);
if (p) {
pending.delete(msg.id);
p.resolve(msg.data);
}
} else if (msg.type === "error" && msg.id) {
const p = pending.get(msg.id);
if (p) {
pending.delete(msg.id);
p.reject(new Error(msg.error));
}
}
// authenticated / subscribed / invalidate frames are no-ops in this slice.
});
// ---------------------------------------------------------------------------
// Call helper — sends a `call` frame and awaits the `result` response
// ---------------------------------------------------------------------------
function ipcCall(
funcPath: string,
args: Record<string, unknown>,
): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = nextCallId();
pending.set(id, { resolve, reject });
pipe.send({ type: "call", id, path: funcPath, args });
});
}
pipe.onMessage((msg) => {
if (msg.type === "result") {
const p = pending.get(msg.id);
if (p) {
pending.delete(msg.id);
p.resolve(msg.data);
}
} else if (msg.type === "error" && msg.id) {
const p = pending.get(msg.id);
if (p) {
pending.delete(msg.id);
p.reject(new Error(msg.error));
}
}
// authenticated / subscribed / invalidate frames are no-ops in this slice.
});
// ---------------------------------------------------------------------------
// Call helper — sends a `call` frame and awaits the `result` response
// ---------------------------------------------------------------------------
function ipcCall(
funcPath: string,
args: Record<string, unknown>,
): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = nextCallId();
const timer = setTimeout(() => {
if (pending.delete(id)) {
reject(new Error(`WyStack IPC call '${funcPath}' timed out`));
}
}, 30_000);
pending.set(id, {
resolve: (data) => {
clearTimeout(timer);
resolve(data);
},
reject: (err) => {
clearTimeout(timer);
reject(err);
},
});
try {
pipe.send({ type: "call", id, path: funcPath, args });
} catch (err) {
pending.delete(id);
clearTimeout(timer);
reject(err instanceof Error ? err : new Error(String(err)));
}
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/wystack.ts` around lines 77 - 107, ipcCall currently
registers pending entries that can leak and hang forever; update ipcCall to
create a timeout (e.g., setTimeout) that rejects the promise with a descriptive
Error after a configurable duration and removes the pending entry, ensure the
timeout is cleared when the promise is settled in the existing pipe.onMessage
handlers (where pending.get(msg.id) resolves/rejects) and also ensure
pending.delete is performed if pipe.send throws by wrapping pipe.send in
try/catch and rejecting+cleaning up there; reference the ipcCall function, the
pending map, nextCallId(), pipe.send, and the onMessage resolve/reject handling
to implement the guaranteed cleanup and timer lifecycle.

Comment thread scripts/build-wystack.mjs
@@ -0,0 +1,107 @@
#!/usr/bin/env node

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run Prettier to clear the failing format check.

CI (Lint, Type Check & Format) reports Prettier --check issues in this file; run bun run format to fix.

🧰 Tools
🪛 GitHub Actions: CI / 1_Lint, Type Check & Format.txt

[warning] 1-1: Prettier --check reported formatting issues. Code style issues found in the above file. Run Prettier with --write to fix.

🪛 GitHub Actions: CI / Lint, Type Check & Format

[warning] 1-1: Prettier --check reported code style issues in this file. Run Prettier with --write to fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-wystack.mjs` at line 1, This file fails Prettier format checks;
run the project's formatter to fix it (e.g., execute the project's format
script: "bun run format" or run Prettier with --write on
scripts/build-wystack.mjs) and commit the reformatted file so CI's "Lint, Type
Check & Format" check passes; ensure the shebang line (#!/usr/bin/env node)
remains intact after formatting.

Comment thread scripts/build-wystack.mjs
import { writeFileSync, mkdirSync } from 'fs';
import path from 'path';

const ROOT = new URL('..', import.meta.url).pathname;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

❓ Verification inconclusive

🌐 Web query:

Node.js fileURLToPath vs URL.pathname Windows file path handling

💡 Result:

In Node.js, URL.pathname does not perform Windows-path conversion; it exposes the URL path component in URL form (leading slashes, forward-slash separators, and any percent-encoding). To get a real Windows filesystem path, use url.fileURLToPath (optionally with { windows: true }). Key Windows behaviors 1) fileURLToPath vs URL.pathname - Example from the Node docs: new URL('file:///C:/path/').pathname is incorrect for file usage (it yields /C:/path/), while fileURLToPath('file:///C:/path/') yields the correct Windows path C:\path\ [1]. - Same idea for UNC: new URL('file://nas/foo.txt').pathname is /foo.txt (not a UNC path), while fileURLToPath('file://nas/foo.txt') yields \nas\foo.txt on Windows [1]. 2) fileURLToPath decodes percent-encoded characters and normalizes - Node’s fileURLToPath “decodes percent-encoded characters … and then normalizes the resulting path” [2]. - Node also rejects encoded slashes/backslashes in the file URL path (encoded forward/back slashes are “correctly rejected”) [2]. - Node warns about security: you should not rely on fileURLToPath alone to prevent directory traversal; validate the returned path before using it for filesystem operations [2]. 3) Explicit Windows/Posix selection - fileURLToPath(url[, options]) supports options.windows: true => treat as Windows rules, false => POSIX rules, undefined => system default [2]. 4) Non-UTF-8 percent-encoded sequences - Node introduced fileURLToPathBuffer because the existing fileURLToPath can fail when the input URL contains percent-encoded bytes that are not valid UTF-8 sequences [3][4]. If you may have legacy encodings (or such bytes), convert to a Buffer instead. 5) If your bug is about pathname with spaces/encoding - A common pitfall is expecting URL.pathname to be decoded/converted for filesystem use. URL.pathname will keep things in URL form (e.g., spaces may remain percent-encoded), whereas fileURLToPath decodes percent encodings as part of the conversion [1][2]. Practical guidance - If you have a file: URL and want a path to pass to fs/path APIs, do: fileURLToPath(urlOrString, { windows: true }) on Windows-targeted paths [2]. - Do not use new URL(...).pathname directly for filesystem operations; it can have the “wrong” leading slash form and won’t yield Windows drive/UNC formats [1]. Note on URL-pathname parsing vs Windows path APIs - Node’s node:path behaves according to the runtime OS, and recommends using path.win32 for Windows-style operations on non-Windows systems [5]. That’s separate from fileURLToPath’s job (converting file: URLs to OS-correct paths).

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the file around the reported lines
ls -la scripts/build-wystack.mjs
nl -ba scripts/build-wystack.mjs | sed -n '1,140p'

# Confirm how ROOT is used downstream
rg -n "ROOT" scripts/build-wystack.mjs

# Check for rm -f / execSync usage and quoting
rg -n "rm -f|execSync\\(" scripts/build-wystack.mjs

# Check package.json for build/run scripts that imply Windows/Electron targeting
if [ -f package.json ]; then
  cat -n package.json | sed -n '1,200p'
  rg -n "\"(start|build|pack|electron|wystack)\"|electron|bun x|node " package.json || true
fi

# If present, check for Prettier config / pipeline hints
ls -ლა .prettierrc* package.json 2>/dev/null || true
find . -maxdepth 3 -type f \( -iname "*prettier*" -o -iname "*.prettierrc*" -o -iname ".prettierrc*" \) | head -n 50 || true

Repository: youhaowei/DashFrame

Length of output: 199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File listing =="
ls -la scripts/build-wystack.mjs

echo "== First 160 lines with numbers (no nl) =="
cat -n scripts/build-wystack.mjs | sed -n '1,160p'

echo "== ROOT usages =="
rg -n "ROOT" scripts/build-wystack.mjs

echo "== rm -f / execSync occurrences =="
rg -n "rm -f|execSync\\(" scripts/build-wystack.mjs

echo "== Check package.json for Electron/Windows hints =="
if [ -f package.json ]; then
  cat -n package.json | sed -n '1,220p'
  rg -n "\"(start|build|pack|electron|wystack)\"" package.json || true
  rg -n "electron|bun x|wystack|tsc" package.json || true
else
  echo "package.json not found"
fi

echo "== Prettier config presence (if any) =="
ls -la .prettierrc* 2>/dev/null || true
ls -la prettier* 2>/dev/null || true
find . -maxdepth 3 -type f \( -iname "*prettier*" -o -iname "*prettierrc*" \) | head -n 50 || true

Repository: youhaowei/DashFrame

Length of output: 8257


new URL(...).pathname is not cross-platform; use fileURLToPath.

const ROOT = new URL('..', import.meta.url).pathname; will produce URL-form paths on Windows (e.g. /C:/... and percent-encoded characters), which can corrupt downstream path.join results.

  • Replace ROOT with fileURLToPath (same intent, correct path semantics).
  • execSync(\bun x tsc -p ${tmpFile}`)andexecSync(`rm -f ${tmpFile}`)build command strings without quoting. If paths contain spaces, they can break; additionallyrm -fis POSIX-only—usefs.rmSync/unlinkSync` instead.
  • The “skip if dist/ exists” logic can leave stale dist when dependencies/inputs change (consider a forced rebuild when appropriate).
♻️ Proposed fix
-import path from 'path';
+import path from 'path';
+import { fileURLToPath } from 'url';

-const ROOT = new URL('..', import.meta.url).pathname;
+const ROOT = fileURLToPath(new URL('..', import.meta.url));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-wystack.mjs` at line 21, Replace the platform-unsafe ROOT
assignment by using fileURLToPath(import.meta.url) (import from 'url') to
compute ROOT correctly; replace shell-string execSync calls that build commands
with unquoted paths (e.g., execSync(`bun x tsc -p ${tmpFile}`) and execSync(`rm
-f ${tmpFile}`)) by using child_process.execFileSync or spawnSync with argument
arrays so paths with spaces are handled safely and remove the POSIX-only rm by
calling fs.rmSync or fs.unlinkSync for tmpFile; finally, change the simple “skip
if dist/ exists” logic so it doesn’t silently reuse stale builds (either always
rebuild, add a forced-rebuild flag, or compare timestamps/checksum of inputs
before skipping) and update references to ROOT, tmpFile, and the exec calls
accordingly (look for constants ROOT, variable tmpFile, and the execSync
usages).

Comment thread scripts/build-wystack.mjs
Comment on lines +74 to +79
for (const pkg of packages) {
const distDir = path.join(pkg.dir, 'dist');
if (existsSync(distDir)) {
console.log(`[build-wystack] ${pkg.name}: dist already exists, skipping`);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale dist/ after a submodule bump will silently be reused.

The skip-if-dist-exists guard never rebuilds. This PR bumps the libs/wystack submodule, so any machine that previously ran setup will keep serving an outdated dist/ and silently mismatch the new sources. Consider keying the skip on source freshness, or documenting that build:wystack requires a manual dist/ clean after submodule updates.

Want me to add a staleness check (compare newest src mtime vs dist) so changed packages rebuild automatically?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-wystack.mjs` around lines 74 - 79, The current loop in
build-wystack.mjs (iterating packages and using distDir/existing guard) can
silently reuse stale dist/, so change the skip logic to compare file mtimes: for
each pkg (pkg.dir), recursively compute the newest mtime under the source tree
(e.g., pkg.dir/src or pkg.dir/**/*.{ts,js,jsx,tsx}) and compare it to the mtime
of distDir (fs.stat). Only skip when distDir exists AND its mtime is newer than
the newest source mtime; otherwise treat it as stale and rebuild; update any
logging to indicate when dist is skipped vs rebuilt. Ensure you reference the
same variables used in the loop (packages, pkg.dir, distDir) when implementing
the check.

Comment thread scripts/build-wystack.mjs
Comment on lines +96 to +104
try {
execSync(`bun x tsc -p ${tmpFile}`, { stdio: 'inherit' });
console.log(`[build-wystack] ${pkg.name}: done`);
} catch (err) {
console.error(`[build-wystack] ${pkg.name}: build failed`);
process.exit(1);
} finally {
try { execSync(`rm -f ${tmpFile}`); } catch {}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

rm -f is POSIX-only; use the already-imported fs API.

rm -f (Line 103) fails on Windows, and unquoted ${tmpFile} interpolation (Lines 97, 103) breaks if the path contains spaces. Since fs is already imported, delete the temp file in-process.

♻️ Proposed fix
-import { writeFileSync, mkdirSync } from 'fs';
+import { writeFileSync, rmSync } from 'fs';
@@
   try {
-    execSync(`bun x tsc -p ${tmpFile}`, { stdio: 'inherit' });
+    execSync(`bun x tsc -p "${tmpFile}"`, { stdio: 'inherit' });
     console.log(`[build-wystack] ${pkg.name}: done`);
   } catch (err) {
     console.error(`[build-wystack] ${pkg.name}: build failed`);
     process.exit(1);
   } finally {
-    try { execSync(`rm -f ${tmpFile}`); } catch {}
+    try { rmSync(tmpFile, { force: true }); } catch {}
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
execSync(`bun x tsc -p ${tmpFile}`, { stdio: 'inherit' });
console.log(`[build-wystack] ${pkg.name}: done`);
} catch (err) {
console.error(`[build-wystack] ${pkg.name}: build failed`);
process.exit(1);
} finally {
try { execSync(`rm -f ${tmpFile}`); } catch {}
}
try {
execSync(`bun x tsc -p "${tmpFile}"`, { stdio: 'inherit' });
console.log(`[build-wystack] ${pkg.name}: done`);
} catch (err) {
console.error(`[build-wystack] ${pkg.name}: build failed`);
process.exit(1);
} finally {
try { rmSync(tmpFile, { force: true }); } catch {}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-wystack.mjs` around lines 96 - 104, The finally block currently
calls execSync(`rm -f ${tmpFile}`) which is POSIX-only and fails on Windows and
the unquoted ${tmpFile} can break on paths with spaces; change that to use the
imported fs API: call fs.unlinkSync(tmpFile) inside the finally try/catch and
ignore ENOENT (or catch and no-op), and also ensure the earlier execSync
invocation that runs tsc includes the tmpFile path quoted (e.g., wrap ${tmpFile}
in quotes in the command string) so paths with spaces are handled safely;
reference the try/catch/finally block, tmpFile variable, and the execSync calls
to locate and update the code.

@youhaowei

Copy link
Copy Markdown
Owner Author

Superseded by #45. YW-69 was reworked from the Electron-IPC transport to the HTTP+WS-loopback model (per the 2026-05-31 Data Path & Transport Deployment decision). #45 is verified end-to-end in the running Electron app.

@youhaowei youhaowei closed this Jun 2, 2026
youhaowei added a commit that referenced this pull request Jun 2, 2026
DashFrame's first integration with WyStack. Replaces the superseded
Electron-IPC attempt (PR #44) with the HTTP+WS-loopback model from the
Data Path & Transport Deployment spec: the renderer is a localhost web
client of a loopback-bound server — the same client + transport the cloud
web client uses.

- apps/server: the dedicated server app gains its first real content — a
  `functions` registry (the tRPC-style API surface) with a `projectInfo`
  query reading project_meta via WyStack's DrizzleTracker, plus a
  deployment-agnostic `createDashframeServer(db)` factory. Exports
  `type Functions` for type-only client consumption.
- apps/desktop main: starts the server on 127.0.0.1:0 before the window,
  exposes the loopback URL over a new `getServerInfo` IPC channel, stops
  it on quit.
- apps/renderer: async bootstrap resolves the URL via IPC, mints the
  WyStack client once at module scope, and a route round-trips
  `projectInfo` into an assertable DOM node.
- Integration test proves the full open-project → serve → HTTP path.

No auth: the loopback token mechanism is an open spec decision, out of
scope for this smoke (single-user trunk treats auth as a no-op).

Two load-bearing seams established here, both called out deliberately:
- Bumps the libs/wystack submodule pointer c2850a5 -> 8a1d4d5 (wystack
  origin/main HEAD; brings client createWyStack + WS engine). Not
  Codex's in-flight #35.
- Adds build:wystack (runs the submodule's own turbo build) wired into
  setup — DashFrame now builds the wystack dist as a prerequisite, since
  this is the first DashFrame code to consume it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
youhaowei added a commit that referenced this pull request Jun 2, 2026
DashFrame's first integration with WyStack. Replaces the superseded
Electron-IPC attempt (PR #44) with the HTTP+WS-loopback model from the
Data Path & Transport Deployment spec: the renderer is a localhost web
client of a loopback-bound server — the same client + transport the cloud
web client uses.

- apps/server: the dedicated server app gains its first real content — a
  `functions` registry (the tRPC-style API surface) with a `projectInfo`
  query reading project_meta via WyStack's DrizzleTracker, plus a
  deployment-agnostic `createDashframeServer(db)` factory. Exports
  `type Functions` for type-only client consumption.
- apps/desktop main: starts the server on 127.0.0.1:0 before the window,
  exposes the loopback URL over a new `getServerInfo` IPC channel, stops
  it on quit.
- apps/renderer: async bootstrap resolves the URL via IPC, mints the
  WyStack client once at module scope, and a route round-trips
  `projectInfo` into an assertable DOM node.
- Integration test proves the full open-project → serve → HTTP path.

No auth: the loopback token mechanism is an open spec decision, out of
scope for this smoke (single-user trunk treats auth as a no-op).

Two load-bearing seams established here, both called out deliberately:
- Bumps the libs/wystack submodule pointer c2850a5 -> 8a1d4d5 (wystack
  origin/main HEAD; brings client createWyStack + WS engine). Not
  Codex's in-flight #35.
- Adds build:wystack (runs the submodule's own turbo build) wired into
  setup — DashFrame now builds the wystack dist as a prerequisite, since
  this is the first DashFrame code to consume it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
youhaowei added a commit that referenced this pull request Jun 2, 2026
DashFrame's first integration with WyStack. Replaces the superseded
Electron-IPC attempt (PR #44) with the HTTP+WS-loopback model from the
Data Path & Transport Deployment spec: the renderer is a localhost web
client of a loopback-bound server — the same client + transport the cloud
web client uses.

- apps/server: the dedicated server app gains its first real content — a
  `functions` registry (the tRPC-style API surface) with a `projectInfo`
  query reading project_meta via WyStack's DrizzleTracker, plus a
  deployment-agnostic `createDashframeServer(db)` factory. Exports
  `type Functions` for type-only client consumption.
- apps/desktop main: starts the server on 127.0.0.1:0 before the window,
  exposes the loopback URL over a new `getServerInfo` IPC channel, stops
  it on quit.
- apps/renderer: async bootstrap resolves the URL via IPC, mints the
  WyStack client once at module scope, and a route round-trips
  `projectInfo` into an assertable DOM node.
- Integration test proves the full open-project → serve → HTTP path.

No auth: the loopback token mechanism is an open spec decision, out of
scope for this smoke (single-user trunk treats auth as a no-op).

Two load-bearing seams established here, both called out deliberately:
- Bumps the libs/wystack submodule pointer c2850a5 -> 8a1d4d5 (wystack
  origin/main HEAD; brings client createWyStack + WS engine). Not
  Codex's in-flight #35.
- Adds build:wystack (runs the submodule's own turbo build) wired into
  setup — DashFrame now builds the wystack dist as a prerequisite, since
  this is the first DashFrame code to consume it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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