Skip to content

Manage workspaces using trpc/lowdb, implement recent projects#112

Merged
saddlepaddle merged 3 commits intomainfrom
handle-opening-projects
Nov 20, 2025
Merged

Manage workspaces using trpc/lowdb, implement recent projects#112
saddlepaddle merged 3 commits intomainfrom
handle-opening-projects

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Nov 20, 2025

  • WIP
  • WIP - about to organize trpc mutations
  • WIP - trpc queries organized

Description

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes

Summary by CodeRabbit

  • New Features
    • Added project and workspace management system with persistent storage
    • Users can now track recent projects and quickly reopen them
    • New workspace management: create, delete, rename, reorder, and switch between workspaces
    • New UI interface displaying recent projects and workspace controls

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 20, 2025

Walkthrough

This PR introduces a comprehensive project and workspace management system for the desktop app. It adds a local database layer using lowdb, creates TRPC routers for projects and workspaces CRUD operations, provides React Query hooks for state management and cache invalidation, and builds new UI components while migrating existing components from local stores to hook-based interactions.

Changes

Cohort / File(s) Summary
Dependencies
apps/desktop/package.json
Adds nanoid (^5.1.6) and zod (^4.1.12) as new runtime dependencies.
Database Layer
apps/desktop/src/main/lib/db/index.ts,
apps/desktop/src/main/lib/db/schemas.ts
Introduces lowdb-based persistent storage initialization and schema definitions for workspaces, recent projects, tabs, and settings.
Main Process Integration
apps/desktop/src/main/index.ts
Adds initDb() call during app startup to initialize the database before app setup.
TRPC Router: Projects
apps/desktop/src/lib/trpc/routers/projects/index.ts,
apps/desktop/src/lib/trpc/routers/projects/projects.ts
Creates projects router with openProject, openRecent, getRecents, and removeRecent endpoints using Electron dialog and Zod validation.
TRPC Router: Workspaces
apps/desktop/src/lib/trpc/routers/workspaces/index.ts,
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Creates workspaces router with full CRUD operations: create, get, getAll, getActive, update, delete, setActive, and reorder with cleanup and state management.
TRPC Router Integration
apps/desktop/src/lib/trpc/routers/index.ts
Composes projects and workspaces routers into the main AppRouter, extending its return type.
React Query Hooks: Projects
apps/desktop/src/renderer/react-query/projects/index.ts,
apps/desktop/src/renderer/react-query/projects/useOpenProject.ts,
apps/desktop/src/renderer/react-query/projects/useOpenRecent.ts,
apps/desktop/src/renderer/react-query/projects/useRemoveRecent.ts
Wraps projects TRPC mutations with automatic workspace query invalidation on success.
React Query Hooks: Workspaces
apps/desktop/src/renderer/react-query/workspaces/index.ts,
apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts,
apps/desktop/src/renderer/react-query/workspaces/useUpdateWorkspace.ts,
apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts,
apps/desktop/src/renderer/react-query/workspaces/useSetActiveWorkspace.ts,
apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts
Wraps workspaces TRPC mutations with targeted query invalidation (e.g., getAll, getActive).
New UI Components
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/StartSection/StartSection.tsx,
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/StartSection/index.ts,
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentSection.tsx,
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentProjectItem.tsx,
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/index.ts,
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/index.ts
Adds StartSection (for opening projects) and RecentSection (for listing and managing recent projects) UI components with list rendering and action handlers.
Refactored UI Components
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView.tsx,
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/AddWorkspaceButton.tsx,
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx,
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx,
apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
Migrates from local store to TRPC queries and mutation hooks; updates workspace management, active state handling, and project opening/recent logic.
Shared Types
apps/desktop/src/shared/types.ts
Exports RecentProject interface (path, name, lastOpenedAt).

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI Component
    participant Hook as React Query Hook
    participant TRPC as TRPC Router
    participant DB as Database
    participant Cache as React Query Cache

    Note over UI,Cache: Opening a Project
    UI->>Hook: Call mutation (openProject)
    activate Hook
    Hook->>TRPC: mutate() → trpc.projects.openProject
    activate TRPC
    TRPC->>UI: Show folder picker dialog
    TRPC->>DB: Add/update recent project entry
    TRPC-->>Hook: Return { success, path, name }
    deactivate TRPC
    Hook->>Cache: Invalidate workspaces queries
    Hook->>UI: onSuccess callback
    deactivate Hook
    UI->>UI: Re-render with updated data

    Note over UI,Cache: Creating a Workspace
    UI->>Hook: Call mutation (createWorkspace)
    activate Hook
    Hook->>TRPC: mutate() → trpc.workspaces.create
    activate TRPC
    TRPC->>DB: Create workspace with order & timestamps
    TRPC->>DB: Update lastActiveWorkspaceId setting
    TRPC-->>Hook: Return created workspace
    deactivate TRPC
    Hook->>Cache: Invalidate all workspace queries
    Hook->>UI: onSuccess callback
    deactivate Hook
    UI->>UI: Re-render workspace list

    Note over UI,Cache: Deleting a Workspace
    UI->>Hook: Call mutation (deleteWorkspace)
    activate Hook
    Hook->>TRPC: mutate({ id })
    activate TRPC
    TRPC->>DB: Remove workspace by ID
    TRPC->>DB: Clean up orphaned recent projects
    TRPC->>DB: Reassign lastActiveWorkspaceId if needed
    TRPC-->>Hook: Return { success }
    deactivate TRPC
    Hook->>Cache: Invalidate all workspace queries
    Hook->>UI: onSuccess callback
    deactivate Hook
    UI->>UI: Re-render & update active workspace
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

  • Workspaces router logic: The delete operation includes multi-step cleanup (orphaned recents removal, lastActiveWorkspaceId reassignment) and the reorder function handles index-based reordering with state mutation.
  • NewWorkspaceView refactoring: Significant state and data-source migration from local stores to TRPC queries; integration of new components (StartSection, RecentSection) and handlers; requires understanding of the flow.
  • Cache invalidation patterns: Multiple hooks implement similar invalidation logic across projects and workspaces; verify consistency and absence of stale-cache scenarios.
  • Database schema and initialization: Verify lowdb setup, schema validation, and data persistence behavior.
  • Component integration: Ensure proper wiring between hooks, TRPC mutations, and UI updates, especially in WorkspaceItem and related drag-and-drop reorder logic.

