Skip to content

Codex web app architecture#8

Closed
t3dotgg wants to merge 15 commits into
mainfrom
cursor/codex-web-app-architecture-f06f
Closed

Codex web app architecture#8
t3dotgg wants to merge 15 commits into
mainfrom
cursor/codex-web-app-architecture-f06f

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Feb 9, 2026

Copy link
Copy Markdown
Member

Overhaul architecture from Electron IPC to a Node.js WebSocket server and browser client for simpler packaging and launch.

This PR replaces the Electron-based architecture with a Node.js WebSocket server (apps/server) that wraps the Codex app-server and serves the React web UI (apps/renderer). The browser client connects to this server via WebSocket, providing a NativeApi implementation. This enables launching the application as a simple npx t3 command, which opens the web interface and automatically connects to the current working directory as a project.


Open in Cursor Open in Web


Open with Devin

Summary by CodeRabbit

  • New Features

    • Web-first architecture: local WebSocket server backend and browser renderer; automatic project bootstrap in web mode.
    • Live WS transport and server welcome/connect messaging for smoother browser startup.
  • Bug Fixes

    • Better runtime detection between Electron and browser; UI adapts accordingly (conditional drag regions, messaging).
  • Documentation

    • Major README and docs updated to reflect server/renderer split and provider protocol.
  • Chores

    • Build/dev scripts and toolchain updated; runtime bun version bumped.

cursoragent and others added 8 commits February 9, 2026 21:42
- Create apps/server with package.json, tsconfig, tsup config
- Copy codexAppServerManager, providerManager, fixPath from desktop (no Electron deps)
- Add WebSocket message protocol schemas (WsRequest, WsResponse, WsPush) to @acme/contracts
- Define WS_METHODS and WS_CHANNELS constants for type-safe routing

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
- wsServer.ts: WebSocket server routes NativeApi methods, serves static files
  - Provider events pushed to all connected clients
  - SPA fallback for client-side routing
  - Server welcome message with cwd/projectName on connect
- index.ts: CLI entry point
  - Auto-detects available port (default 3773)
  - Resolves renderer dist for static serving
  - Opens browser on startup via 'open' package
  - Graceful shutdown on SIGINT/SIGTERM

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
- wsTransport.ts: WebSocket client with request/response, push subscriptions,
  auto-reconnect with exponential backoff
- wsNativeApi.ts: Full NativeApi implementation over WebSocket transport
  - All provider methods routed through WS_METHODS
  - Provider events streamed via WS_CHANNELS.providerEvent
  - Todos/terminal/agent stubbed (not needed for web mode)
  - dialogs.pickFolder returns null (text input fallback in UI)
- session-logic.ts: readNativeApi() now auto-detects environment
  - Prefers Electron preload bridge if available (window.nativeApi)
  - Falls back to WebSocket transport for browser mode
- onServerWelcome() hook for renderer to receive cwd on connect

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
- App.tsx: Add AutoProjectBootstrap that listens for server welcome
  and auto-creates project + thread from server's cwd
- Sidebar: Conditionalize drag region (Electron only), hide Browse button in browser
- ChatView: Conditionalize drag region, adjust padding for browser
- DiffPanel: Same drag region treatment
- index.html: Update title to 'CodeThing'
- env.ts: isElectron detection utility

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
- Root package.json: Add dev, dev:server, dev:web, start scripts
  - dev runs both server and renderer in parallel via turbo
  - VITE_WS_URL injected so renderer connects to server in dev mode
