Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[tools]
node = "24.13.1"
node = "26.2.0"
bun = "1.3.9"
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

## Environment

- Tool versions are pinned in `.mise.toml`: Node `24.13.1`, Bun `1.3.9`.
- Tool versions are pinned in `.mise.toml`: Node `26.2.0`, Bun `1.3.9`.
- If LSP or tooling exits with a mise trust error, report that `.mise.toml` is untrusted instead of changing trust settings automatically.
- Keep committed defaults publishable: no tokens, owner pairing links, private tailnet URLs, or machine-specific service files.

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ brew tap Jay1/jcode https://github.com/Jay1/jcode
brew install --cask jcode
```

#### Download the latest macOS
#### Download the latest macOS

`JCode-<version>-arm64.dmg` or
`JCode-<version>-x64.dmg`, or the latest Linux `JCode-<version>-x64.AppImage`, from
[GitHub Releases](https://github.com/Jay1/jcode/releases).

### Windows

#### Scoop
#### Scoop

If you do not have Scoop yet, follow the [official Scoop installation guide](https://scoop.sh/):

Expand All @@ -42,7 +42,7 @@ scoop bucket add jcode https://github.com/Jay1/scoop-jcode
scoop install jcode
```

#### Winget
#### Winget

```powershell
winget install Jay1.JCode
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/browserUsePipeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,10 @@ export class BrowserUsePipeServer {
private handleSocketConnection(socket: Net.Socket): void {
this.sockets.add(socket);
this.pendingBySocket.set(socket, Buffer.alloc(0));
socket.on("data", (chunk) => this.handleSocketData(socket, chunk));
socket.on("data", (chunk) => {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
this.handleSocketData(socket, buffer);
});
socket.on("close", () => {
this.sockets.delete(socket);
this.pendingBySocket.delete(socket);
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/browserStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { ThreadBrowserState, ThreadId } from "@jcode/contracts";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { getLocalStorage } from "./lib/storage";

const BROWSER_STATE_STORAGE_KEY = "jcode:browser-state:v1";
const BROWSER_HISTORY_LIMIT = 12;
Expand Down Expand Up @@ -137,7 +138,7 @@ export const useBrowserStateStore = create<BrowserStateStore>()(
}),
{
name: BROWSER_STATE_STORAGE_KEY,
storage: createJSONStorage(() => localStorage),
storage: createJSONStorage(() => getLocalStorage()),
partialize: (state) => ({
recentHistoryByThreadId: state.recentHistoryByThreadId,
}),
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/latestProjectStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ProjectId } from "@jcode/contracts";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { getLocalStorage } from "./lib/storage";

const LATEST_PROJECT_STORAGE_KEY = "jcode:latest-project:v1";

Expand Down Expand Up @@ -28,7 +29,7 @@ export const useLatestProjectStore = create<LatestProjectStore>()(
}),
{
name: LATEST_PROJECT_STORAGE_KEY,
storage: createJSONStorage(() => localStorage),
storage: createJSONStorage(() => getLocalStorage()),
},
),
);
7 changes: 7 additions & 0 deletions apps/web/src/lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ export function createMemoryStorage(): StateStorage {
};
}

export function getLocalStorage(): Storage {
if (typeof localStorage === "undefined") {
throw new Error("localStorage is not available");
}
return localStorage;
}
Comment on lines +26 to +31
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

🧩 Analysis chain

🌐 Web query:

In Node.js v26.2.0, can globalThis.localStorage exist outside a browser runtime, and what are the documented conditions/limitations?

💡 Result:

In Node.js v26.2.0, globalThis.localStorage does exist as part of a browser-compatible implementation of the Web Storage API [1][2]. However, its availability and behavior depend on specific runtime conditions and CLI configurations: Conditions and Limitations 1. CLI Flag Requirement: To access localStorage, you must provide the --localstorage-file CLI flag, which specifies the file path where data is stored unencrypted [1][2]. 2. Error Handling: As of Node.js v26.0.0, attempting to access globalThis.localStorage without providing the --localstorage-file flag causes the runtime to throw a DOMException [1][2]. This change was introduced for compliance with the Web Storage specification [1]. 3. Nature of Storage: The data is not stored per-user or per-request; it is shared globally across all users and requests within the process context [1][2]. 4. Capacity: The implementation enforces a maximum storage limit of 10 MB [1][2]. 5. Stability: The API is marked as "Release candidate" (Stability: 1.2) and can be explicitly disabled using the --no-experimental-webstorage flag [1][2]. 6. Usage Note: Modifying the specified storage file outside of the Web Storage API is not supported [1][2]. Because this implementation is intended for specific use cases and has strict requirements, developers often continue to use user-land packages (such as node-localstorage) for legacy compatibility or when a different storage strategy is required [3][4]. Applications relying on localStorage should be aware that its presence in the global scope on Node.js can cause issues for code designed specifically for browser environments that check for typeof localStorage !== "undefined" without verifying the environment [5][6].

Citations:


🌐 Web query:

In zustand v5 (createJSONStorage), what is the behavior when the provided storage getter throws?

💡 Result:

In Zustand v5, the createJSONStorage helper function includes a try-catch block around the provided storage getter [1][2]. If the getter function throws an error, the error is caught, and the function returns undefined [1][2]. This behavior is explicitly designed to handle environments where the intended storage might not be available, such as during server-side rendering (SSR) when attempting to access browser-specific APIs like localStorage or sessionStorage [1][2]. By returning undefined, the persist middleware will fail to initialize the storage, effectively bypassing persistence rather than crashing the application [1][2].

Citations:


Guard getLocalStorage() against Node 26 globalThis.localStorage by requiring window.

typeof localStorage === "undefined" is not a reliable browser check in Node.js 26: globalThis.localStorage can be present depending on runtime configuration, so this helper may return Node’s Web Storage and route zustand persistence unexpectedly. Throwing in non-browser runtimes is fine—zustand v5 createJSONStorage will catch a throwing getter and disable persistence.

🔧 Proposed fix
 export function getLocalStorage(): Storage {
-  if (typeof localStorage === "undefined") {
+  if (
+    typeof window === "undefined" ||
+    typeof window.localStorage === "undefined"
+  ) {
     throw new Error("localStorage is not available");
   }
-  return localStorage;
+  return window.localStorage;
 }
📝 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 function getLocalStorage(): Storage {
if (typeof localStorage === "undefined") {
throw new Error("localStorage is not available");
}
return localStorage;
}
export function getLocalStorage(): Storage {
if (
typeof window === "undefined" ||
typeof window.localStorage === "undefined"
) {
throw new Error("localStorage is not available");
}
return window.localStorage;
}
🤖 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/web/src/lib/storage.ts` around lines 26 - 31, getLocalStorage currently
checks typeof localStorage which can return Node 26's globalThis.localStorage;
update getLocalStorage to require a browser window by checking that typeof
window !== "undefined" and typeof window.localStorage !== "undefined" before
returning window.localStorage, and throw when not present so zustand's
createJSONStorage will disable persistence in non-browser runtimes; reference
the getLocalStorage function and ensure callers (e.g., any use with
createJSONStorage/zustand persistence) keep expecting a throwing getter in
non-browser environments.