Possibly related PRs

  • superset#102: Modifies desktop app tRPC routing, TRPC client/provider, and renderer stack integration; directly supports the new projects/workspaces routers introduced here.
  • superset#105: Overlaps with workspace UI updates (WorkspaceTabs, AddWorkspaceButton, WorkspaceItem); shares changes to workspace management components.
  • superset#101: Extensive changes to desktop workspace storage, IPCs, and shared types; code-level overlap with this PR's database schemas, workspace operations, and shared type definitions.

Poem

🐰 A workspace tale in code so fine,
Projects persist, recent projects align.
With zod and nanoid, our database blooms,
TRPC routers open new rooms.
React Query caches refresh with grace,
And StartSection finds its perfect place! 🌿

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete with only WIP notes and unfilled template sections; no substantive information about changes, related issues, testing, or type of change is provided. Fill in all required sections: describe changes made, link related issues, select appropriate type of change, and document testing steps and validation performed.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: managing workspaces via trpc/lowdb and implementing recent projects functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 handle-opening-projects

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (14)
apps/desktop/src/renderer/react-query/workspaces/useSetActiveWorkspace.ts (1)

1-23: Good implementation; optional parallel invalidation

The hook looks correct and consistent with other workspace mutations. If you care about tiny perf/latency wins, you could run the two invalidations in parallel:

await Promise.all([
	utils.workspaces.getActive.invalidate(),
	utils.workspaces.getAll.invalidate(),
]);

Not required, just a small optional tweak.

apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/AddWorkspaceButton.tsx (1)

2-11: Consider guarding against duplicate clicks while create is pending

The mutation wiring looks correct. To avoid users spamming the button and enqueueing multiple “New Workspace” creations, consider disabling the button while the mutation is in flight, e.g.:

const createWorkspace = useCreateWorkspace();

<Button
  variant="ghost"
  size="icon"
  onClick={handleAddWorkspace}
  disabled={createWorkspace.isPending}
  aria-label="Add new workspace"
>

Nice-to-have, not blocking.

Also applies to: 17-17

apps/desktop/src/shared/types.ts (1)

149-154: Clarify lastOpenedAt semantics for RecentProject

The RecentProject shape matches how the UI uses it, but lastOpenedAt: number is a bit opaque. Consider documenting the units (e.g., UNIX ms since epoch) via a short comment or JSDoc to avoid confusion or inconsistent usage later.

apps/desktop/src/renderer/react-query/workspaces/useUpdateWorkspace.ts (1)

1-22: Solid wrapper; consider guarding user onSuccess against invalidate failures

The mutation wrapper correctly:

  • Uses the same options type as the underlying mutation.
  • Centralizes workspace cache invalidation before calling the user-provided onSuccess.

One refinement: if utils.workspaces.invalidate() ever rejects, the user’s onSuccess will be skipped. A small try/finally keeps the user callback reliable:

onSuccess: async (...args) => {
  try {
    await utils.workspaces.invalidate();
  } finally {
    await options?.onSuccess?.(...args);
  }
},

This preserves your behavior while avoiding accidental coupling between cache invalidation success and the caller’s side effects.

apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx (1)

12-15: Guard width calculation for empty list and confirm workspace naming

Two small points here:

  1. Width calc when there are no workspaces
const availableWidth = containerWidth - ADD_BUTTON_WIDTH;
const calculatedWidth = Math.max(
  MIN_WORKSPACE_WIDTH,
  Math.min(MAX_WORKSPACE_WIDTH, availableWidth / workspaces.length),
);

When workspaces.length === 0, availableWidth / 0 yields Infinity, and the math happens to clamp this to MAX_WORKSPACE_WIDTH. It works, but it’s non-obvious. A simple guard is clearer:

if (workspaces.length === 0) {
  setWorkspaceWidth(MAX_WORKSPACE_WIDTH);
  return;
}
  1. Confirm the name field on workspace
<WorkspaceItem
  id={workspace.id}
  title={workspace.name}
  // ...
/>

Assuming the workspace objects from your query expose name (and not an older title field), this is correct. If any callers or backend types still use title, they’ll need aligning to avoid blank labels in the tabs.

Also applies to: 39-43, 92-92

apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentSection.tsx (1)

1-38: RecentSection logic is sound; consider making height configurable

The component correctly:

  • No-ops when there are no recents.
  • Delegates open/remove behavior via onOpenRecent / onRemoveRecent.
  • Uses project.path as a stable key.

One small UX refinement: className="h-[400px]" makes the scroll area fixed-height, which may feel cramped on small windows or overly constrained on large ones. Exposing the height as a prop or tying it to viewport/container size would make this section more adaptable if the layout needs to evolve.

apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts (1)

14-16: Consider using broader invalidation for consistency.

This hook invalidates only utils.workspaces.getAll, while other workspace mutation hooks (useCreateWorkspace, useSetActiveWorkspace, etc.) invalidate all workspace queries using utils.workspaces.invalidate(). This inconsistency could lead to stale data if other workspace-related queries exist.

Apply this diff for consistency:

-			await utils.workspaces.getAll.invalidate();
+			await utils.workspaces.invalidate();

Alternatively, if the specific invalidation is intentional for performance reasons, consider documenting why this hook differs from the others.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView.tsx (1)

9-15: Factor shared success logic and clarify behavior when activeWorkspace is missing