- turbo.json: Add WS env vars to globalEnv
- vite.config.ts: Inject VITE_WS_URL at build time for WS server discovery
- wsTransport.ts: Use VITE_WS_URL env var in dev, fall back to page URL in prod
- Server dev script: Uses tsx for direct TypeScript execution with VITE_DEV_SERVER_URL

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
- Delete apps/desktop/ entirely (replaced by apps/server)
- Remove IPC_CHANNELS constant from contracts (no longer needed)
- Keep NativeApi interface (still the renderer's API contract)
- Keep EDITORS constant (used by both server and renderer)
- Update README.md with new architecture diagram and scripts
- Update AGENTS.md with new package roles and architecture

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
- Tests welcome message on connect with cwd/projectName
- Tests server.getConfig method
- Tests providers.listSessions returns empty array
- Tests error response for unknown methods
- Tests graceful handling of invalid JSON
- All 73 tests pass across 12 test files

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
- wsTransport.ts: Use addEventListener instead of on-function assignments
- wsServer.ts: Replace filter(Boolean).pop() with direct array access
- wsServer.test.ts: Use addEventListener for WebSocket events

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
@cursor

cursor Bot commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown

Walkthrough

The PR shifts the project from an Electron desktop app to a web-first architecture: removes the desktop app, adds a Node.js WebSocket server package, implements a renderer WebSocket transport and fallback NativeApi, updates contracts and build/config, and revises docs and CI accordingly.

Changes

Cohort / File(s) Summary
Electron desktop removal
apps/desktop/package.json, apps/desktop/src/main.ts, apps/desktop/src/preload.ts, apps/desktop/src/ipcHelpers.ts, apps/desktop/src/ipcHelpers.test.ts, apps/desktop/src/processManager.ts, apps/desktop/src/processManager.test.ts, apps/desktop/src/todoStore.ts, apps/desktop/scripts/dev-electron.mjs, apps/desktop/scripts/smoke-test.mjs
Entire Electron desktop package and related scripts, IPC bridge, process manager, todo store, helpers and tests removed.
New server package
apps/server/package.json, apps/server/src/index.ts, apps/server/src/wsServer.ts, apps/server/src/wsServer.test.ts, apps/server/scripts/bundle-client.mjs, apps/server/tsconfig.json, apps/server/tsup.config.ts, apps/server/src/logger.ts
Adds Node.js HTTP+WebSocket server, static asset handling, provider routing, tests, build scripts, logger, and packaging config.
Renderer: WebSocket transport & fallback NativeApi
apps/renderer/src/wsTransport.ts, apps/renderer/src/wsNativeApi.ts, apps/renderer/src/session-logic.ts, apps/renderer/src/env.ts, apps/renderer/vite.config.ts
Implements WsTransport with request/response and pub/sub, WebSocket-backed NativeApi (with welcome replay), env detection for Electron, and Vite config exposing VITE_WS_URL.
Renderer UI changes
apps/renderer/src/App.tsx, apps/renderer/src/components/ChatView.tsx, apps/renderer/src/components/DiffPanel.tsx, apps/renderer/src/components/Sidebar.tsx, apps/renderer/index.html
Adds AutoProjectBootstrap, conditions UI on Electron presence (drag regions/padding), updates messages and title.
Contracts: WebSocket RPC
packages/contracts/src/ws.ts, packages/contracts/src/index.ts, packages/contracts/src/ipc.ts
Adds WS schemas/types and constants (methods/channels/welcome); re-exports ws; removes legacy IPC_CHANNELS.
Workspace & build config
package.json, turbo.json, apps/renderer/vite.config.ts, .github/workflows/ci.yml, .gitignore
Adjusts root scripts (dev:server/dev:web/start), removes smoke-test, updates CI bun-version, modifies turbo outputs/envs, changes Vite server port, and removes apps/*/dist-electron ignore rule.
Docs and README
AGENTS.md (renamed/rewritten to CLAUDE.md), README.md
Rewrites architecture, quickstart, runtime modes and provider protocol to reflect server/renderer WebSocket design and workspace changes.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser/Renderer
    participant NativeApi as NativeApi (ws or preload)
    participant WsTransport as WsTransport
    participant WebSocket as WebSocket
    participant Server as WsServer
    Browser->>NativeApi: create/read API (readNativeApi)
    alt browser fallback
        NativeApi->>WsTransport: createWsNativeApi()
        WsTransport->>WebSocket: new WebSocket(url)
        WebSocket->>Server: open
        Server->>WebSocket: push serverWelcome
        WebSocket->>WsTransport: onmessage (push)
        WsTransport->>Browser: onServerWelcome(payload)
    end
    Browser->>NativeApi: request('providers.startSession', params)
    NativeApi->>WsTransport: request(...)
    WsTransport->>WebSocket: send WsRequest (id)
    WebSocket->>Server: receive request
    Server->>Server: route -> ProviderManager
    Server->>WebSocket: send WsResponse (id, result)
    WebSocket->>WsTransport: onmessage (response)
    WsTransport->>NativeApi: resolve request
    NativeApi->>Browser: return result
    Server->>WebSocket: push providers.event
    WebSocket->>WsTransport: onmessage (push)
    WsTransport->>Browser: dispatch event subscribers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Codex web app architecture' accurately summarizes the main architectural shift from Electron desktop to Node.js WebSocket server with browser-based UI.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/codex-web-app-architecture-f06f

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
apps/server/src/wsServer.ts (1)

206-221: Consider adding runtime validation or stronger typing for request params.

The as never type assertions bypass TypeScript's type checking for request.params. While this works, it means type mismatches between the client and server won't be caught at compile time. If the contracts package already has Zod schemas for these params, consider validating them here for runtime safety.

apps/server/src/wsServer.test.ts (1)

51-67: Add a timeout to prevent tests from hanging indefinitely.

The while (true) loop waits for a response with a matching id, but if the server never responds (e.g., due to a bug or connection drop), this will hang the test forever. Consider adding a timeout.

Suggested fix with timeout
 async function sendRequest(
   ws: WebSocket,
   method: string,
   params?: unknown,
 ): Promise<WsResponse> {
   const id = crypto.randomUUID();
   const message = JSON.stringify({ id, method, ...(params !== undefined ? { params } : {}) });
   ws.send(message);

+  const timeout = 5000;
+  const start = Date.now();
   // Wait for response with matching id
   while (true) {
+    if (Date.now() - start > timeout) {
+      throw new Error(`Timeout waiting for response to ${method}`);
+    }
     const parsed = (await waitForMessage(ws)) as Record<string, unknown>;
     if (parsed.id === id) {
       return parsed as WsResponse;
     }
   }
 }

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

@macroscopeapp

macroscopeapp Bot commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

Replace Electron desktop with a Node.js HTTP/WebSocket server and web UI for the Codex app, adding CLI t3 to serve the React client and RPC channels

Introduce a new server in index.ts that serves static assets or redirects to a dev URL, exposes WebSocket RPC per @acme/contracts/ws, broadcasts provider events, and handles graceful shutdown; add CLI packaging via package.json and build config in tsup.config.ts; add WS protocol types in ws.ts and re-export in index.ts; implement client WebSocket transport and NativeApi in wsTransport.ts and wsNativeApi.ts; update renderer bootstrapping and runtime checks in App.tsx and env.ts; remove Electron preload and related references; update workspace scripts and docs.

📍Where to Start

Start with the server entrypoint and request handling in index.ts and wsServer.ts, then review the client transport in wsTransport.ts and API wrapper in wsNativeApi.ts.


Macroscope summarized 8cc7135.

- wsNativeApi: Cache last welcome payload and replay to late subscribers.
  Fixes race where WS connect + welcome arrives before React effect fires.
- App.tsx: Add bootstrappedRef to prevent duplicate project creation,
  skip bootstrap in Electron mode
- wsTransport: Use standard import.meta.env for VITE_WS_URL access
- CLAUDE.md: Fix header (was incorrectly '# AGENTS.md')

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
this.connect();
}

async request<T = unknown>(method: string, params?: unknown): Promise<T> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

src/wsTransport.ts:36 window.location.port is empty on standard ports (80/443), producing an invalid URL like ws://example.com:. Consider conditionally appending the port only when present.

Suggested change
async request<T = unknown>(method: string, params?: unknown): Promise<T> {
: `ws://${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`);

🚀 Want me to fix this? Reply ex: "fix it for me".

});

// WebSocket server — upgrades from the HTTP server
const wss = new WebSocketServer({ server: httpServer });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High

src/wsServer.ts:118 The WebSocketServer lacks origin validation, enabling Cross-Site WebSocket Hijacking (CSWSH). Any malicious website could connect to ws://localhost and invoke methods like shellOpenInEditor. Consider adding a verifyClient callback that checks the Origin header against an allowlist.

🚀 Want me to fix this? Reply ex: "fix it for me".

}

const url = new URL(req.url ?? "/", `http://localhost:${port}`);
let filePath = path.join(staticDir, url.pathname);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/wsServer.ts:78 Consider decoding url.pathname with decodeURIComponent() before passing to path.join. Currently, percent-encoded characters like %20 are used literally, so files with spaces won't be found.

Suggested change
let filePath = path.join(staticDir, url.pathname);
let filePath = path.join(staticDir, decodeURIComponent(url.pathname));

🚀 Want me to fix this? Reply ex: "fix it for me".

Comment thread apps/server/src/index.ts
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(preferred, () => {
server.close(() => resolve(preferred));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High

src/index.ts:18 If an external client connects to the temporary server, server.close() won't invoke its callback until that connection closes—causing startup to hang. Consider calling server.closeAllConnections() before server.close() to force-close any lingering sockets.

Suggested change
server.close(() => resolve(preferred));
server.closeAllConnections();
server.close(() => resolve(preferred));

🚀 Want me to fix this? Reply ex: "fix it for me".


// If not connected, wait for connection
const waitForOpen = () => {
const check = setInterval(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

src/wsTransport.ts:164 Offline send path: waitForOpen polls at 50ms and may send after a request times out. Suggest queuing messages and flushing on open; if polling remains, use a longer interval and check this.pending for the request before sending.

🚀 Want me to fix this? Reply ex: "fix it for me".

}

const fileExt = path.extname(filePath);
const contentType = MIME_TYPES[fileExt] ?? "application/octet-stream";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/wsServer.ts:103 Consider normalizing fileExt to lowercase before the MIME_TYPES lookup, since path.extname preserves case but the map uses lowercase keys.

🚀 Want me to fix this? Reply ex: "fix it for me".

cursoragent and others added 3 commits February 9, 2026 22:09
Use 'tsx src/index.ts' instead of 'node --import tsx' for simpler dev execution

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
- Remove smoke-test script (was desktop-only)
- Remove dist-electron from turbo outputs and .gitignore
- Rename ELECTRON_RENDERER_PORT → PORT in vite.config.ts
- Clean up turbo globalEnv (remove ELECTRON_RENDERER_PORT, WS_SERVER_PORT)
- Update CI workflow to use bun 1.3.9 (matches package.json engines)

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
- Remove private:true so package can be published to npm
- Add files field to limit published files to dist/
- Move @acme/contracts to devDependencies (bundled by tsup)
- Add @acme/renderer devDependency for turbo build ordering
- Add bundle-client.mjs: copies renderer build into dist/client/
- Update resolveStaticDir to check dist/client/ first (npx scenario),
  then fall back to monorepo layout (../../renderer/dist)
- Full build pipeline: contracts → renderer → server + bundle client

Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
Comment thread apps/server/src/index.ts
const DEFAULT_PORT = 3773;
const cwd = process.cwd();

async function findAvailablePort(preferred: number): Promise<number> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/index.ts:14 This has a TOCTOU race condition—the port may be claimed by another process between close() and createServer. Consider passing the already-bound server to createServer instead of just the port number, or document this as an acceptable risk given the narrow time window.

🚀 Want me to fix this? Reply ex: "fix it for me".

@devin-ai-integration devin-ai-integration 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 7 additional findings.

Open in Devin Review

@t3dotgg t3dotgg marked this pull request as ready for review February 10, 2026 03:42
@t3dotgg

t3dotgg commented Feb 10, 2026

Copy link
Copy Markdown
Member Author

@greptileai review

@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: 7

🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Line 20: Update the CI entry that sets "bun-version: 1.3.9" to ensure the test
matrix actually exercises the changed behaviors: add/modify a workflow step to
run "bun run test && bun run check" under bun-version: 1.3.9 and (optionally)
add a matrix axis or additional job that runs the same commands against the
previous stable version (e.g., 1.1.38) for comparison; explicitly ensure tests
cover code paths using fetch() and new WebSocket() (proxy behavior) and any HTTP
server request parsing/handlers (chunked encoding/request-smuggling scenarios)
so differences are detected before merging.

In `@AGENTS.md`:
- Line 1: The top-level header in the file does not match the filename:
AGENTS.md contains “# CLAUDE.md”; update the header to match the file's intent
or rename the file to CLAUDE.md. Specifically either change the markdown
top-line header from “# CLAUDE.md” to “# AGENTS” (or a more appropriate title)
inside AGENTS.md, or rename the file to CLAUDE.md so the filename and header are
consistent.

In `@apps/renderer/src/App.tsx`:
- Around line 31-92: The onServerWelcome callback registered inside
AutoProjectBootstrap captures stale closures of state.projects, state.threads
and state.activeThreadId; replace those captured accesses with refs that are
kept up-to-date (e.g., projectsRef, threadsRef, activeThreadIdRef) so the
callback reads current values when it runs, keep bootstrappedRef as-is, and
update the effect to reference and maintain these refs (synchronize them inside
a useEffect or on every dispatch) before using them in the onServerWelcome
handler to perform the find() checks and dispatch
ADD_PROJECT/ADD_THREAD/SET_ACTIVE_THREAD actions.

In `@apps/renderer/src/wsTransport.ts`:
- Around line 24-33: The constructor's default URL building can produce a
trailing colon and uses hardcoded "ws://" which breaks on HTTPS; change the
fallback for this.url (used in the constructor and before calling
this.connect()) to compute protocol = window.location.protocol === "https:" ?
"wss:" : "ws:" and use window.location.host (which already includes the port
when present) instead of window.location.hostname and window.location.port, so
the fallback becomes `${protocol}//${window.location.host}` when envUrl is not
provided.

In `@apps/server/src/wsServer.ts`:
- Around line 34-39: Update the ServerOptions interface to include an optional
host?: string with default '127.0.0.1', then use that host when starting the
server: locate the ServerOptions interface and add host, ensure any place that
reads options (e.g., the ws/http server bootstrap using options.port and
options.cwd) sets a default host = '127.0.0.1' during destructuring, and replace
plain httpServer.listen(port) calls with httpServer.listen(port, host) (also
update any other server.listen(...) invocations such as the provider/shell
endpoints) so the server binds to localhost by default while still allowing an
override.
- Line 15: The readyState check incorrectly uses the instance property
client.OPEN (undefined); change the import to bring in the WebSocket class
(default export) and use its static OPEN constant for comparisons. Update the
import to include WebSocket and WebSocketServer (so WebSocket.OPEN is available)
and replace any client.OPEN usage with WebSocket.OPEN in the readyState checks
(e.g., in the send-to-clients loop) so connected clients are detected correctly.

In `@README.md`:
- Around line 11-27: Add a language specifier to the fenced code block
containing the ASCII architecture diagram in README.md (e.g., use ```text or
```plaintext) so the markdown linter MD040 is satisfied; locate the fenced block
that starts with the ASCII box diagram (the Browser/Server/Codex diagram) and
replace the opening ``` with ```text (or ```plaintext) to mark it as plain text.
🧹 Nitpick comments (11)
apps/server/package.json (1)

17-17: Consider adding vitest to devDependencies.

The test script uses vitest run, but vitest is not listed in this package's devDependencies. While it may resolve via workspace hoisting from the root, explicitly declaring it ensures reliable test execution and aligns with the practice of making package dependencies self-contained.

📦 Proposed fix
   "devDependencies": {
     "@acme/contracts": "workspace:*",
     "@acme/renderer": "workspace:*",
     "@types/node": "^22.10.2",
     "@types/ws": "^8.5.13",
     "tsup": "^8.3.5",
     "tsx": "^4.19.0",
-    "typescript": "^5.7.3"
+    "typescript": "^5.7.3",
+    "vitest": "^3.0.0"
   }
package.json (1)

9-11: WebSocket URL is duplicated across scripts.

ws://localhost:3773 appears in both the dev and dev:web scripts. If the default port changes, both locations need updating. Consider extracting this to a single source of truth or documenting the coupling.

💡 Alternative approach using a shared variable

One option is to define the URL in a .env.development file or use a shell variable in a wrapper script. However, for simple cases, this inline approach is acceptable if the team is aware of the coupling.

apps/server/src/wsServer.test.ts (1)

51-67: sendRequest can hang indefinitely if the expected response never arrives.

The while (true) loop at Line 61 waits for a message with a matching id, but if the server crashes, disconnects, or sends malformed responses, this will block forever and cause test timeouts to be uninformative.

Consider adding a timeout or maximum iteration count:

🛡️ Proposed fix with timeout
 async function sendRequest(
   ws: WebSocket,
   method: string,
   params?: unknown,
 ): Promise<WsResponse> {
   const id = crypto.randomUUID();
   const message = JSON.stringify({ id, method, ...(params !== undefined ? { params } : {}) });
   ws.send(message);

+  const deadline = Date.now() + 5000; // 5 second timeout
   // Wait for response with matching id
   while (true) {
+    if (Date.now() > deadline) {
+      throw new Error(`Timed out waiting for response to ${method}`);
+    }
     const parsed = (await waitForMessage(ws)) as Record<string, unknown>;
     if (parsed.id === id) {
       return parsed as WsResponse;
     }
   }
 }
apps/renderer/src/wsNativeApi.ts (1)

10-12: Module-level state without cleanup mechanism.

The singleton pattern uses module-level variables (instance, welcomeListeners, lastWelcome) that persist for the lifetime of the page. While this is intentional for the singleton transport, there's no dispose or reset mechanism. This could cause issues during hot module replacement (HMR) in development or if the app needs to reconnect with fresh state.

Consider exposing a dispose function or ensuring HMR handles the module correctly:

💡 Optional: Add reset capability for HMR
// At module level, after instance declaration:
if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    instance?.transport.dispose();
    instance = null;
    welcomeListeners.clear();
    lastWelcome = null;
  });
}
apps/server/src/index.ts (1)

14-35: Minor TOCTOU race condition in port detection.

The findAvailablePort function closes the test server before returning, creating a window where another process could claim the port. This is a known limitation of port probing and is generally acceptable for development tooling, but worth documenting.

For a more robust approach in future iterations, consider passing the server instance directly to createServer or using httpServer.listen(0) directly in wsServer.ts.

packages/contracts/src/ws.ts (2)

5-26: Optional: align WS constants with camelCase naming.

These exported constants are in SCREAMING_SNAKE_CASE, while repo conventions call for camelCase variable names. Consider renaming to wsMethods/wsChannels (and updating imports) or documenting an exception for exported constants.

Based on learnings: Applies to **/*.{ts,tsx} : Use camelCase for variable names.


30-58: Constrain method/channel values in schemas.

method and channel currently accept any non-empty string, so typos can pass validation. Consider deriving z.enum values from WS_METHODS / WS_CHANNELS to keep the contract in sync.

♻️ Proposed schema tightening
+const wsMethodSchema = z.enum(Object.values(WS_METHODS) as [string, ...string[]]);
+const wsChannelSchema = z.enum(Object.values(WS_CHANNELS) as [string, ...string[]]);
+
 export const wsRequestSchema = z.object({
   id: z.string().min(1),
-  method: z.string().min(1),
+  method: wsMethodSchema,
   params: z.unknown().optional(),
 });
@@
 export const wsPushSchema = z.object({
   type: z.literal("push"),
-  channel: z.string().min(1),
+  channel: wsChannelSchema,
   data: z.unknown(),
 });
apps/server/src/wsServer.ts (2)

1-1: Rename file to kebab-case (ws-server.ts).

Repo conventions call for kebab-case filenames; consider renaming this file and updating imports accordingly.

Based on learnings: Applies to **/*.{ts,tsx} : Use kebab-case for file names.


19-32: Optional: camelCase the MIME types map.

If you want to strictly follow the camelCase variable-name convention, consider renaming MIME_TYPES to mimeTypes and updating its usage.

♻️ Proposed rename
-const MIME_TYPES: Record<string, string> = {
+const mimeTypes: Record<string, string> = {
@@
-      const contentType = MIME_TYPES[fileExt] ?? "application/octet-stream";
+      const contentType = mimeTypes[fileExt] ?? "application/octet-stream";

Based on learnings: Applies to **/*.{ts,tsx} : Use camelCase for variable names.

Also applies to: 102-104

apps/renderer/src/wsTransport.ts (2)

1-1: Rename file to kebab-case (ws-transport.ts).

Repo conventions call for kebab-case filenames; consider renaming and updating imports.

Based on learnings: Applies to **/*.{ts,tsx} : Use kebab-case for file names.


11-12: Optional: camelCase timeout/backoff constants.

If you want to follow the camelCase variable naming rule consistently, consider renaming REQUEST_TIMEOUT_MS and RECONNECT_DELAYS_MS. The request/subscribe/dispose flow otherwise looks good.

♻️ Proposed rename
-const REQUEST_TIMEOUT_MS = 60_000;
-const RECONNECT_DELAYS_MS = [500, 1_000, 2_000, 4_000, 8_000];
+const requestTimeoutMs = 60_000;
+const reconnectDelaysMs = [500, 1_000, 2_000, 4_000, 8_000];
@@
-      }, REQUEST_TIMEOUT_MS);
+      }, requestTimeoutMs);
@@
-      setTimeout(() => clearInterval(check), REQUEST_TIMEOUT_MS);
+      setTimeout(() => clearInterval(check), requestTimeoutMs);
@@
-    const delay =
-      RECONNECT_DELAYS_MS[
-        Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)
-      ] ?? RECONNECT_DELAYS_MS[0]!;
+    const delay =
+      reconnectDelaysMs[
+        Math.min(this.reconnectAttempt, reconnectDelaysMs.length - 1)
+      ] ?? reconnectDelaysMs[0]!;

Based on learnings: Applies to **/*.{ts,tsx} : Use camelCase for variable names.

Also applies to: 36-85, 156-187

Comment thread .github/workflows/ci.yml
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.1.38
bun-version: 1.3.9

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

🧩 Analysis chain

🌐 Web query:

Bun 1.3.9 release changelog breaking changes

💡 Result:

Bun v1.3.9 (published Feb 8, 2026) does not call out any explicit “breaking changes” section in the official changelog. The release is mostly new features, fixes, and perf work. [1]

Possible breaking / behavior-changing items to watch:

  • NO_PROXY is now respected even when you pass an explicit proxy option to fetch() / new WebSocket()—requests that previously went through the proxy may now bypass it (or vice‑versa, depending on your env). [1]
  • Security fix: HTTP server chunked encoding parser issue that could lead to request smuggling (behavior may change for malformed/ambiguous requests). [1]

Not breaking, but notable changelog items:

  • bun run --parallel / --sequential for running multiple scripts (with workspace/filter integration). [1]
  • bun:test mock() / spyOn() implement Symbol.dispose (works with using to auto-restore). [1]
  • --compile now supports ESM bytecode (--bytecode --format=esm). [1]
  • Fix for SIGILL crashes on older ARMv8.0 aarch64 CPUs. [1]

Source

  • [1] Bun v1.3.9 blog changelog (bun.com) (bun.com)

Citations:


🌐 Web query:

Bun version history 1.1.38 to 1.3.9 what changed

💡 Result:

Between Bun v1.1.38 (Nov 29, 2024) and Bun v1.3.9 (Feb 8, 2026), the highlights were:

  • v1.1.38: bugfix release (VSCode terminal/debugger hang, postgres crash, TypeScript minification bug, fetch() rare crash, Bun.FileSystemRouter.reload() fix), reusePort for Bun.listen/node:net, SQLite 3.45 → 3.47, plus other compatibility fixes. [1]
  • v1.2.3: major frontend tooling update—run .html directly as a dev server with hot reload/bundling; built-in routing for Bun.serve() (routes); bun init React template; bun install --analyze; plus Node.js compatibility and Bun.SQL improvements. [2]
  • v1.3.4: URLPattern API + fake timers in bun:test (and other fixes). [3]
  • v1.3.5: Bun.Terminal (PTY) support via Bun.spawn({ terminal: ... }) for truly interactive subprocesses. [4]
  • v1.3.6: Bun.Archive (create/extract tar/tar.gz) + Bun.JSONC.parse() (JSON-with-comments) and related tooling updates. [5]
  • v1.3.8: built-in Bun.markdown (CommonMark-compliant parser) + bun build --metafile-md for Markdown bundle analysis output. [6]
  • v1.3.9: bun run --parallel / --sequential for running multiple scripts; bun:test Symbol.dispose support for mock()/spyOn() (works with using); NO_PROXY honored even with explicit proxy options; --cpu-prof-interval; ESM bytecode support in --compile --bytecode; ARM crash fix on older ARMv8.0 CPUs; plus performance work (Markdown, AbortSignal) and a JavaScriptCore upgrade with multiple JIT/SIMD improvements. [7]

Sources: Bun blog release notes for v1.1.38, v1.2.3, v1.3.4, v1.3.5, v1.3.6, v1.3.8, v1.3.9. [1][2][3][4][5][6][7]


Confirm Bun 1.3.9 is tested against behavior changes before merging.

Bun 1.3.9 was published Feb 8, 2026 and is stable. However, the jump from 1.1.38 introduces two behavior-changing items to watch:

  • NO_PROXY is now respected even with an explicit proxy option to fetch() / new WebSocket()—requests may route differently than before.
  • HTTP chunked encoding parser security fix for request smuggling—malformed/ambiguous requests will behave differently.

Ensure bun run test && bun run check exercise code paths affected by these changes.

🤖 Prompt for AI Agents
In @.github/workflows/ci.yml at line 20, Update the CI entry that sets
"bun-version: 1.3.9" to ensure the test matrix actually exercises the changed
behaviors: add/modify a workflow step to run "bun run test && bun run check"
under bun-version: 1.3.9 and (optionally) add a matrix axis or additional job
that runs the same commands against the previous stable version (e.g., 1.1.38)
for comparison; explicitly ensure tests cover code paths using fetch() and new
WebSocket() (proxy behavior) and any HTTP server request parsing/handlers
(chunked encoding/request-smuggling scenarios) so differences are detected
before merging.

Comment thread AGENTS.md
@@ -1,7 +1,7 @@
# AGENTS.md
# CLAUDE.md

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

Filename and header mismatch.

The file is named AGENTS.md but the header reads # CLAUDE.md. This appears to be either an incomplete rename or a copy-paste issue. Consider renaming the file to CLAUDE.md or updating the header to match the current filename.

🤖 Prompt for AI Agents
In `@AGENTS.md` at line 1, The top-level header in the file does not match the
filename: AGENTS.md contains “# CLAUDE.md”; update the header to match the
file's intent or rename the file to CLAUDE.md. Specifically either change the
markdown top-line header from “# CLAUDE.md” to “# AGENTS” (or a more appropriate
title) inside AGENTS.md, or rename the file to CLAUDE.md so the filename and
header are consistent.

