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 apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default defineConfig({
plugins: [
tsconfigPaths,
externalizeDepsPlugin({
exclude: ["@superset/*"],
exclude: ["@superset/*", "electron-store"],
}),
],

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"electron-router-dom": "^2.1.0",
"electron-store": "^11.0.2",
"fast-glob": "^3.3.3",
"framer-motion": "^12.23.24",
"http-proxy": "^1.18.1",
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from "node:path";
import { app } from "electron";
import { makeAppSetup } from "lib/electron-app/factories/app/setup";
import { registerStorageHandlers } from "./lib/storage-ipcs";
import { MainWindow } from "./windows/main";

// Protocol scheme for deep linking
Expand All @@ -23,6 +24,9 @@ app.on("open-url", (event, url) => {
event.preventDefault();
});

// Register storage IPC handlers
registerStorageHandlers();

// Allow multiple instances - removed single instance lock
(async () => {
await app.whenReady();
Expand Down
23 changes: 23 additions & 0 deletions apps/desktop/src/main/lib/storage-ipcs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ipcMain } from "electron";
import { store } from "./storage-manager";

/**
* Register storage IPC handlers
* These handlers provide access to electron-store from the renderer process
*/
export function registerStorageHandlers() {
ipcMain.handle("storage:get", async (_event, input: { key: string }) => {
return store.get(input.key);
});

ipcMain.handle(
"storage:set",
async (_event, input: { key: string; value: any }) => {
store.set(input.key, input.value);
},
);

ipcMain.handle("storage:delete", async (_event, input: { key: string }) => {
store.delete(input.key);
});
}
Comment on lines +8 to +23
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.

⚠️ Potential issue | 🟠 Major

Add error handling and input validation.

The IPC handlers lack:

  1. Error handling for store operations (could throw)
  2. Input validation (empty/invalid keys)
  3. Return value handling for storage:set

Without error handling, exceptions will crash or hang the renderer process awaiting the IPC response.

Apply this diff:

 export function registerStorageHandlers() {
   ipcMain.handle("storage:get", async (_event, input: { key: string }) => {
+    if (!input?.key || typeof input.key !== 'string') {
+      throw new Error('Invalid key provided');
+    }
+    try {
       return store.get(input.key);
+    } catch (error) {
+      console.error(`Failed to get key "${input.key}":`, error);
+      throw error;
+    }
   });
 
   ipcMain.handle(
     "storage:set",
     async (_event, input: { key: string; value: any }) => {
+      if (!input?.key || typeof input.key !== 'string') {
+        throw new Error('Invalid key provided');
+      }
+      try {
         store.set(input.key, input.value);
+      } catch (error) {
+        console.error(`Failed to set key "${input.key}":`, error);
+        throw error;
+      }
     },
   );
 
   ipcMain.handle("storage:delete", async (_event, input: { key: string }) => {
+    if (!input?.key || typeof input.key !== 'string') {
+      throw new Error('Invalid key provided');
+    }
+    try {
       store.delete(input.key);
+    } catch (error) {
+      console.error(`Failed to delete key "${input.key}":`, error);
+      throw error;
+    }
   });
 }

12 changes: 12 additions & 0 deletions apps/desktop/src/main/lib/storage-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Store from "electron-store";
import { homedir } from "node:os";
import { join } from "node:path";

/**
* Electron store instance for persisting application state
* Stores data in ~/.superset/app-state.json
*/
export const store = new Store({
cwd: join(homedir(), ".superset"),
name: "app-state",
});
13 changes: 13 additions & 0 deletions apps/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ declare global {
interface Window {
App: typeof API;
ipcRenderer: typeof ipcRendererAPI;
electronStore: {
get: (key: string) => any;
set: (key: string, value: any) => void;
delete: (key: string) => void;
};
}
}

Expand Down Expand Up @@ -66,5 +71,13 @@ const ipcRendererAPI = {
// Expose electron-trpc IPC channel FIRST (must be before contextBridge calls)
exposeElectronTRPC();

// Expose electron-store API via IPC
const electronStoreAPI = {
get: (key: string) => ipcRenderer.invoke("storage:get", { key }),
set: (key: string, value: any) => ipcRenderer.invoke("storage:set", { key, value }),
delete: (key: string) => ipcRenderer.invoke("storage:delete", { key }),
};

contextBridge.exposeInMainWorld("App", API);
contextBridge.exposeInMainWorld("ipcRenderer", ipcRendererAPI);
contextBridge.exposeInMainWorld("electronStore", electronStoreAPI);
20 changes: 20 additions & 0 deletions apps/desktop/src/renderer/lib/electron-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createJSONStorage } from "zustand/middleware";

/**
* Custom Zustand storage adapter that uses electron-store for persistence via IPC
* Stores state in ~/.superset/app-state.json
*/
const electronStorageAdapter = {
getItem: async (name: string): Promise<string | null> => {
const value = await window.electronStore.get(name);
return value as string | null;
Comment on lines +8 to +10
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.

⚠️ Potential issue | 🟠 Major

Unsafe type assertion could cause runtime errors.

The type cast as string | null on line 10 assumes the stored value is always a string, but electron-store can return any type. If non-string data was previously stored under the same key, this will cause issues when Zustand tries to parse it as JSON.

Apply this diff to add validation:

 getItem: async (name: string): Promise<string | null> => {
   const value = await window.electronStore.get(name);
-  return value as string | null;
+  if (value === undefined || value === null) return null;
+  if (typeof value !== 'string') {
+    console.warn(`Expected string for key "${name}", got ${typeof value}`);
+    return null;
+  }
+  return value;
 },
📝 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
getItem: async (name: string): Promise<string | null> => {
const value = await window.electronStore.get(name);
return value as string | null;
getItem: async (name: string): Promise<string | null> => {
const value = await window.electronStore.get(name);
if (value === undefined || value === null) return null;
if (typeof value !== 'string') {
console.warn(`Expected string for key "${name}", got ${typeof value}`);
return null;
}
return value;
},
🤖 Prompt for AI Agents
In apps/desktop/src/renderer/lib/electron-storage.ts around lines 8 to 10,
remove the unsafe type assertion and add runtime validation: check if the
retrieved value is undefined or null and return null; if it's already a string
return it; otherwise convert non-string values to a JSON string (JSON.stringify)
before returning so Zustand receives a string and you avoid runtime type errors.

},
setItem: async (name: string, value: string): Promise<void> => {
await window.electronStore.set(name, value);
},
removeItem: async (name: string): Promise<void> => {
await window.electronStore.delete(name);
},
};

export const electronStorage = createJSONStorage(() => electronStorageAdapter);
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { useWorkspacesStore } from "renderer/stores/workspaces";
import { useAddTab } from "renderer/stores";

export function NewWorkspaceView() {
const activeWorkspaceId = useWorkspacesStore(
(state) => state.activeWorkspaceId,
);
const addWorkspace = useWorkspacesStore((state) => state.addWorkspace);
const markWorkspaceAsUsed = useWorkspacesStore(
(state) => state.markWorkspaceAsUsed,
);
const addTab = useAddTab();

return (
<div className="flex-1 h-full flex items-center justify-center">
<div className="text-center max-w-2xl px-6">
Expand Down Expand Up @@ -29,11 +41,27 @@ export function NewWorkspaceView() {
</button>
<button
type="button"
onClick={() => {
if (!activeWorkspaceId) {
addWorkspace();
setTimeout(() => {
const newWorkspaceId =
useWorkspacesStore.getState().activeWorkspaceId;
if (newWorkspaceId) {
addTab(newWorkspaceId);
markWorkspaceAsUsed(newWorkspaceId);
}
}, 0);
} else {
addTab(activeWorkspaceId);
markWorkspaceAsUsed(activeWorkspaceId);
}
}}
Comment on lines +44 to +59
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.

⚠️ Potential issue | 🟠 Major

Race condition: Remove setTimeout anti-pattern.

The setTimeout(..., 0) pattern is unnecessary and introduces a race condition. Zustand state updates are synchronous by default, so activeWorkspaceId is updated immediately when addWorkspace() completes. The setTimeout adds complexity without benefit and the direct getState() call (line 49) bypasses the hook-based reactivity model.

Apply this diff to simplify the logic:

 onClick={() => {
   if (!activeWorkspaceId) {
     addWorkspace();
-    setTimeout(() => {
-      const newWorkspaceId =
-        useWorkspacesStore.getState().activeWorkspaceId;
-      if (newWorkspaceId) {
-        addTab(newWorkspaceId);
-        markWorkspaceAsUsed(newWorkspaceId);
-      }
-    }, 0);
-  } else {
-    addTab(activeWorkspaceId);
-    markWorkspaceAsUsed(activeWorkspaceId);
   }
+  // activeWorkspaceId is now guaranteed to be set (either existing or newly created)
+  const currentWorkspaceId = useWorkspacesStore.getState().activeWorkspaceId;
+  if (currentWorkspaceId) {
+    addTab(currentWorkspaceId);
+    markWorkspaceAsUsed(currentWorkspaceId);
+  }
 }}

Alternatively, refactor addWorkspace to return the new workspace ID for even cleaner code.

🤖 Prompt for AI Agents
In
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView.tsx
around lines 44 to 59, remove the setTimeout anti-pattern and the bypass of hook
reactivity: call addWorkspace(), then immediately read the new ID from
useWorkspacesStore.getState() (or even better, change addWorkspace to return the
new workspace ID) and proceed to addTab(newWorkspaceId) and
markWorkspaceAsUsed(newWorkspaceId); if activeWorkspaceId already exists, keep
the existing branch that calls addTab(activeWorkspaceId) and
markWorkspaceAsUsed(activeWorkspaceId). Ensure no asynchronous wrapper
(setTimeout) is used and rely on Zustand’s synchronous update or return value
from addWorkspace.

className="p-6 border border-border rounded-lg hover:bg-accent hover:border-accent-foreground transition-colors text-left"
>
<div className="text-2xl mb-2">🔍</div>
<div className="font-medium text-foreground">Search</div>
<div className="text-sm text-muted-foreground">Find anything</div>
<div className="font-medium text-foreground">Create Tab & Start</div>
<div className="text-sm text-muted-foreground">Test persistence</div>
Comment on lines +63 to +64
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.

⚠️ Potential issue | 🟡 Minor

Replace debug text with production-ready copy.

The subtitle "Test persistence" appears to be temporary debug text. Please update with appropriate user-facing copy before merging.

Suggested alternatives:

-<div className="text-sm text-muted-foreground">Test persistence</div>
+<div className="text-sm text-muted-foreground">Open a new tab</div>
📝 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
<div className="font-medium text-foreground">Create Tab & Start</div>
<div className="text-sm text-muted-foreground">Test persistence</div>
<div className="font-medium text-foreground">Create Tab & Start</div>
<div className="text-sm text-muted-foreground">Open a new tab</div>
🤖 Prompt for AI Agents
In
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView.tsx
around lines 63-64, the subtitle currently reads "Test persistence" which is
debug text; replace it with production-ready user-facing copy such as "Save your
workspace settings", "Automatically persist tabs and state", or simply
"Workspace will be saved automatically" (choose the wording that best fits the
UI tone), and ensure the string is i18n-ready if the app uses localization.

</button>
<button
type="button"
Expand Down
19 changes: 13 additions & 6 deletions apps/desktop/src/renderer/stores/tabs/store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { MosaicNode } from "react-mosaic-component";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { devtools, persist } from "zustand/middleware";
import {
cleanLayout,
handleDragTabToTab,
removeTabFromLayout,
} from "./drag-logic";
import { type Tab, TabType } from "./types";
import { createNewTab, getChildTabIds } from "./utils";
import { electronStorage } from "../../lib/electron-storage";

interface TabsState {
tabs: Tab[];
Expand Down Expand Up @@ -174,10 +175,11 @@ const handleEmptyGroupRemoval = (

export const useTabsStore = create<TabsState>()(
devtools(
(set, get) => ({
tabs: createInitialTabs(),
activeTabIds: { "workspace-1": "tab-single-1" },
tabHistoryStacks: { "workspace-1": [] },
persist(
(set, get) => ({
tabs: [],
activeTabIds: {},
tabHistoryStacks: {},
Comment on lines +180 to +182
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.

⚠️ Potential issue | 🟠 Major

Breaking change: Empty initial state will affect new and existing users.

The initial state has changed from populated tabs (via createInitialTabs()) to empty structures. This means:

  • New users won't see welcome tabs on first launch
  • The createInitialTabs() function (lines 43-95) is now unused

Consider initializing with createInitialTabs() on first load, then relying on persistence for subsequent sessions.

Example approach:

-		tabs: [],
-		activeTabIds: {},
-		tabHistoryStacks: {},
+		tabs: createInitialTabs(),
+		activeTabIds: { "workspace-1": "tab-single-1" },
+		tabHistoryStacks: {},

Then let persistence override these defaults on subsequent loads.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src/renderer/stores/tabs/store.ts around lines 180 to 182, the
reducer/store initial state was changed to empty structures which removes the
welcome/default tabs and leaves createInitialTabs() unused; restore initial tabs
by calling createInitialTabs() when initializing state if no persisted state
exists (i.e., use createInitialTabs() as the default initial value or run it on
first load), then allow the persistence layer to override that default on
subsequent loads so new users still see the welcome tabs while existing users
keep their persisted tabs.


addTab: (workspaceId, type = TabType.Single) => {
const newTab = createNewTab(workspaceId, type);
Expand Down Expand Up @@ -605,7 +607,12 @@ export const useTabsStore = create<TabsState>()(
const historyStack = get().tabHistoryStacks[workspaceId] || [];
return historyStack[0] || null;
},
}),
}),
{
name: "tabs-storage",
storage: electronStorage,
},
),
{ name: "TabsStore" },
),
);
Expand Down
111 changes: 59 additions & 52 deletions apps/desktop/src/renderer/stores/workspaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { devtools, persist } from "zustand/middleware";
import { electronStorage } from "../lib/electron-storage";

export interface Workspace {
id: string;
Expand All @@ -26,65 +27,71 @@ const createNewWorkspace = (): Workspace => ({

export const useWorkspacesStore = create<WorkspacesState>()(
devtools(
(set) => ({
workspaces: [{ id: "workspace-1", title: "Home" }],
activeWorkspaceId: "workspace-1",
persist(
(set) => ({
workspaces: [],
activeWorkspaceId: null,

addWorkspace: () => {
const newWorkspace = createNewWorkspace();
set((state) => ({
workspaces: [...state.workspaces, newWorkspace],
activeWorkspaceId: newWorkspace.id,
}));
},

removeWorkspace: (id) => {
set((state) => {
const workspaces = state.workspaces.filter(
(workspace) => workspace.id !== id,
);
if (workspaces.length === 0) {
const newWorkspace = createNewWorkspace();
return {
workspaces: [newWorkspace],
activeWorkspaceId: newWorkspace.id,
};
}
addWorkspace: () => {
const newWorkspace = createNewWorkspace();
set((state) => ({
workspaces: [...state.workspaces, newWorkspace],
activeWorkspaceId: newWorkspace.id,
}));
},

if (id === state.activeWorkspaceId) {
const closedIndex = state.workspaces.findIndex(
(workspace) => workspace.id === id,
removeWorkspace: (id) => {
set((state) => {
const workspaces = state.workspaces.filter(
(workspace) => workspace.id !== id,
);
const nextWorkspace =
workspaces[closedIndex] || workspaces[closedIndex - 1];
return { workspaces, activeWorkspaceId: nextWorkspace.id };
}
if (workspaces.length === 0) {
const newWorkspace = createNewWorkspace();
return {
workspaces: [newWorkspace],
activeWorkspaceId: newWorkspace.id,
};
}

return { workspaces };
});
},
if (id === state.activeWorkspaceId) {
const closedIndex = state.workspaces.findIndex(
(workspace) => workspace.id === id,
);
const nextWorkspace =
workspaces[closedIndex] || workspaces[closedIndex - 1];
return { workspaces, activeWorkspaceId: nextWorkspace.id };
}

setActiveWorkspace: (id) => {
set({ activeWorkspaceId: id });
},
return { workspaces };
});
},

reorderWorkspaces: (startIndex, endIndex) => {
set((state) => {
const workspaces = [...state.workspaces];
const [removed] = workspaces.splice(startIndex, 1);
workspaces.splice(endIndex, 0, removed);
return { workspaces };
});
},
setActiveWorkspace: (id) => {
set({ activeWorkspaceId: id });
},

reorderWorkspaces: (startIndex, endIndex) => {
set((state) => {
const workspaces = [...state.workspaces];
const [removed] = workspaces.splice(startIndex, 1);
workspaces.splice(endIndex, 0, removed);
return { workspaces };
});
},

markWorkspaceAsUsed: (id) => {
set((state) => ({
workspaces: state.workspaces.map((workspace) =>
workspace.id === id ? { ...workspace, isNew: false } : workspace,
),
}));
markWorkspaceAsUsed: (id) => {
set((state) => ({
workspaces: state.workspaces.map((workspace) =>
workspace.id === id ? { ...workspace, isNew: false } : workspace,
),
}));
},
}),
{
name: "workspaces-storage",
storage: electronStorage,
},
}),
),
Comment on lines +30 to +94
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Verify initial empty state doesn't break existing components.

The initial state changed to workspaces: [] and activeWorkspaceId: null (lines 32-33). While appropriate for persistence (loading saved state), this differs from previous behavior and may break components that assume at least one workspace always exists. The empty-workspace guard in removeWorkspace (lines 48-54) doesn't cover the initial load case.

Run this script to verify all consumers handle null/empty state:

Ensure all consuming components handle:

  • activeWorkspaceId === null
  • workspaces.length === 0

Or initialize with a default workspace if this breaks expected behavior.


🏁 Script executed:

#!/bin/bash
# Description: Find components using activeWorkspaceId and workspaces to verify null/empty handling

# Search for activeWorkspaceId usage
echo "=== Components accessing activeWorkspaceId ==="
rg -n --type=tsx --type=ts -C3 'activeWorkspaceId' -g '!*.test.*' -g '!workspaces.ts'

echo -e "\n=== Components accessing workspaces array ==="
rg -n --type=tsx --type=ts -C3 'useWorkspacesStore.*workspaces[^a-zA-Z]' -g '!*.test.*' -g '!workspaces.ts'

echo -e "\n=== Check for .workspaces[0] or similar direct access ==="
rg -n --type=tsx --type=ts 'workspaces\[0\]' -g '!*.test.*'

Length of output: 236


I need to fix the script—the --type=tsx flag isn't recognized by ripgrep. Let me run a corrected version that properly searches TypeScript files:


🏁 Script executed:

#!/bin/bash
# Description: Find components using activeWorkspaceId and workspaces to verify null/empty handling

# Search for activeWorkspaceId usage (corrected)
echo "=== Components accessing activeWorkspaceId ==="
rg -n 'activeWorkspaceId' --type=ts -C3 -g '!*.test.*' -g '!workspaces.ts'

echo -e "\n=== Components accessing workspaces array ==="
rg -n 'workspaces\s*\??\.' --type=ts -C3 -g '!*.test.*' -g '!workspaces.ts'

echo -e "\n=== Check for .workspaces[0] or similar direct array access ==="
rg -n 'workspaces\s*\[0\]' --type=ts -g '!*.test.*'

echo -e "\n=== Check for activeWorkspaceId?.id or null checks ==="
rg -n 'activeWorkspaceId\?' --type=ts -C2 -g '!workspaces.ts'

Length of output: 66636


I need to examine more closely what happens when the store initializes with empty state. Let me search for app initialization and the WorkspaceView render logic:

Let me search for the electronStorage implementation to see if it's synchronous or asynchronous:


🏁 Script executed:

#!/bin/bash
# Find electronStorage definition and usage
echo "=== Find electronStorage definition ==="
rg -n 'electronStorage' --type=ts -B2 -A5 -g '!*.test.*' | head -60

echo -e "\n=== Check if electronStorage is sync or async ==="
rg -n 'export.*electronStorage|const.*electronStorage.*=' --type=ts -A3 -g '!*.test.*'

Length of output: 3928


Now let me examine the WorkspaceView component more closely to see how it handles the case where activeWorkspace might be undefined:


🏁 Script executed:

#!/bin/bash
# Get full WorkspaceView implementation
echo "=== WorkspaceView full implementation ==="
cat apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx

echo -e "\n=== Check app initialization / root component ==="
rg -n 'WorkspaceView|main entry' --type=ts -B3 -A3 | head -40

Length of output: 4474


Let me verify if there's app initialization that creates a default workspace, and check what Sidebar renders:


🏁 Script executed:

#!/bin/bash
# Check for app initialization / root setup
echo "=== Check App.tsx or main entry ==="
find apps/desktop/src/renderer -name "App.tsx" -o -name "main.tsx" -o -name "index.tsx" | head -5 | xargs -I {} sh -c 'echo "File: {}"; head -40 "{}"'

echo -e "\n=== Check Sidebar implementation ==="
cat apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/index.tsx 2>/dev/null | head -60

echo -e "\n=== Check if there's any useEffect that initializes workspace ==="
rg -n 'useEffect|init.*workspace' --type=ts apps/desktop/src/renderer/screens/main/ -A3 | head -40

Length of output: 11062


WorkspaceView doesn't handle the undefined activeWorkspace case during initial load.

While most consumers properly guard against null activeWorkspaceId, the WorkspaceView component (line 12) only checks if (activeWorkspace?.isNew). When activeWorkspaceId is null during async storage hydration, activeWorkspace becomes undefined, causing the condition to be false. This renders the default Sidebar + ContentView layout instead of NewWorkspaceView, creating a broken UI momentarily.

Fix: Change line 12 to:

if (!activeWorkspace || activeWorkspace?.isNew) {

This ensures NewWorkspaceView renders when no active workspace exists, matching the intended UX flow where users create their first workspace.

🤖 Prompt for AI Agents
In apps/desktop/src/renderer/stores/workspaces.ts around lines 30 to 94, the
store allows activeWorkspaceId to be null during async hydration which can make
consumers receive undefined activeWorkspace; update consumers to treat missing
activeWorkspace as the "new" case by checking for both undefined/null and isNew.
Specifically, change the guard in WorkspaceView from only checking
activeWorkspace?.isNew to a combined check that treats !activeWorkspace
(null/undefined) or activeWorkspace.isNew as the NewWorkspaceView case so the UI
shows the creation flow during initial load.

{ name: "WorkspacesStore" },
),
);
4 changes: 3 additions & 1 deletion apps/desktop/src/shared/ipc-channels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { DeepLinkChannels } from "./deep-link";
import type { ExternalChannels } from "./external";
import type { ProxyChannels } from "./proxy";
import type { StorageChannels } from "./storage";
import type { TabChannels } from "./tab";
import type { TerminalChannels } from "./terminal";
import type { UiChannels } from "./ui";
Expand Down Expand Up @@ -35,7 +36,8 @@ export interface IpcChannels
ExternalChannels,
DeepLinkChannels,
WindowChannels,
UiChannels {}
UiChannels,
StorageChannels {}

/**
* Type-safe IPC channel names
Expand Down
20 changes: 20 additions & 0 deletions apps/desktop/src/shared/ipc-channels/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Storage-related IPC channels for electron-store persistence
*/

export interface StorageChannels {
"storage:get": {
request: { key: string };
response: any;
};

"storage:set": {
request: { key: string; value: any };
response: void;
};

"storage:delete": {
request: { key: string };
response: void;
};
}
Comment on lines +5 to +20
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.

🛠️ Refactor suggestion | 🟠 Major

Replace any with unknown for type safety.

Using any for response (line 8) and value (line 12) bypasses TypeScript's type checking. Use unknown instead to maintain type safety while allowing any value—consumers must then explicitly narrow the type before use.

Apply this diff:

 export interface StorageChannels {
   "storage:get": {
     request: { key: string };
-    response: any;
+    response: unknown;
   };

   "storage:set": {
-    request: { key: string; value: any };
+    request: { key: string; value: unknown };
     response: void;
   };

   "storage:delete": {
     request: { key: string };
     response: void;
   };
 }
🤖 Prompt for AI Agents
In apps/desktop/src/shared/ipc-channels/storage.ts around lines 5 to 20, replace
the uses of the any type with unknown: change the "storage:get" response type
from any to unknown and change the "storage:set" request.value type from any to
unknown so callers must explicitly narrow types; leave other shapes unchanged
and run TypeScript checks to ensure no downstream type errors.

Loading
Loading