export function createDebouncedStorage(
baseStorage: StateStorage,
debounceMs: number = 300,
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/pinnedThreadsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { type ThreadId } from "@jcode/contracts";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { getLocalStorage } from "./lib/storage";

interface PinnedThreadsStoreState {
pinnedThreadIds: ThreadId[];
Expand Down Expand Up @@ -85,7 +86,7 @@ export const usePinnedThreadsStore = create<PinnedThreadsStoreState>()(
}),
{
name: PINNED_THREADS_STORAGE_KEY,
storage: createJSONStorage(() => localStorage),
storage: createJSONStorage(() => getLocalStorage()),
partialize: (state) => ({
pinnedThreadIds: normalizePinnedThreadIds(state.pinnedThreadIds),
}),
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/repoDiffScopeStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { GitReadWorkingTreeDiffInput } from "@jcode/contracts";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { getLocalStorage } from "./lib/storage";

export type RepoDiffScope = NonNullable<GitReadWorkingTreeDiffInput["scope"]>;

Expand Down Expand Up @@ -39,7 +40,7 @@ export const useRepoDiffScopeStore = create<RepoDiffScopeStore>()(
}),
{
name: REPO_DIFF_SCOPE_STORAGE_KEY,
storage: createJSONStorage(() => localStorage),
storage: createJSONStorage(() => getLocalStorage()),
partialize: (state) => ({ scope: state.scope }),
},
),
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/singleChatPanelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { ThreadId, TurnId } from "@jcode/contracts";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { getLocalStorage } from "./lib/storage";
import type { ChatRightPanel } from "./diffRouteSearch";

export interface SingleChatPanelState {
Expand Down Expand Up @@ -81,7 +82,7 @@ export const useSingleChatPanelStore = create<SingleChatPanelStore>()(
}),
{
name: SINGLE_CHAT_PANEL_STORAGE_KEY,
storage: createJSONStorage(() => localStorage),
storage: createJSONStorage(() => getLocalStorage()),
},
),
);
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/splitViewStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { type ProjectId, type ThreadId, type TurnId } from "@jcode/contracts";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { getLocalStorage } from "./lib/storage";

import { type ChatRightPanel } from "./diffRouteSearch";
import { randomUUID } from "./lib/utils";
Expand Down Expand Up @@ -757,7 +758,7 @@ export const useSplitViewStore = create<SplitViewStore>()(
{
name: SPLIT_VIEW_STORAGE_KEY,
version: SPLIT_VIEW_STORAGE_VERSION,
storage: createJSONStorage(() => localStorage),
storage: createJSONStorage(() => getLocalStorage()),
partialize: (state) => ({
splitViewsById: state.splitViewsById,
splitViewIdBySourceThreadId: state.splitViewIdBySourceThreadId,
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/terminalStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { type TerminalActivityState, type TerminalCliKind } from "@jcode/shared/
import type { ThreadId } from "@jcode/contracts";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { getLocalStorage } from "./lib/storage";
import {
DEFAULT_THREAD_TERMINAL_HEIGHT,
DEFAULT_THREAD_TERMINAL_ID,
Expand Down Expand Up @@ -1361,7 +1362,7 @@ export const useTerminalStateStore = create<TerminalStateStoreState>()(
{
name: TERMINAL_STATE_STORAGE_KEY,
version: 1,
storage: createJSONStorage(() => localStorage),
storage: createJSONStorage(() => getLocalStorage()),
partialize: (state) => ({
terminalStateByThreadId: state.terminalStateByThreadId,
}),
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/workspaceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { type ThreadId } from "@jcode/contracts";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { getLocalStorage } from "./lib/storage";
import {
DEFAULT_WORKSPACE_LAYOUT_PRESET_ID,
getWorkspaceLayoutPreset,
Expand Down Expand Up @@ -238,7 +239,7 @@ export const useWorkspaceStore = create<WorkspaceStoreState>()(
{
name: WORKSPACE_STORE_STORAGE_KEY,
version: 2,
storage: createJSONStorage(() => localStorage),
storage: createJSONStorage(() => getLocalStorage()),
partialize: (state) => ({
homeDir: state.homeDir,
workspacePages: state.workspacePages,
Expand Down
Loading
Loading