Both openProject and openRecent have the same onSuccess body. You can DRY this up and make the behavior around activeWorkspace more explicit:

  • Extract a small helper that takes { success, path, name } and, if success and activeWorkspace are truthy, runs updateWorkspace.mutateAsync and addTab.
  • Optionally log or otherwise handle the case where result.success === true but activeWorkspace is undefined (e.g. query still loading, workspace deleted), instead of silently no‑op’ing.

Example refactor:

-	const updateWorkspace = useUpdateWorkspace();
-
-	const openProject = useOpenProject({
-		onSuccess: async (result) => {
-			if (result.success && activeWorkspace) {
-				// Update workspace in DB with path and wait for it to complete
-				await updateWorkspace.mutateAsync({
-					id: activeWorkspace.id,
-					patch: {
-						path: result.path,
-						name: result.name,
-					},
-				});
-
-				// Add a tab for the project (still using Zustand for now)
-				addTab(activeWorkspace.id);
-			}
-		},
-	});
-
-	const openRecent = useOpenRecent({
-		onSuccess: async (result) => {
-			if (result.success && activeWorkspace) {
-				// Update workspace in DB with path and wait for it to complete
-				await updateWorkspace.mutateAsync({
-					id: activeWorkspace.id,
-					patch: {
-						path: result.path,
-						name: result.name,
-					},
-				});
-
-				// Add a tab for the project (still using Zustand for now)
-				addTab(activeWorkspace.id);
-			}
-		},
-	});
+	const updateWorkspace = useUpdateWorkspace();
+
+	const handleProjectOpenSuccess = async (result: {
+		success: boolean;
+		path: string;
+		name: string;
+	}) => {
+		if (!result.success || !activeWorkspace) return;
+
+		await updateWorkspace.mutateAsync({
+			id: activeWorkspace.id,
+			patch: {
+				path: result.path,
+				name: result.name,
+			},
+		});
+
+		addTab(activeWorkspace.id);
+	};
+
+	const openProject = useOpenProject({
+		onSuccess: handleProjectOpenSuccess,
+	});
+
+	const openRecent = useOpenRecent({
+		onSuccess: handleProjectOpenSuccess,
+	});

Also applies to: 16-32, 34-50

apps/desktop/src/lib/trpc/routers/projects/projects.ts (2)

19-52: Consider bounding recents list size to avoid unbounded growth

openProject appends to data.recentProjects and getRecents simply returns all entries sorted. Over time this will grow without bound, which may not be desirable for a “recent projects” list.

If you intend to show only a limited number (e.g. last 10–20):