Comment thread apps/renderer/src/App.tsx
Comment on lines +31 to +92
function AutoProjectBootstrap() {
const { state, dispatch } = useStore();
const bootstrappedRef = useRef(false);

useEffect(() => {
// Only relevant in browser mode — Electron doesn't send server welcome
if (isElectron) return;

return onServerWelcome((payload) => {
if (bootstrappedRef.current) return;

// Don't create duplicate projects for the same cwd
const existing = state.projects.find((p) => p.cwd === payload.cwd);
if (existing) {
bootstrappedRef.current = true;
// Ensure a thread is active
const existingThread = state.threads.find(
(t) => t.projectId === existing.id,
);
if (existingThread && !state.activeThreadId) {
dispatch({
type: "SET_ACTIVE_THREAD",
threadId: existingThread.id,
});
}
return;
}

bootstrappedRef.current = true;

// Create project + thread from server cwd
const projectId = crypto.randomUUID();
dispatch({
type: "ADD_PROJECT",
project: {
id: projectId,
name: payload.projectName,
cwd: payload.cwd,
model: DEFAULT_MODEL,
expanded: true,
},
});
dispatch({
type: "ADD_THREAD",
thread: {
id: crypto.randomUUID(),
codexThreadId: null,
projectId,
title: "New thread",
model: DEFAULT_MODEL,
session: null,
messages: [],
events: [],
error: null,
createdAt: new Date().toISOString(),
},
});
});
}, [state.projects, state.threads, state.activeThreadId, dispatch]);

return null;
}

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

Stale closure risk in onServerWelcome callback.

The useEffect captures state.projects, state.threads, and state.activeThreadId at registration time, but the onServerWelcome callback may fire asynchronously (or be replayed from cache) after the state has changed. This could cause the find() checks on Lines 43 and 47-49 to operate on stale data.

Consider using refs to access current state values, or restructure to avoid capturing state in the closure:

♻️ Suggested approach using refs
 function AutoProjectBootstrap() {
   const { state, dispatch } = useStore();
   const bootstrappedRef = useRef(false);
+  const stateRef = useRef(state);
+  stateRef.current = state;

   useEffect(() => {
     // Only relevant in browser mode — Electron doesn't send server welcome
     if (isElectron) return;

     return onServerWelcome((payload) => {
       if (bootstrappedRef.current) return;
+      const currentState = stateRef.current;

       // Don't create duplicate projects for the same cwd
-      const existing = state.projects.find((p) => p.cwd === payload.cwd);
+      const existing = currentState.projects.find((p) => p.cwd === payload.cwd);
       if (existing) {
         bootstrappedRef.current = true;
         // Ensure a thread is active
-        const existingThread = state.threads.find(
+        const existingThread = currentState.threads.find(
           (t) => t.projectId === existing.id,
         );
-        if (existingThread && !state.activeThreadId) {
+        if (existingThread && !currentState.activeThreadId) {
           dispatch({
             type: "SET_ACTIVE_THREAD",
             threadId: existingThread.id,
           });
         }
         return;
       }
       // ... rest unchanged
     });
-  }, [state.projects, state.threads, state.activeThreadId, dispatch]);
+  }, [dispatch]);

   return null;
 }
🤖 Prompt for AI Agents
In `@apps/renderer/src/App.tsx` around lines 31 - 92, The onServerWelcome callback
registered inside AutoProjectBootstrap captures stale closures of
state.projects, state.threads and state.activeThreadId; replace those captured
accesses with refs that are kept up-to-date (e.g., projectsRef, threadsRef,
activeThreadIdRef) so the callback reads current values when it runs, keep
bootstrappedRef as-is, and update the effect to reference and maintain these
refs (synchronize them inside a useEffect or on every dispatch) before using
them in the onServerWelcome handler to perform the find() checks and dispatch
ADD_PROJECT/ADD_THREAD/SET_ACTIVE_THREAD actions.

Comment on lines +24 to +33
constructor(url?: string) {
// In dev mode, VITE_WS_URL points to the server's WebSocket endpoint.
// In production, the page is served by the WS server on the same host:port.
const envUrl = import.meta.env.VITE_WS_URL as string | undefined;
this.url =
url ??
(envUrl && envUrl.length > 0
? envUrl
: `ws://${window.location.hostname}:${window.location.port}`);
this.connect();

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

🧩 Analysis chain

🏁 Script executed:

cat -n apps/renderer/src/wsTransport.ts | sed -n '20,40p'

Repository: pingdotgg/codething-mvp

Length of output: 1034


🌐 Web query:

window.location.port empty default ports 80 443 HTTP HTTPS

💡 Result:

window.location.port is a string that contains the URL’s port only if it’s explicitly non-default. If the URL’s port is missing or is the default port for the scheme, browsers return the empty string "". [1][2]

  • http: / ws: default port → 80location.port === "" [1][3]
  • https: / wss: default port → 443location.port === "" [1][3]

Examples:

  • http://example.com/location.port === "" [2]
  • http://example.com:80/location.port === "" [2]
  • http://example.com:8080/location.port === "8080" [2]

If you need the “effective” port, you must infer it, e.g.:

const port = location.port || (location.protocol === "https:" ? "443" : "80");

Sources: MDN Location.port, MDN URL.port, WHATWG URL Standard default-port table. [1][2][3]


🌐 Web query:

WebSocket wss protocol HTTPS ws protocol HTTP requirements

💡 Result:

  • ws:// vs wss://

    • ws:// is WebSocket over plain TCP (no TLS).
    • wss:// is WebSocket tunneled over TLS (encrypted), analogous to HTTPS. By default, WebSocket uses port 80 for ws and port 443 for wss. The client must complete a TLS handshake (and certificate verification) before the WebSocket handshake when using wss. [1]
  • Relationship to HTTP/HTTPS (what’s actually required)

    • WebSocket is its own protocol; the only HTTP part is the opening handshake, which is an HTTP/1.1 Upgrade request (typically GET ... HTTP/1.1 with Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key, Sec-WebSocket-Version: 13, etc.). After the handshake succeeds (101 Switching Protocols), traffic is no longer HTTP. [1]
  • Browser security requirements (HTTPS page → WSS)

    • If your web app is loaded over HTTPS, browsers generally block insecure subresource loads (mixed content). A ws:// connection from an https:// page is treated as insecure (“mixed content”) and is typically blocked; use wss:// instead. [2]
  • URL schemes you can pass in code

    • The WHATWG WebSockets spec allows ws, wss, and also http/https in the WebSocket(url) constructor; http maps to ws and https maps to wss for establishing the connection. [3]

Practical checklist

  • Serving site over HTTPS ⇒ expose WebSocket endpoint over wss:// with a valid TLS cert. [1][2]
  • Ensure your server/reverse proxy supports HTTP Upgrade (and forwards Upgrade / Connection headers). [1]

Sources: RFC 6455 WebSocket Protocol [1], MDN Mixed Content [2], WHATWG WebSockets Standard [3].


Use wss/ws protocol detection and window.location.host for the default URL.

The current fallback produces an invalid URL on default ports—window.location.port returns an empty string for ports 80 and 443, resulting in ws://hostname: with a trailing colon. Additionally, hardcoded ws:// violates mixed-content policy on HTTPS pages and will be blocked by browsers; HTTPS-served pages must use wss://.

Suggested fix
-    this.url =
-      url ??
-      (envUrl && envUrl.length > 0
-        ? envUrl
-        : `ws://${window.location.hostname}:${window.location.port}`);
+    const fallbackUrl = (() => {
+      const protocol = window.location.protocol === "https:" ? "wss" : "ws";
+      return `${protocol}://${window.location.host}`;
+    })();
+    this.url = url ?? (envUrl && envUrl.length > 0 ? envUrl : fallbackUrl);
📝 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
constructor(url?: string) {
// In dev mode, VITE_WS_URL points to the server's WebSocket endpoint.
// In production, the page is served by the WS server on the same host:port.
const envUrl = import.meta.env.VITE_WS_URL as string | undefined;
this.url =
url ??
(envUrl && envUrl.length > 0
? envUrl
: `ws://${window.location.hostname}:${window.location.port}`);
this.connect();
constructor(url?: string) {
// In dev mode, VITE_WS_URL points to the server's WebSocket endpoint.
// In production, the page is served by the WS server on the same host:port.
const envUrl = import.meta.env.VITE_WS_URL as string | undefined;
const fallbackUrl = (() => {
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
return `${protocol}://${window.location.host}`;
})();
this.url = url ?? (envUrl && envUrl.length > 0 ? envUrl : fallbackUrl);
this.connect();
🤖 Prompt for AI Agents
In `@apps/renderer/src/wsTransport.ts` around lines 24 - 33, The constructor's
default URL building can produce a trailing colon and uses hardcoded "ws://"
which breaks on HTTPS; change the fallback for this.url (used in the constructor
and before calling this.connect()) to compute protocol =
window.location.protocol === "https:" ? "wss:" : "ws:" and use
window.location.host (which already includes the port when present) instead of
window.location.hostname and window.location.port, so the fallback becomes
`${protocol}//${window.location.host}` when envUrl is not provided.

type WsResponse,
wsRequestSchema,
} from "@acme/contracts";
import { WebSocketServer, type WebSocket } from "ws";

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

🧩 Analysis chain

🏁 Script executed:

find . -name "wsServer.ts" -type f | head -20

Repository: pingdotgg/codething-mvp

Length of output: 96


🏁 Script executed:

cat -n ./apps/server/src/wsServer.ts

Repository: pingdotgg/codething-mvp

Length of output: 8834


🌐 Web query:

ws npm WebSocket OPEN constant static property

💡 Result:

In the ws npm package, the ready-state constants (including OPEN) are exposed as static properties on the WebSocket class, so you use them like WebSocket.OPEN (not ws.OPEN). The project’s README uses this pattern when checking client.readyState === WebSocket.OPEN. [1]

They match the standard WebSocket API constants: CONNECTING = 0, OPEN = 1, CLOSING = 2, CLOSED = 3. [2]

import WebSocket, { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", (socket) => {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send("hi");
  }
});

console.log(WebSocket.OPEN); // 1

Sources: [1] [2]


Change import and use WebSocket.OPEN for readyState checks.

OPEN is a static constant on the WebSocket class, not an instance property. The current code at line 55 compares against client.OPEN (undefined), causing the condition to always fail and preventing provider pushes from being sent to connected clients.

Fix required
-import { WebSocketServer, type WebSocket } from "ws";
+import { WebSocketServer, WebSocket } from "ws";

And at line 55:

-      if (client.readyState === client.OPEN) {
+      if (client.readyState === WebSocket.OPEN) {
📝 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
import { WebSocketServer, type WebSocket } from "ws";
import { WebSocketServer, WebSocket } from "ws";
🤖 Prompt for AI Agents
In `@apps/server/src/wsServer.ts` at line 15, The readyState check incorrectly
uses the instance property client.OPEN (undefined); change the import to bring
in the WebSocket class (default export) and use its static OPEN constant for
comparisons. Update the import to include WebSocket and WebSocketServer (so
WebSocket.OPEN is available) and replace any client.OPEN usage with
WebSocket.OPEN in the readyState checks (e.g., in the send-to-clients loop) so
connected clients are detected correctly.

Comment on lines +34 to +39
export interface ServerOptions {
port: number;
cwd: string;
staticDir?: string | undefined;
devUrl?: string | undefined;
}

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

🧩 Analysis chain

🏁 Script executed:

fd -t f "wsServer.ts" --path-separator /

Repository: pingdotgg/codething-mvp

Length of output: 94


🏁 Script executed:

wc -l apps/server/src/wsServer.ts

Repository: pingdotgg/codething-mvp

Length of output: 98


🏁 Script executed:

cat -n apps/server/src/wsServer.ts | head -100

Repository: pingdotgg/codething-mvp

Length of output: 3627


🏁 Script executed:

cat -n apps/server/src/wsServer.ts | sed -n '100,150p'

Repository: pingdotgg/codething-mvp

Length of output: 1738


🏁 Script executed:

cat -n apps/server/src/wsServer.ts | sed -n '220,251p'

Repository: pingdotgg/codething-mvp

Length of output: 843


🏁 Script executed:

cat -n apps/server/src/wsServer.ts | sed -n '34,45p'

Repository: pingdotgg/codething-mvp

Length of output: 479


🏁 Script executed:

# Let me check how httpServer.listen is being called currently and verify the Node.js behavior
cd apps/server && find . -name "*.ts" -o -name "*.js" | grep -E "(index|main|app)" | head -20

Repository: pingdotgg/codething-mvp

Length of output: 81


🏁 Script executed:

cat -n apps/server/src/index.ts

Repository: pingdotgg/codething-mvp

Length of output: 3936


Add a host option to bind the server to localhost by default, preventing unintended LAN exposure.

Currently, httpServer.listen(port) binds to all interfaces (0.0.0.0), exposing shell execution and provider control endpoints to the network. Add an optional host parameter defaulting to 127.0.0.1.

🔒 Suggested localhost binding
 export interface ServerOptions {
   port: number;
+  host?: string;
   cwd: string;
   staticDir?: string | undefined;
   devUrl?: string | undefined;
 }
@@
 export function createServer(options: ServerOptions) {
-  const { port, cwd, staticDir, devUrl } = options;
+  const { port, host = "127.0.0.1", cwd, staticDir, devUrl } = options;
@@
   function start() {
     return new Promise<void>((resolve) => {
-      httpServer.listen(port, () => {
+      httpServer.listen(port, host, () => {
         resolve();
       });
     });
   }

Also applies to lines 42 and 229–233.

📝 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
export interface ServerOptions {
port: number;
cwd: string;
staticDir?: string | undefined;
devUrl?: string | undefined;
}
export interface ServerOptions {
port: number;
host?: string;
cwd: string;
staticDir?: string | undefined;
devUrl?: string | undefined;
}
🤖 Prompt for AI Agents
In `@apps/server/src/wsServer.ts` around lines 34 - 39, Update the ServerOptions
interface to include an optional host?: string with default '127.0.0.1', then
use that host when starting the server: locate the ServerOptions interface and
add host, ensure any place that reads options (e.g., the ws/http server
bootstrap using options.port and options.cwd) sets a default host = '127.0.0.1'
during destructuring, and replace plain httpServer.listen(port) calls with
httpServer.listen(port, host) (also update any other server.listen(...)
invocations such as the provider/shell endpoints) so the server binds to
localhost by default while still allowing an override.

Comment thread README.md
Comment on lines +11 to +27
```
┌─────────────────────────────────┐
│ Browser (React + Vite) │
│ Connected via WebSocket │
└──────────┬──────────────────────┘
│ ws://localhost:3773
┌──────────▼──────────────────────┐
│ apps/server (Node.js) │
│ WebSocket + HTTP static server │
│ ProviderManager │
│ CodexAppServerManager │
└──────────┬──────────────────────┘
│ JSON-RPC over stdio
┌──────────▼──────────────────────┐
│ codex app-server │
└─────────────────────────────────┘
```

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

Add language specifier to fenced code block.

The ASCII architecture diagram lacks a language specifier, which triggers a markdownlint warning (MD040). While text or plaintext would satisfy the linter, some renderers also support an empty language or none.

📝 Suggested fix
-```
+```text
 ┌─────────────────────────────────┐
 │  Browser (React + Vite)         │
📝 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
```
┌─────────────────────────────────┐
│ Browser (React + Vite) │
│ Connected via WebSocket │
└──────────┬──────────────────────┘
│ ws://localhost:3773
┌──────────▼──────────────────────┐
│ apps/server (Node.js) │
│ WebSocket + HTTP static server │
│ ProviderManager │
│ CodexAppServerManager │
└──────────┬──────────────────────┘
│ JSON-RPC over stdio
┌──────────▼──────────────────────┐
│ codex app-server │
└─────────────────────────────────┘
```
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 11-11: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@README.md` around lines 11 - 27, Add a language specifier to the fenced code
block containing the ASCII architecture diagram in README.md (e.g., use ```text
or ```plaintext) so the markdown linter MD040 is satisfied; locate the fenced
block that starts with the ASCII box diagram (the Browser/Server/Codex diagram)
and replace the opening ``` with ```text (or ```plaintext) to mark it as plain
text.

Comment on lines +257 to +261
function start() {
return new Promise<void>((resolve) => {
httpServer.listen(port, () => {
resolve();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High

src/wsServer.ts:257 Consider adding an error listener to httpServer before calling listen, rejecting the promise on failure (e.g., EADDRINUSE). Without it, binding errors crash the process instead of propagating through the returned Promise.

-  function start() {
-    return new Promise<void>((resolve) => {
-      httpServer.listen(port, () => {
-        resolve();
-      });
+  function start() {
+    return new Promise<void>((resolve, reject) => {
+      httpServer.once("error", reject);
+      httpServer.listen(port, () => {
+        httpServer.off("error", reject);
+        resolve();
+      });

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/wsServer.ts around lines 257-261:

Consider adding an `error` listener to `httpServer` before calling `listen`, rejecting the promise on failure (e.g., `EADDRINUSE`). Without it, binding errors crash the process instead of propagating through the returned Promise.

Comment thread apps/server/src/index.ts
Comment on lines +96 to +101
// Graceful shutdown
const shutdown = () => {
logger.info("shutting down");
server.stop();
process.exit(0);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/index.ts:96 Consider awaiting server.stop() (or making it async) and adding a small delay before process.exit(0). The log file streams may not finish flushing before the process terminates, causing data loss.

-  // Graceful shutdown
-  const shutdown = () => {
-    logger.info("shutting down");
-    server.stop();
-    process.exit(0);
-  };
+  // Graceful shutdown
+  const shutdown = () => {
+    logger.info("shutting down");
+    server.stop();
+    // Allow streams to flush before exiting
+    setTimeout(() => process.exit(0), 100);
+  };

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/index.ts around lines 96-101:

Consider awaiting `server.stop()` (or making it async) and adding a small delay before `process.exit(0)`. The log file streams may not finish flushing before the process terminates, causing data loss.

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.

3 participants