-			await db.update((data) => {
+			await db.update((data) => {
 				const existingIndex = data.recentProjects.findIndex(
 					(p) => p.path === path,
 				);
 				if (existingIndex !== -1) {
 					data.recentProjects[existingIndex].lastOpenedAt = Date.now();
 				} else {
 					data.recentProjects.push({
 						path,
 						name,
 						lastOpenedAt: Date.now(),
 					});
+					// Keep only the N most recent entries
+					data.recentProjects
+						.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)
+						.splice(20); // or whatever max length you want
 				}
 			});

Also applies to: 72-76


53-71: Clarify “not found” behavior and keep docs in sync with implementation

Two small points here:

  • openRecent and removeRecent both return success even if the path does not exist in recentProjects. That’s fine if callers only ever pass paths from getRecents, but if you ever need to distinguish “not found” from “updated/removed”, consider returning success: false (or an additional flag) when no matching entry is found.
  • The file-level comment mentions “workspace creation”, but this router currently only handles project selection and recents. It would be good to update the comment to reflect the actual responsibilities.

Also applies to: 77-87, 9-12

apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx (1)

5-9: Reordering via mutation on hover may be heavier than needed

The TRPC wiring for setActive and deleteWorkspace looks good, but for reordering you’re now calling reorderWorkspaces.mutate inside the DnD hover handler. That means each boundary crossed during a drag issues a mutation and a DB write.

Consider an approach like:

  • Keep the existing hover logic purely local (e.g. update a local/Zustand ordering to keep drag feedback snappy).
  • Fire a single reorderWorkspaces.mutate({ fromIndex, toIndex }) in the drop handler once the drag ends.

That preserves the UX from the old implementation while avoiding multiple persistence calls per drag gesture.

Also applies to: 32-35, 47-55, 71-72, 94-97

apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (2)

15-44: Compute order inside the db.update call to avoid stale reads

maxOrder is computed from db.data.workspaces before calling await db.update(...). If db.update does not synchronously mutate db.data before returning its Promise, two concurrent create calls could both see the same maxOrder and assign duplicate order values, which would make ordering ambiguous.

A safer pattern is to compute maxOrder and construct the workspace inside the db.update callback, then return it:

create: publicProcedure
  .input(
    z.object({
      name: z.string(),
      path: z.string().nullable().optional(),
    }),
  )
  .mutation(async ({ input }) => {
    let created = null as (typeof db.data.workspaces)[number] | null;

    await db.update((data) => {
      const maxOrder =
        data.workspaces.length > 0
          ? Math.max(...data.workspaces.map((w) => w.order))
          : -1;

      const workspace = {
        id: nanoid(),
        name: input.name,
        path: input.path ?? null,
        order: maxOrder + 1,
        createdAt: Date.now(),
        updatedAt: Date.now(),
        lastOpenedAt: Date.now(),
      };

      data.workspaces.push(workspace);
      data.settings.lastActiveWorkspaceId = workspace.id;
      created = workspace;
    });

    return created!;
  });

This keeps the read/modify sequence atomic with respect to other updates.


82-113: Consider decoupling updatedAt from lastOpenedAt on generic updates

In the update mutation, every patch (including simple renames or path changes) sets both updatedAt and lastOpenedAt to Date.now().

If you intend lastOpenedAt to reflect when the workspace was last activated/used, it may be clearer to only set:

  • updatedAt in update
  • both updatedAt and lastOpenedAt in setActive (and any “open project” mutations)

This keeps “edited” and “opened” timelines distinct:

// inside update
workspace.updatedAt = Date.now();
// leave lastOpenedAt unchanged

Up to you whether to keep current behavior or narrow it.

apps/desktop/src/main/lib/db/schemas.ts (1)

6-29: Be mindful of duplicated and diverging type definitions vs shared/types

There are similarly named interfaces in apps/desktop/src/shared/types.ts:

  • RecentProject with the same fields.
  • Tab and Workspace with different fields and timestamp types (strings vs numbers, additional props like repoPath, worktrees, etc.).

Having two Workspace/Tab types with different shapes can be confusing and can drift over time. Consider:

  • Renaming these DB-layer interfaces (e.g., DbWorkspace, DbTab, DbRecentProject), or
  • Importing and reusing the shared RecentProject where feasible, and clearly documenting where the DB types intentionally differ.

This will reduce accidental misuse of the wrong type in main vs renderer code.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e217fbb and b43a698.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (31)
  • apps/desktop/package.json (2 hunks)
  • apps/desktop/src/lib/trpc/routers/index.ts (2 hunks)
  • apps/desktop/src/lib/trpc/routers/projects/index.ts (1 hunks)
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts (1 hunks)
  • apps/desktop/src/lib/trpc/routers/workspaces/index.ts (1 hunks)
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (1 hunks)
  • apps/desktop/src/main/index.ts (2 hunks)
  • apps/desktop/src/main/lib/db/index.ts (1 hunks)
  • apps/desktop/src/main/lib/db/schemas.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/projects/index.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/projects/useOpenProject.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/projects/useOpenRecent.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/projects/useRemoveRecent.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/workspaces/index.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/workspaces/useSetActiveWorkspace.ts (1 hunks)
  • apps/desktop/src/renderer/react-query/workspaces/useUpdateWorkspace.ts (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/AddWorkspaceButton.tsx (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx (5 hunks)
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx (3 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView.tsx (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentProjectItem.tsx (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentSection.tsx (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/index.ts (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/StartSection/StartSection.tsx (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/StartSection/index.ts (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/index.ts (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx (1 hunks)
  • apps/desktop/src/shared/types.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (17)
apps/desktop/src/renderer/react-query/projects/useRemoveRecent.ts (1)
apps/desktop/src/renderer/react-query/projects/index.ts (1)
  • useRemoveRecent (3-3)
apps/desktop/src/renderer/react-query/projects/useOpenRecent.ts (1)
apps/desktop/src/renderer/react-query/projects/index.ts (1)
  • useOpenRecent (2-2)
apps/desktop/src/renderer/react-query/workspaces/useSetActiveWorkspace.ts (1)
apps/desktop/src/renderer/react-query/workspaces/index.ts (1)
  • useSetActiveWorkspace (4-4)
apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts (1)
apps/desktop/src/renderer/react-query/workspaces/index.ts (1)
  • useReorderWorkspaces (5-5)
apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts (1)
apps/desktop/src/renderer/react-query/workspaces/index.ts (1)
  • useDeleteWorkspace (3-3)
apps/desktop/src/renderer/react-query/workspaces/useUpdateWorkspace.ts (1)
apps/desktop/src/renderer/react-query/workspaces/index.ts (1)
  • useUpdateWorkspace (2-2)
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (2)
apps/desktop/src/lib/trpc/routers/workspaces/index.ts (2)
  • createWorkspacesRouter (1-1)
  • WorkspacesRouter (2-2)
apps/desktop/src/lib/trpc/index.ts (1)
  • router (15-15)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView.tsx (7)
apps/desktop/src/renderer/stores/tabs/store.ts (1)
  • useAddTab (624-624)
apps/desktop/src/renderer/react-query/workspaces/useUpdateWorkspace.ts (1)
  • useUpdateWorkspace (7-22)
apps/desktop/src/renderer/react-query/projects/useOpenProject.ts (1)
  • useOpenProject (7-22)
apps/desktop/src/renderer/react-query/projects/useOpenRecent.ts (1)
  • useOpenRecent (7-22)
apps/desktop/src/renderer/react-query/projects/useRemoveRecent.ts (1)
  • useRemoveRecent (7-22)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/StartSection/StartSection.tsx (1)
  • StartSection (9-26)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentSection.tsx (1)
  • RecentSection (12-38)
apps/desktop/src/main/index.ts (1)
apps/desktop/src/main/lib/db/index.ts (1)
  • initDb (11-17)
apps/desktop/src/lib/trpc/routers/projects/projects.ts (2)
apps/desktop/src/lib/trpc/routers/projects/index.ts (2)
  • createProjectsRouter (1-1)
  • ProjectsRouter (2-2)
apps/desktop/src/lib/trpc/index.ts (1)
  • router (15-15)
apps/desktop/src/main/lib/db/index.ts (1)
apps/desktop/src/main/lib/db/schemas.ts (2)
  • Database (35-39)
  • defaultDatabase (44-48)
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/AddWorkspaceButton.tsx (1)
apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts (1)
  • useCreateWorkspace (7-22)
apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts (1)
apps/desktop/src/renderer/react-query/workspaces/index.ts (1)
  • useCreateWorkspace (1-1)
apps/desktop/src/main/lib/db/schemas.ts (1)
apps/desktop/src/shared/types.ts (3)
  • RecentProject (150-154)
  • Tab (44-56)
  • Workspace (70-82)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentSection.tsx (2)
apps/desktop/src/shared/types.ts (1)
  • RecentProject (150-154)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentProjectItem.tsx (1)
  • RecentProjectItem (32-70)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentProjectItem.tsx (3)
apps/desktop/src/main/lib/db/schemas.ts (1)
  • RecentProject (6-10)
apps/desktop/src/shared/types.ts (1)
  • RecentProject (150-154)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/index.ts (1)
  • RecentProjectItem (2-2)
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx (3)
apps/desktop/src/renderer/react-query/workspaces/useSetActiveWorkspace.ts (1)
  • useSetActiveWorkspace (7-23)
apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts (1)
  • useDeleteWorkspace (7-22)
apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts (1)
  • useReorderWorkspaces (7-22)
🔇 Additional comments (27)
apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts (1)

1-21: useDeleteWorkspace hook pattern looks solid

Hook correctly wraps the TRPC mutation, invalidates workspace queries, and then delegates to any user-provided onSuccess; matches the pattern used in useCreateWorkspace.

apps/desktop/src/renderer/react-query/projects/index.ts (1)

1-3: Barrel exports look good

Cleanly re-exports the project hooks and matches the pattern used for other React Query barrels.

apps/desktop/src/main/index.ts (1)

6-6: DB initialization placement makes sense; verify no early db access

Initializing the DB right after app.whenReady() and before makeAppSetup is a good place for initDb(). Please double-check that no other main-process module accesses the db proxy at import time (e.g., top-level code in TRPC routers), otherwise the proxy’s guard will throw before initDb has run.

Also applies to: 33-38

apps/desktop/package.json (1)

52-53: New runtime dependencies look appropriate; check for version alignment

Adding nanoid and zod here aligns with the new DB/TRPC/validation work. Please verify there aren’t conflicting versions of these packages in other workspace packages so that tooling and bundling stay consistent.

Also applies to: 67-67

apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/StartSection/index.ts (1)

1-1: StartSection barrel export is fine

Straightforward re-export; keeps imports tidy for NewWorkspaceView.

apps/desktop/src/lib/trpc/routers/projects/index.ts (1)

1-2: Barrel re-exports look consistent and correct

The router factory and type are re-exported cleanly from the projects module; this matches the intended TRPC router structure and keeps imports tidy.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/index.ts (1)

1-2: Component barrel exports are straightforward

Publicly exposing StartSection and RecentSection from this index keeps imports ergonomic and matches the surrounding folder structure.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/index.ts (1)

1-2: Recent section barrel exports are clean

Re-exporting both RecentSection and RecentProjectItem from this index keeps the public surface simple and matches how other component folders are structured.

apps/desktop/src/lib/trpc/routers/workspaces/index.ts (1)

1-2: LGTM! Clean barrel export.

The re-exports follow standard module organization patterns and correctly expose the workspaces router factory and type.

apps/desktop/src/renderer/react-query/projects/useOpenProject.ts (1)

1-22: LGTM! Solid mutation hook with proper cache invalidation.

The hook correctly wraps the TRPC mutation, invalidates workspace queries on success, and forwards the user's callback. The pattern is consistent with similar hooks in the codebase.

apps/desktop/src/lib/trpc/routers/index.ts (1)

4-5: LGTM! AppRouter correctly extended.

The projects and workspaces routers are properly integrated into the main router composition. The type inference via ReturnType<typeof createAppRouter> will automatically reflect these additions.

Also applies to: 14-15

apps/desktop/src/renderer/react-query/projects/useOpenRecent.ts (1)

1-22: LGTM! Consistent with useOpenProject pattern.

The hook follows the same pattern as useOpenProject, properly invalidating workspace queries when a recent project is opened.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/RecentSection/RecentProjectItem.tsx (2)

11-30: LGTM! Clean timestamp formatting logic.

The relative timestamp logic is well-structured and provides appropriate granularity. The "Yesterday" case (days === 1, representing 24-48 hours ago) is reasonable for a relative time display.


32-70: LGTM! Solid component implementation.

The component properly handles user interactions with good accessibility practices (semantic buttons, proper event handling with stopPropagation). The UI layout with folder icon, project details, timestamp, and remove button is well-structured.

apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts (1)

1-22: LGTM! Consistent invalidation pattern.

The hook properly uses broad invalidation (utils.workspaces.invalidate()) to ensure all workspace-related queries are refreshed after creation. This aligns with the pattern used by most other workspace mutation hooks.

apps/desktop/src/renderer/react-query/projects/useRemoveRecent.ts (1)

1-22: LGTM! Appropriate cache invalidation for recents removal.

The hook correctly invalidates the specific getRecents query after removing a recent project. This differs from useOpenRecent (which invalidates workspace queries) because the operations have different cache impact scopes—removal affects the recents list, while opening affects workspace state.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView.tsx (1)

52-64: New handlers + two‑column layout look clean and composable

The handler wiring (handleOpenProject, handleOpenRecent, handleRemoveRecent) and the new two‑column layout with StartSection/RecentSection are straightforward and keep the view nicely decoupled from TRPC details. Passing openProject.isPending down to disable the button is a good touch.

Also applies to: 66-101

apps/desktop/src/renderer/screens/main/components/WorkspaceView/NewWorkspaceView/components/StartSection/StartSection.tsx (1)

1-26: StartSection API and implementation are minimal and solid

Props surface (onOpenProject, isLoading) is clear, and the button wiring (including disabled state) is correct. No changes needed here.

apps/desktop/src/renderer/react-query/workspaces/index.ts (1)

1-5: Barrel exports are consistent and help keep imports tidy

The barrel cleanly re-exports all workspace mutation hooks and matches the underlying filenames. This will simplify imports across the renderer.

apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (6)

1-4: Router structure and db wiring look coherent

Overall router composition and use of a shared db instance for all procedures is consistent and keeps workspace state centralized. No structural issues here.

Also applies to: 10-14


49-63: Basic read queries (get / getAll) are straightforward and correct

get and getAll read directly from db.data.workspaces, returning null when not found and always sorting by order. This matches the stored schema and should behave as expected.


68-76: getActive behavior is consistent with settings.lastActiveWorkspaceId

Looking up the active workspace via settings.lastActiveWorkspaceId and returning null when it is unset or when the workspace is missing is reasonable and aligns with the delete/setActive logic.


119-157: Delete flow and recents cleanup logic are sound

The delete mutation:

  • Short-circuits with { success: false, error: "Workspace not found" } when appropriate.
  • Removes the workspace by id.
  • Only prunes recentProjects for workspace.path when no other workspace still uses that path.
  • Recomputes lastActiveWorkspaceId based on lastOpenedAt ordering.

This is a solid set of side effects and should keep recents and settings consistent.


162-177: setActive correctly updates both settings and activity timestamps

Finding the workspace by id, throwing when missing, then updating:

  • settings.lastActiveWorkspaceId
  • workspace.lastOpenedAt
  • workspace.updatedAt

is consistent with the rest of the router and with how getActive and the delete fallback work.


216-216: Exported WorkspacesRouter type is useful for composing the app router

Exporting WorkspacesRouter as ReturnType<typeof createWorkspacesRouter> is a good pattern for keeping TRPC router types in sync with implementation.

apps/desktop/src/main/lib/db/schemas.ts (2)

6-39: Schema shapes align with router usage and lowdb needs

The RecentProject, Tab, Workspace, Settings, and Database interfaces are simple, numeric-timestamp structures well-suited for lowdb and match how the workspaces router interacts with workspaces, recentProjects, and settings. No shape issues here.


41-48: defaultDatabase is minimal and consistent with the schema

Initializing workspaces and recentProjects as empty arrays and settings as {} matches the Database interface (all fields present; lastActiveWorkspaceId optional). This should give a safe, predictable initial state.

Comment on lines +182 to +212
reorder: publicProcedure
.input(
z.object({
fromIndex: z.number(),
toIndex: z.number(),
}),
)
.mutation(async ({ input }) => {
await db.update((data) => {
const { fromIndex, toIndex } = input;

// Get all workspaces sorted by order
const workspaces = data.workspaces
.slice()
.sort((a, b) => a.order - b.order);

// Move workspace from fromIndex to toIndex
const [removed] = workspaces.splice(fromIndex, 1);
workspaces.splice(toIndex, 0, removed);

// Update order fields to reflect new positions
workspaces.forEach((workspace, index) => {
const ws = data.workspaces.find((w) => w.id === workspace.id);
if (ws) {
ws.order = index;
}
});
});

return { success: true };
}),
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 | 🔴 Critical

Guard against out-of-range indices in reorder to avoid runtime errors

reorder assumes fromIndex and toIndex are valid in the sorted workspaces array:

const [removed] = workspaces.splice(fromIndex, 1);
workspaces.splice(toIndex, 0, removed);

If fromIndex is out of bounds, removed becomes undefined, and workspace.id in the later forEach will throw a TypeError. Similarly, an invalid toIndex can insert undefined into the array.

Add explicit bounds checks before splicing:

await db.update((data) => {
  const { fromIndex, toIndex } = input;

  const workspaces = data.workspaces
    .slice()
    .sort((a, b) => a.order - b.order);

  const lastIndex = workspaces.length - 1;
  if (
    fromIndex < 0 ||
    fromIndex > lastIndex ||
    toIndex < 0 ||
    toIndex > lastIndex
  ) {
    // Either no-op or throw a typed error here
    return;
  }

  const [removed] = workspaces.splice(fromIndex, 1);
  workspaces.splice(toIndex, 0, removed);

  workspaces.forEach((workspace, index) => {
    const ws = data.workspaces.find((w) => w.id === workspace.id);
    if (ws) {
      ws.order = index;
    }
  });
});

This prevents malformed state and makes the mutation robust to bad indices.

🤖 Prompt for AI Agents
In apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts around lines
182–212, the reorder mutation assumes fromIndex/toIndex are valid and can splice
undefined into the workspaces array; add an explicit bounds check before any
splicing: compute lastIndex = workspaces.length - 1 and if fromIndex or toIndex
are outside 0..lastIndex return early (or throw a typed TRPC error), and ensure
you only proceed with splice and order reassignment when the removed item is
defined so you never access workspace.id on undefined.

Comment on lines +1 to +26
import { JSONFilePreset } from "lowdb/node";
import { join } from "node:path";
import { app } from "electron";
import type { Database } from "./schemas";
import { defaultDatabase } from "./schemas";

type DB = Awaited<ReturnType<typeof JSONFilePreset<Database>>>;

let _db: DB | null = null;

export async function initDb(): Promise<void> {
if (_db) return;

const dbPath = join(app.getPath("userData"), "db.json");
_db = await JSONFilePreset<Database>(dbPath, defaultDatabase);
console.log(`Database initialized at: ${dbPath}`);
}

export const db = new Proxy({} as DB, {
get(_target, prop) {
if (!_db) {
throw new Error("Database not initialized. Call initDb() first.");
}
return _db[prop as keyof DB];
},
});
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

🧩 Analysis chain

Revisit DB type alias and be mindful of import-time db access

The lazy init + proxy pattern is nice, but two things to double-check:

  1. The DB type alias
type DB = Awaited<ReturnType<typeof JSONFilePreset<Database>>>;

Using typeof JSONFilePreset<Database> is likely not doing what you intend in TypeScript (you generally can’t apply type arguments on the value-side like this). Consider either:

  • Importing the DB type from lowdb and parameterizing it directly (e.g., Low<Database>), or
  • Falling back to type DB = Awaited<ReturnType<typeof JSONFilePreset>>; if you’re OK with looser typing.

Please run the typecheck to confirm this compiles as expected.

  1. Proxy usage

The proxy correctly throws when _db is null, but that also means any top-level access of db in other modules (outside of functions/handlers called after initDb) will hard-fail at import time. Make sure all consumers only touch db after initDb has been awaited (as wired in apps/desktop/src/main/index.ts).


I'll verify the concerns raised in the review comment by checking TypeScript compilation, inspecting how db is used throughout the codebase, and verifying the initialization sequence.


🏁 Script executed:

#!/bin/bash
# Check if TypeScript compilation succeeds
cd /repo
if [ -f "tsconfig.json" ]; then
  echo "=== Running TypeScript type check ==="
  npx tsc --noEmit 2>&1 | head -50
else
  echo "tsconfig.json not found"
fi

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Find all usages of the db export from index.ts
cd /repo
echo "=== Searching for imports/usages of db from db/index.ts ==="
rg -n "from ['\"].*db/index['\"]|from ['\"].*db['\"]" --type=ts --type=tsx -A 2 -B 2

Length of output: 143


🏁 Script executed:

#!/bin/bash
# Check the main entry point for initialization order
cd /repo
echo "=== Checking apps/desktop/src/main/index.ts for initDb call ==="
if [ -f "apps/desktop/src/main/index.ts" ]; then
  cat "apps/desktop/src/main/index.ts"
else
  echo "File not found, searching for main entry point..."
  find apps/desktop/src/main -name "index.ts" -o -name "main.ts" | head -5
fi

Length of output: 1268


🏁 Script executed:

#!/bin/bash
# Search for direct db access patterns outside of function scopes
cd /repo
echo "=== Searching for top-level db accesses ==="
rg -n "\bdb\." --type=ts --type=tsx | grep -v "function\|const\|let\|async\|=>|//|initDb" | head -20

Length of output: 127


I see the issue with the previous scripts. Let me regenerate them to work with the correct environment and settings.


🏁 Script executed:

#!/bin/bash
# Check if TypeScript exists and run type check
echo "=== Checking TypeScript and running type check ==="
if command -v tsc &> /dev/null; then
  tsc --noEmit 2>&1 | grep -i "error\|db" | head -30
else
  echo "tsc not found, checking for lowdb types..."
  find . -name "lowdb" -type d | head -5
  find . -name "package.json" | head -1 | xargs grep -l "lowdb" 2>/dev/null
fi

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Search for all db imports and usages
echo "=== Searching for db imports ==="
rg -n "from.*db/index|import.*db" --type=js -A 3 -B 1 | head -80

Length of output: 32


🏁 Script executed:

#!/bin/bash
# Check lowdb version and exported types in package.json
echo "=== Checking lowdb package ==="
find . -name "package.json" | xargs grep -A 2 -B 2 "lowdb" 2>/dev/null | head -20

Length of output: 846


🏁 Script executed:

#!/bin/bash
# Look for top-level db property access (not inside functions)
echo "=== Checking for direct db accesses ==="
rg -n "db\.\w+" --type=js -B 5 -A 2 | grep -v "function\|async\|=>\|//" | head -50

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Verify the db/schemas file exists and check Database type
echo "=== Checking db/schemas file ==="
if [ -f "apps/desktop/src/main/lib/db/schemas.ts" ]; then
  echo "=== Content of schemas.ts ==="
  cat "apps/desktop/src/main/lib/db/schemas.ts"
else
  find . -name "schemas.ts" -path "*db*" | xargs cat 2>/dev/null
fi

Length of output: 1029


Let me search more thoroughly for db usages and verify the lowdb type behavior.

[/run_scripts]

[/web_search]


🏁 Script executed:

#!/bin/bash
# Search with different patterns for db usage
echo "=== Searching all .ts files for db references ==="
find . -name "*.ts" -type f | xargs grep -l "db\." 2>/dev/null | head -20

Length of output: 981


🏁 Script executed:

#!/bin/bash
# Check the storage-ipcs handler mentioned in main entry
echo "=== Checking registerStorageHandlers ==="
find . -path "*storage-ipcs*" -name "*.ts" | head -5 | xargs cat 2>/dev/null | head -100

Length of output: 646


🏁 Script executed:

#!/bin/bash
# Look for any imports of db from db/index
echo "=== Searching for db imports from db/index ==="
grep -r "from.*db/index\|from.*db\"" --include="*.ts" . 2>/dev/null | head -30

Length of output: 324


🏁 Script executed:

#!/bin/bash
# Check if db is ever accessed at module level (not in functions)
echo "=== Checking db/index.ts for the DB type alias ==="
cat "apps/desktop/src/main/lib/db/index.ts" | head -30

Length of output: 777


🌐 Web query:

lowdb v7 JSONFilePreset type signature TypeScript

💡 Result:

Signature (TypeScript):

async function JSONFilePreset(filename: fs.PathLike | string, defaultData: T): Promise<Low>

Usage example:
const db = await JSONFilePreset('db.json', defaultData);

Sources: lowdb README and v7 release notes. [1][2]

References
[1] lowdb README (usage / presets).
[2] lowdb v7 release notes (presets renamed to JSONFilePreset).


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check how db is used in projects router
echo "=== Checking projects/projects.ts db usage ==="
cat "./apps/desktop/src/lib/trpc/routers/projects/projects.ts" | grep -A 10 -B 5 "db\."

Length of output: 1307


🏁 Script executed:

#!/bin/bash
# Check how db is used in workspaces router
echo "=== Checking workspaces/workspaces.ts db usage ==="
cat "./apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts" | grep -A 10 -B 5 "db\."

Length of output: 3760


🏁 Script executed:

#!/bin/bash
# Verify the actual type returned by JSONFilePreset
echo "=== Checking lowdb Low type structure ==="
find . -path "*node_modules/lowdb*" -name "*.d.ts" | head -5 | xargs grep -A 3 "class Low\|interface Low\|export.*Low" 2>/dev/null | head -30

Length of output: 41


Fix the DB type alias to preserve TypeScript type safety

The lazy init + proxy pattern is sound, and all db consumers correctly access it only within handler callbacks (never at module level). However, the type alias does have an issue:

type DB = Awaited<ReturnType<typeof JSONFilePreset<Database>>>;

typeof JSONFilePreset<Database> is invalid in TypeScript—type parameters cannot be applied on the value side. This causes the alias to resolve to Low<unknown>, losing the Database type information. TypeScript will not validate that consumers are accessing the correct properties.

To fix, import Low from lowdb and parameterize it directly:

import { Low } from "lowdb";
type DB = Low<Database>;

Alternatively, if you prefer avoiding the extra import, use typeof JSONFilePreset without the type parameter (though this will still result in Low<unknown>).

The import-time access concern is addressed by proper initialization sequencing in apps/desktop/src/main/index.ts—all db accesses occur only within handler callbacks after initDb() is awaited.

🤖 Prompt for AI Agents
In apps/desktop/src/main/lib/db/index.ts lines 1-26, the DB type alias uses
Awaited<ReturnType<typeof JSONFilePreset<Database>>> which misapplies a type
parameter to a value and collapses to Low<unknown>; replace that with an
explicit Low<Database> type by importing Low from "lowdb" and change the DB
alias to Low<Database>, and update imports accordingly so TypeScript preserves
the Database shape.

Comment on lines +7 to 13
const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery();

// If no workspace or workspace has no path, show new workspace view
const isNew = !activeWorkspace || activeWorkspace.path === null;

if (activeWorkspace?.isNew) {
if (isNew) {
return (
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

Loading/error state currently masquerades as “new workspace”

Right now isNew is computed from data only:

const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery();
// ...
const isNew = !activeWorkspace || activeWorkspace.path === null;

This means:

  • While the query is still loading, or if it errors and data stays undefined, the view will render NewWorkspaceView, potentially prompting users to create a new workspace even though one exists or an error occurred.
  • The logic also assumes activeWorkspace.path exists and becomes exactly null for “new” workspaces; if the backend ever uses undefined/empty string instead, this check will misclassify the state.

Consider:

  • Explicitly handling isLoading/error (or using Suspense) so those states don’t fall into the “new workspace” path.
  • Treating the path as “unset” via a null-safe check like activeWorkspace?.path == null or, if available, delegating to a dedicated flag from the backend (e.g., activeWorkspace.isNew).

This will avoid incorrect rendering and UX flicker around startup and error cases.


I'll verify the concerns in this review comment by examining the codebase. Let me start by gathering the necessary information.


---

🏁 Script executed:

```shell
#!/bin/bash
# Read the WorkspaceView file to see current implementation
cat apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx

Length of output: 708


🏁 Script executed:

#!/bin/bash
# Find workspace type definition
find . -type f \( -name "*.ts" -o -name "*.tsx" \) -path "*/workspace*" | head -20

Length of output: 1227


🏁 Script executed:

#!/bin/bash
# Search for trpc workspaces getActive query definition
rg -n "workspaces\.getActive" --type ts --type tsx -A 5

Length of output: 27


Let me continue verifying with corrected commands:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for workspaces getActive query definition (fix tsx issue)
rg -n "getActive" --type ts -A 8 apps/desktop/src/lib/trpc/routers/workspaces/

Length of output: 800


🏁 Script executed:

#!/bin/bash
# Check workspace type definition
cat apps/desktop/src/types/workspace.ts

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Look at workspaces router implementation
cat apps/desktop/src/lib/trpc/routers/workspaces/index.ts

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Check if there's a Suspense boundary or layout wrapper handling loading
rg -n "useQuery\|isLoading\|Suspense" apps/desktop/src/renderer/screens/main/ -B 2 -A 2

Length of output: 0


Let me search more broadly for the workspace type and check the parent component:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for workspace interface/type definition
rg -n "interface.*Workspace\|type.*Workspace\|export.*Workspace" --type ts -B 1 -A 5 | head -50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check parent components/layouts that might wrap WorkspaceView
find apps/desktop/src/renderer/screens/main -name "*.tsx" -type f | xargs cat | head -100

Length of output: 2386


🏁 Script executed:

#!/bin/bash
# Check the main app entry or layout
cat apps/desktop/src/renderer/screens/main/index.tsx 2>/dev/null || cat apps/desktop/src/renderer/App.tsx 2>/dev/null || find apps/desktop/src/renderer -name "App.tsx" -o -name "index.tsx" | head -1

Length of output: 617


🏁 Script executed:

#!/bin/bash
# Look for useQuery destructuring patterns to see if isLoading is typically extracted elsewhere
rg -n "useQuery.*isLoading\|useQuery.*error" apps/desktop/src/renderer --type ts -B 1 -A 1 | head -30

Length of output: 0


Handle loading and error states explicitly in the isNew logic

The issue is confirmed. During the query's loading phase, activeWorkspace is undefined, causing isNew to evaluate to true and render NewWorkspaceView—even though a workspace may exist and is just being fetched. Similarly, if the query errors, the same fallback occurs.

The fix is to extract isLoading from the useQuery hook:

const { data: activeWorkspace, isLoading } = trpc.workspaces.getActive.useQuery();
const isNew = !isLoading && (!activeWorkspace || activeWorkspace.path === null);

This ensures the view doesn't render NewWorkspaceView while the query is in flight or errored, preventing UX flicker and confusion at startup. Alternatively, return a loading/error state from this component instead of falling through to the new workspace view.

🤖 Prompt for AI Agents
In apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
around lines 7–13, the component treats undefined query data as a "new"
workspace during the query's loading or error states; extract isLoading (and
optionally isError) from trpc.workspaces.getActive.useQuery and change the isNew
calculation to only consider absence of a workspace after the query has finished
(e.g., isNew = !isLoading && !isError && (no workspace or workspace.path ===
null)), or alternatively render an explicit loading/error UI while
isLoading/isError are true so NewWorkspaceView is not shown during fetch or
error conditions.

@saddlepaddle saddlepaddle merged commit af797db into main Nov 20, 2025
1 of 5 checks passed
This was referenced Nov 21, 2025
@Kitenite Kitenite deleted the handle-opening-projects branch November 21, 2025 18:05
This was referenced Nov 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant