Skip to content

feat(desktop): streaming workspace creation UX - #580

Merged
Kitenite merged 14 commits into
ParadevAI:mainfrom
andreasasprou:improve-workspace-creation-ux
Jan 5, 2026
Merged

Kitenite merged 14 commits into
ParadevAI:mainfrom
andreasasprou:improve-workspace-creation-ux

Conversation

@andreasasprou

@andreasasprou andreasasprou commented Jan 3, 2026 •

Copy link
Copy Markdown
Contributor

Note: This PR is stacked on #579 and depends on it being merged first.

Links

  • ExecPlan: apps/desktop/plans/done/20260103-0901-workspace-creation-streaming-ux.md

Summary

  • Transform blocking workspace creation (5-15s) into an instant, non-blocking experience
  • Real-time progress streaming shows users exactly what's happening during initialization
  • Graceful error handling with retry capability

Why / Context

Previously, workspace creation blocked the UI for 5-15 seconds with a "Creating workspace..." toast while git operations completed. This felt sluggish and provided no visibility.

Now users experience:

  1. Click "Create Workspace" → modal closes instantly
  2. Workspace appears in sidebar with loading indicator
  3. Navigate to it to see step-by-step progress view
  4. On completion: seamless transition to terminal
  5. On failure: clear error with Retry button

How It Works

Backend:

  • WorkspaceInitManager class tracks initialization jobs with per-project mutex
  • Mutex prevents concurrent git operations that could cause lock conflicts
  • tRPC subscription (onInitProgress) streams real-time progress events
  • Usability guards block terminal/file operations until workspace is ready
  • Best-effort cleanup on failure after worktree creation
  • Auto-updates baseBranch if initially auto-derived and remote default differs

Frontend:

  • Zustand store (useWorkspaceInitStore) holds progress state + pending terminal setups
  • WorkspaceInitEffects (always mounted) handles terminal setup when workspaces become ready
  • WorkspaceItem shows spinner and status indicators
  • WorkspaceInitializingView displays step-by-step progress
  • useCreateWorkspace registers pending terminal setup in global store
  • Modal closes immediately, shows toast

Manual QA Checklist

improve-workspace-creation-ux.mp4

Happy Path

  • Create workspace → modal closes instantly
  • Workspace appears in sidebar with loading spinner
  • Navigate to initializing workspace → see progress view with steps
  • Progress updates in real-time
  • On completion → workspace ready, terminal opens

Error Handling

image
  • Failed init shows error state with Retry button
  • Retry successfully restarts initialization
  • Delete during init → workspace removed cleanly

Edge Cases

  • Create 2 workspaces in same project → no git lock conflicts (mutex)
  • Terminal blocked during init → appropriate error
  • Create workspace from CloneRepoDialog → terminal still opens after init (dialog unmount safe)

Testing

  • bun run typecheck ✅
  • bun run lint ✅
  • Manual testing in dev mode (pending)

Design Decisions

  • Why in-memory state, not database: Initialization is transient (seconds), simpler implementation. Documented limitation: app restart during init leaves workspace in incomplete state.
  • Why per-project mutex: Prevents git lock conflicts when creating multiple workspaces in same project simultaneously.
  • Why observable subscription (not async generator): trpc-electron only supports observables for IPC transport.
  • Why global Zustand store for pending terminal setups: Dialog components (InitGitDialog, CloneRepoDialog) unmount before init completes; hook-local state would be lost.
  • Why track baseBranchWasExplicit: Allows auto-correction when user relied on cached default branch that differs from remote.

Known Limitations

  • App restart during initialization leaves workspace in incomplete state requiring manual cleanup
  • No persistent initialization queue (in-progress init lost on restart)
  • Per-project mutex not used in all git operations (pre-existing, tracked for follow-up PR)

Files Changed

New Files

  • apps/desktop/src/main/lib/workspace-init-manager.ts - Init job tracking with mutex
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts - Workspace readiness guards
  • apps/desktop/src/shared/types/workspace-init.ts - Shared types
  • apps/desktop/src/renderer/stores/workspace-init.ts - Zustand store
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/ - Progress view component
  • apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx - Terminal setup effects (survives dialog unmount)

Modified Files

  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts - Split create into fast-path + background init, added subscription, baseBranch auto-update
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts - Added usability guard
  • apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx - Instant close
  • apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts - Simplified to use global store for pending setups
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx - Loading indicators
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx - Progress view integration
  • apps/desktop/src/renderer/screens/main/index.tsx - Subscription setup, WorkspaceInitEffects mount

Summary by CodeRabbit

  • New Features

    • Workspace creation returns immediately and runs initialization in the background with real-time progress streaming and retry/cancel controls.
    • New in-app initialization view showing step-by-step status, progress indicators, and actionable retry/delete options.
    • Terminal tabs are created only after a workspace is ready; pending terminal setups are processed automatically.
    • Contextual toasts report background progress and completion.
  • Bug Fixes / UX

    • Prevent deletion while a workspace is initializing.
    • Creation modal closes instantly with background progress feedback.

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

@coderabbitai

coderabbitai Bot commented Jan 3, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Implements asynchronous workspace creation: a fast-path workspace create returns immediately while a background, stepwise initialization runs with progress streaming, per-project locking, cancellation/retry, renderer-side progress store/effects, deferred terminal creation, and new tRPC procedures for progress and control.

Changes

Cohort / File(s) Summary
Plan & Types
apps/desktop/plans/done/20260103-0901-workspace-creation-streaming-ux.md, apps/desktop/src/shared/types/workspace-init.ts, apps/desktop/src/shared/types/index.ts
Adds design doc and typed progress model (WorkspaceInitStep, WorkspaceInitProgress, INIT_STEP_MESSAGES, INIT_STEP_ORDER, helpers).
Init Manager
apps/desktop/src/main/lib/workspace-init-manager.ts
New in-memory WorkspaceInitManager singleton: job lifecycle, EventEmitter progress broadcasting, cancellation flags, per-project mutexes, wait/cleanup primitives.
Workspaces Router
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
workspaces.create fast-path + background initializeWorkspaceWorktree (sync/verify/fetch/create_worktree/copy_config/finalize), per-project locking, sanitized git handling, DB updates, and new tRPC: onInitProgress, retryInit, getInitProgress, getSetupCommands.
Usability Guards
apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts
New checkWorkspaceUsability / assertWorkspaceUsable and helpers; maps init state to TRPC errors (initializing, failed, path_missing) and embeds progress in error causes.
Git Helpers
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
Richer git error categorization, ExecFileException guard, BranchExistsResult return shape, branchExistsOnRemote → status-based, refExistsLocally, sanitizeGitError; GitHub PR status adapted to new shape.
Terminal Guard
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
createOrAttach now calls assertWorkspaceUsable for worktree workspaces to prevent terminal attach while initializing/failed.
Renderer Store & Hooks
apps/desktop/src/renderer/stores/workspace-init.ts, apps/desktop/src/renderer/stores/index.ts
New Zustand store for per-workspace initProgress and pendingTerminalSetups; exports useWorkspaceInitProgress, useIsWorkspaceInitializing, useHasWorkspaceFailed, and actions to add/remove pending setups.
Creation UX & Effects
apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx, apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx
Modal closes immediately after create; creation enqueues pending terminal setup in store; WorkspaceInitEffects (new renderless component) processes pending setups when workspace becomes ready (creates terminals, handles toasts, fetches fallback setup commands).
Workspace View & UI
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/*, apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx, apps/desktop/src/renderer/screens/main/index.tsx
Top-bar shows init/loading/failure indicators; WorkspaceInitializingView added for stepwise progress with retry/delete flows; WorkspaceView conditionally renders init view; main subscribes to onInitProgress and updates store and queries.
Misc / Cleanup
apps/desktop/src/lib/electron-app/factories/app/setup.ts, apps/desktop/src/lib/electron-app/factories/windows/create.ts, apps/desktop/src/lib/trpc/routers/auth/index.ts
Removed some console.error wrappers around external URL open and auth error logging (reduced local logging).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Modal as NewWorkspaceModal
    participant API as tRPC Workspaces
    participant InitMgr as WorkspaceInitManager
    participant Git as Git/FS
    participant Store as Renderer Store
    participant Effects as WorkspaceInitEffects
    participant TerminalAPI as tRPC Terminal
    participant UI as Workspace UI

    User->>Modal: submit create
    Modal->>API: workspaces.create()
    API->>API: create DB record (fast-path), mark initializing
    API-->>Modal: return workspaceId
    Modal->>UI: close modal, show background toast

    par Background initialization
        API->>InitMgr: startJob(workspaceId)
        InitMgr->>InitMgr: acquire project lock
        loop init steps
            API->>Git: perform step (sync/verify/fetch/create/copy/finalize)
            API->>InitMgr: updateProgress(step)
            InitMgr->>Store: emit progress event (via trpc subscription)
            Store->>UI: hooks trigger re-render
        end
        alt success
            InitMgr->>Store: emit ready
        else failure
            InitMgr->>Store: emit failed (with error)
        end
        InitMgr->>InitMgr: release lock, finalize
    end

    par When ready -> terminal creation
        Effects->>Store: detect ready & pendingTerminalSetup
        Effects->>TerminalAPI: createOrAttach (TerminalAPI calls assertWorkspaceUsable)
        TerminalAPI-->>Effects: terminal created
        Effects->>Store: clear pending
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped behind the scenes to weave a quiet thread,

Workspaces wake stepwise now, no blocking on my bed.
I stream the steps and mind the locks, then nudge the terminal door,
A tiny hop, a cheerful blink — async wonder, less folklore.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding streaming workspace creation UX to the desktop app.
Description check ✅ Passed The PR description is comprehensive, covering summary, context, implementation details, QA results, design decisions, limitations, and file changes.

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3123b7 and ce76b95.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • apps/desktop/src/lib/electron-app/factories/app/setup.ts
  • apps/desktop/src/lib/electron-app/factories/windows/create.ts
  • apps/desktop/src/lib/trpc/routers/auth/index.ts

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

❤️ Share

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

@andreasasprou
andreasasprou force-pushed the improve-workspace-creation-ux branch 2 times, most recently from 9f51974 to eec1e1f Compare January 3, 2026 08:25
andreasasprou added a commit to andreasasprou/superset that referenced this pull request Jan 3, 2026
Includes:
- feat(desktop): streaming workspace creation UX
- feat: add agent commands for plan creation and PR workflow
- Multiple fixes for race conditions and error handling

Conflicts resolved:
- workspaces.ts: merged imports (observable, workspaceInitManager, getActiveTerminalManager)
- stores/index.ts: added both workspace-init and workspace-sidebar-state exports
- main/index.tsx: merged imports for both workspace sidebar and init effects
- WorkspaceView/index.tsx: merged imports for view mode and workspace init hooks
- WorkspaceItem.tsx: combined StatusIndicator with init state indicators
Add create-plan.md and create-pr.md commands that standardize how
agents create execution plans and pull requests in this repo.
- Add Design Standards section linking to AGENTS.md for best practices
- Add Common Failure Modes section with anti-patterns to avoid
- Add Milestones section for breaking complex work into verifiable chunks
- Add Prototyping & Spike Milestones for de-risking unknowns
- Add Parallel Implementations guidance for safe refactoring
Remove verbose duplication of AGENTS.md rules. The planning phase
just needs to ensure alignment; implementation details are referenced
during implementation.
Let the agent read AGENTS.md directly instead of maintaining
a separate list that can drift out of sync.
Transform blocking workspace creation (5-15s) into a responsive,
non-blocking experience with real-time progress streaming:

Backend:
- WorkspaceInitManager class with per-project mutex, cancellation support
- tRPC subscription (onInitProgress) for progress streaming
- Usability guards preventing operations on initializing workspaces
- Best-effort cleanup on failure after worktree creation

Frontend:
- Zustand store for init progress tracking
- WorkspaceItem loading indicators (spinner, status dots)
- WorkspaceInitializingView with step-by-step progress display
- Deferred terminal creation (waits for workspace ready)
- Instant modal close with background initialization

Known limitation: App restart during init leaves workspace in
incomplete state requiring manual cleanup (documented in plan).
P0: Fix race condition where delete could clear job before init sees cancellation
- Add durable cancellations Set that persists even after job is cleared
- Add isCancellationRequested() for race-safe cancellation checks
- Add done promise tracking with finalizeJob()/waitForInit() for coordination
- Delete now waits for init to complete before git cleanup
- Delete acquires project lock before git operations
- Move clearJob() to end of delete (after all cleanup)

P2: Fix terminal setup race condition
- Remove 1500ms auto-clear from Zustand store (was racing React effects)
- Add 5-minute TTL for memory hygiene instead
- Check progress synchronously in onSuccess to handle fast init
- Explicitly clear progress after terminal setup

P2: Add delete confirmation dialog to failed workspace view
P0: Move pendingTerminalSetups to global Zustand store
- Create WorkspaceInitEffects component (always mounted in MainScreen)
- Terminal setup survives dialog unmount (InitGitDialog, CloneRepoDialog)
- Simplify useCreateWorkspace to just add to global store

P1: Auto-update baseBranch after remote refresh
- Track baseBranchWasExplicit flag through init flow
- Update worktree.baseBranch when auto-derived and remote differs
- Retry now uses correct baseBranch from worktree record
P1 fixes:
- Fix memory leak: clean up donePromises/doneResolvers in finalizeJob
- Fix init gating: detect incomplete init via worktree.gitStatus === null
  - Handles app restart during init with new 'interrupted' UI state

P2 fixes:
- Fix terminal setup error handling: move removePendingTerminalSetup to
  onSuccess callback, show toast with retry action on failure
- Fix offline detection: branchExistsOnRemote now returns discriminated
  union (exists/not_found/error) to distinguish network errors from
  branch not found. Allows graceful fallback to local refs when offline.
…dling

- Add refExistsLocally() utility with --verify --quiet for local ref checking
- Add sanitizeGitError() to clean git error messages for UI display
- Improve remote-unavailable fallback: try origin/<branch> locally first, then local branch, then fail with clear error
- Apply same fallback logic when hasRemote === false (was using raw branch name)
- Surface network/auth errors to user via progress updates instead of just logging
- Normalize gitStatus to null in getActive response to ensure consistent detection
- Use explicit === null || === undefined check in WorkspaceView for incomplete init
- Add getSetupCommands endpoint to fetch initialCommands from project config
- Update WorkspaceInitEffects to fetch commands when ready but no pending setup
- Fixes: retry after failure now runs setup commands
- Fixes: app restart during init now runs setup commands when ready
- Pending setup data loss is no longer fatal - we fetch on demand as fallback
@andreasasprou
andreasasprou force-pushed the improve-workspace-creation-ux branch from 1a82b16 to 28116d4 Compare January 4, 2026 07:24
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@andreasasprou
andreasasprou marked this pull request as ready for review January 4, 2026 09:17

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (7)
apps/desktop/src/main/lib/workspace-init-manager.ts (3)

264-279: Potential race in lock acquisition loop.

The while loop checks projectLocks.has(projectId) then awaits projectLocks.get(projectId). If releaseProjectLock is called between these operations by another async context, get() returns undefined and await undefined resolves immediately, which is safe. However, if two callers enter the loop simultaneously after the lock is released, both could pass the while check before either sets the new lock.

Consider using a more robust mutex pattern:

🔎 Suggested improvement
 async acquireProjectLock(projectId: string): Promise<void> {
-	// Wait for any existing lock to be released
-	while (this.projectLocks.has(projectId)) {
-		await this.projectLocks.get(projectId);
-	}
+	// Wait for any existing lock to be released
+	while (this.projectLocks.has(projectId)) {
+		const existingLock = this.projectLocks.get(projectId);
+		if (existingLock) {
+			await existingLock;
+		}
+		// Re-check in case another caller acquired the lock
+	}

 	// Create a new lock
 	let resolve: () => void;

The current implementation should work in practice since JavaScript is single-threaded and the race window is minimal, but the explicit re-check after await makes the intent clearer.


134-141: Consider extracting the cleanup delay as a named constant.

The 2000ms timeout is a magic number. Per coding guidelines, extract hardcoded values to named constants at module top.

🔎 Suggested fix
+const READY_STATE_CLEANUP_DELAY_MS = 2000;
+
 class WorkspaceInitManager extends EventEmitter {
   // ...
   
   updateProgress(/* ... */) {
     // ...
     if (step === "ready") {
       setTimeout(() => {
         if (this.jobs.get(workspaceId)?.progress.step === "ready") {
           this.jobs.delete(workspaceId);
         }
-      }, 2000);
+      }, READY_STATE_CLEANUP_DELAY_MS);
     }
   }

235-257: Consider extracting the default timeout as a named constant.

The 30000ms default timeout is embedded in the function signature. Extract to a module-level constant for clarity and easier adjustment.

🔎 Suggested fix
+const DEFAULT_WAIT_FOR_INIT_TIMEOUT_MS = 30000;
+
 // In the class:
-async waitForInit(workspaceId: string, timeoutMs = 30000): Promise<void> {
+async waitForInit(workspaceId: string, timeoutMs = DEFAULT_WAIT_FOR_INIT_TIMEOUT_MS): Promise<void> {
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx (2)

127-163: Consider extracting the delete confirmation dialog to reduce duplication.

The AlertDialog for delete confirmation is duplicated between the interrupted state (lines 127-163) and the failed state (lines 220-256). The only difference is the description text.

🔎 Suggested refactor

Extract to a separate file per the "one component per file" guideline:

// DeleteWorkspaceConfirmDialog.tsx
interface DeleteWorkspaceConfirmDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  workspaceName: string;
  description: string;
  onConfirm: () => void;
}

export function DeleteWorkspaceConfirmDialog({
  open,
  onOpenChange,
  workspaceName,
  description,
  onConfirm,
}: DeleteWorkspaceConfirmDialogProps) {
  return (
    <AlertDialog open={open} onOpenChange={onOpenChange}>
      {/* ... dialog content with props */}
    </AlertDialog>
  );
}

Then use it in both places with appropriate description text.

Also applies to: 220-256


51-72: Consider handling mutation errors.

The handleRetry and handleDelete functions don't include onError callbacks. While tRPC may handle errors globally, explicit error handling with toast feedback would improve UX consistency with the rest of the PR.

🔎 Suggested improvement
 const handleRetry = () => {
   retryMutation.mutate(
     { workspaceId },
     {
       onSuccess: () => {
         utils.workspaces.invalidate();
       },
+      onError: (error) => {
+        toast.error(
+          error instanceof Error ? error.message : "Failed to retry initialization"
+        );
+      },
     },
   );
 };

 const handleDelete = () => {
   setShowDeleteConfirm(false);
   deleteMutation.mutate(
     { id: workspaceId },
     {
       onSuccess: () => {
         utils.workspaces.invalidate();
       },
+      onError: (error) => {
+        toast.error(
+          error instanceof Error ? error.message : "Failed to delete workspace"
+        );
+      },
     },
   );
 };
apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx (1)

163-193: Consider cleanup for async operations.

The async fetch operation (utils.workspaces.getSetupCommands.fetch) creates a floating promise without cleanup handling. While this component is mounted at the app root and unlikely to unmount during normal operation, adding cleanup would be more robust:

🔎 Optional improvement for robustness
 useEffect(() => {
+  let mounted = true;
   // ... existing code for pending setups loop ...

   // Handle workspaces that became ready without pending setup data
   for (const [workspaceId, progress] of Object.entries(initProgress)) {
     // ... existing checks ...

     processingRef.current.add(workspaceId);

     utils.workspaces.getSetupCommands
       .fetch({ workspaceId })
       .then((setupData) => {
+        if (!mounted) return;
         if (!setupData) {
           clearProgress(workspaceId);
           processingRef.current.delete(workspaceId);
           return;
         }
         // ... rest of handler ...
       })
       .catch((error) => {
+        if (!mounted) return;
         console.error(
           "[WorkspaceInitEffects] Failed to fetch setup commands:",
           error,
         );
         clearProgress(workspaceId);
         processingRef.current.delete(workspaceId);
       });
   }
+
+  return () => {
+    mounted = false;
+  };
 }, [/* deps */]);
apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts (1)

104-122: Consider type-safe error cause handling.

The as cast for error.cause works but could be made type-safe with a dedicated interface:

🔎 Optional type-safe improvement
+interface WorkspaceUsabilityErrorCause {
+  reason: WorkspaceUsabilityReason;
+  progress?: WorkspaceInitProgress;
+}
+
+function isWorkspaceUsabilityErrorCause(
+  cause: unknown
+): cause is WorkspaceUsabilityErrorCause {
+  return (
+    typeof cause === "object" &&
+    cause !== null &&
+    "reason" in cause &&
+    typeof (cause as { reason: unknown }).reason === "string"
+  );
+}
+
 export function isInitializingError(error: unknown): boolean {
   if (error instanceof TRPCError) {
-    const cause = error.cause as { reason?: string } | undefined;
-    return cause?.reason === "initializing";
+    return (
+      isWorkspaceUsabilityErrorCause(error.cause) &&
+      error.cause.reason === "initializing"
+    );
   }
   return false;
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba03ea4 and c584bd7.

📒 Files selected for processing (19)
  • apps/desktop/plans/done/20260103-0901-workspace-creation-streaming-ux.md
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/lib/workspace-init-manager.ts
  • apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx
  • apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/screens/main/index.tsx
  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/stores/workspace-init.ts
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/shared/types/workspace-init.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
  • apps/desktop/src/renderer/stores/workspace-init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/workspace-init-manager.ts
  • apps/desktop/src/shared/types/workspace-init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/renderer/screens/main/index.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
  • apps/desktop/src/renderer/stores/workspace-init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/workspace-init-manager.ts
  • apps/desktop/src/shared/types/workspace-init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/renderer/screens/main/index.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts
  • apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
  • apps/desktop/src/renderer/stores/workspace-init.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/index.ts
  • apps/desktop/src/renderer/screens/main/index.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
  • apps/desktop/src/renderer/stores/workspace-init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/workspace-init-manager.ts
  • apps/desktop/src/shared/types/workspace-init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/renderer/screens/main/index.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
  • apps/desktop/src/renderer/stores/workspace-init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/workspace-init-manager.ts
  • apps/desktop/src/shared/types/workspace-init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/renderer/screens/main/index.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
  • apps/desktop/src/renderer/screens/main/index.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx
🧠 Learnings (7)
📚 Learning: 2026-01-02T06:50:28.662Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.662Z
Learning: Applies to apps/desktop/src/renderer/**/*.{ts,tsx} : Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Applied to files:

  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/shared/types/index.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : Use alias as defined in `tsconfig.json` when possible

Applied to files:

  • apps/desktop/src/shared/types/index.ts
📚 Learning: 2026-01-02T06:50:28.662Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.662Z
Learning: Applies to apps/desktop/src/main/index.ts : Load environment variables from monorepo root .env in desktop app with override: true before any imports in src/main/index.ts and electron.vite.config.ts

Applied to files:

  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
📚 Learning: 2026-01-02T06:50:28.662Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.662Z
Learning: Applies to apps/desktop/src/lib/electron-router-dom.ts : Do not import Node.js modules like node:path or dotenv in electron-router-dom.ts and similar shared files - they run in both main and renderer processes

Applied to files:

  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.

Applied to files:

  • apps/desktop/src/renderer/stores/workspace-init.ts
  • apps/desktop/src/renderer/screens/main/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx
📚 Learning: 2026-01-02T06:50:28.662Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.662Z
Learning: Applies to apps/api/**/*.{ts,tsx} : Use TRPCError with appropriate error codes: NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_IMPLEMENTED

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from `trpc/server/observable` instead of async generators, as the library explicitly checks `isObservable(result)` and throws an error otherwise

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
🧬 Code graph analysis (9)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx (3)
apps/desktop/src/main/lib/workspace-init-manager.ts (2)
  • isInitializing (38-45)
  • hasFailed (50-53)
apps/desktop/src/renderer/stores/workspace-init.ts (2)
  • useIsWorkspaceInitializing (88-96)
  • useHasWorkspaceFailed (98-102)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx (1)
  • WorkspaceInitializingView (38-328)
apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts (2)
apps/desktop/src/renderer/react-query/workspaces/index.ts (1)
  • useCreateWorkspace (3-3)
apps/desktop/src/renderer/stores/workspace-init.ts (1)
  • useWorkspaceInitStore (29-83)
apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx (1)
packages/ui/src/components/ui/sonner.tsx (1)
  • toast (40-40)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx (4)
apps/desktop/src/shared/types/workspace-init.ts (4)
  • WorkspaceInitStep (6-15)
  • INIT_STEP_ORDER (41-50)
  • isStepComplete (64-72)
  • INIT_STEP_MESSAGES (25-35)
apps/desktop/src/renderer/stores/workspace-init.ts (2)
  • useWorkspaceInitProgress (85-86)
  • useHasWorkspaceFailed (98-102)
apps/desktop/src/main/lib/workspace-init-manager.ts (1)
  • hasFailed (50-53)
packages/ui/src/lib/utils.ts (1)
  • cn (4-6)
apps/desktop/src/renderer/stores/workspace-init.ts (2)
apps/desktop/src/shared/types/workspace-init.ts (1)
  • WorkspaceInitProgress (17-23)
apps/cli/src/lib/storage/lowdb-adapter.ts (1)
  • set (162-172)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (1)
apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts (1)
  • assertWorkspaceUsable (65-98)
apps/desktop/src/main/lib/workspace-init-manager.ts (1)
apps/desktop/src/shared/types/workspace-init.ts (2)
  • WorkspaceInitProgress (17-23)
  • WorkspaceInitStep (6-15)
apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts (2)
apps/desktop/src/shared/types/workspace-init.ts (1)
  • WorkspaceInitProgress (17-23)
apps/desktop/src/main/lib/workspace-init-manager.ts (1)
  • workspaceInitManager (302-302)
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx (2)
apps/desktop/src/main/lib/workspace-init-manager.ts (2)
  • isInitializing (38-45)
  • hasFailed (50-53)
apps/desktop/src/renderer/stores/workspace-init.ts (3)
  • useIsWorkspaceInitializing (88-96)
  • useHasWorkspaceFailed (98-102)
  • useWorkspaceInitProgress (85-86)
🪛 LanguageTool
apps/desktop/plans/done/20260103-0901-workspace-creation-streaming-ux.md

[grammar] ~365-~365: Ensure spelling is correct
Context: ...on, workspace creation returns in under 100ms while initialization continues asynchro...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~588-~588: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...t-effort cleanup attempts to remove it. If cleanup fails, the error message includ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~725-~725: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...t operations to prevent lock conflicts. Added cancellation support with isCancelled()...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~725-~725: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...isCancelled() checks between each step. Added best-effort cleanup on failure after wo...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🔇 Additional comments (35)
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts (1)

47-59: LGTM!

The adaptation to the new BranchExistsResult discriminated union is correct. The comment clearly explains that both "not_found" and "error" statuses are treated as "branch doesn't exist" for the purposes of GitHub status display, which is appropriate given that this function already returns null on errors anyway.

apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts (4)

416-419: LGTM!

Well-designed discriminated union type that enables callers to differentiate between "branch doesn't exist" and "couldn't check due to error" scenarios. This provides richer context for error handling in the UI.


436-488: LGTM!

Comprehensive error classification that properly distinguishes:

  • not_found: Branch genuinely doesn't exist (exit code 2 or "could not find remote ref")
  • error: Network/connectivity issues, authentication failures, or unknown errors

The user-friendly error messages will help users troubleshoot issues directly from the UI.


719-732: LGTM!

Clean implementation using --verify --quiet to check local ref existence without network access. The ^{commit} suffix ensures the ref resolves to an actual commit object.


740-746: LGTM!

Simple and effective sanitization for git error messages. The regex chain handles the common prefixes and normalizes whitespace for cleaner UI display.

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

8-8: LGTM!

The new re-export follows the existing barrel file pattern and exposes workspace initialization types to consumers.

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

1-1: LGTM!

Clean barrel export for the new component, following the standard component organization pattern.

apps/desktop/src/renderer/stores/index.ts (1)

9-9: LGTM!

The new re-export follows the existing pattern and exposes the workspace initialization Zustand store to consumers.

apps/desktop/src/renderer/screens/main/index.tsx (2)

59-70: LGTM!

The subscription pattern is correctly implemented:

  • Uses Zustand store for state management (per coding guidelines)
  • Updates progress on every event
  • Invalidates relevant workspace queries when initialization completes or fails, ensuring the UI reflects the latest state

361-361: LGTM!

WorkspaceInitEffects is correctly placed at the app root level, outside the conditional content rendering. This ensures terminal setup effects persist even when views change or dialogs unmount.

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

62-68: LGTM!

The guard correctly prevents terminal creation for worktree workspaces that are still initializing or have failed. The comment clearly explains why branch workspaces are exempt (they use the main repo path which always exists). This ensures a clean user experience by failing fast with a meaningful error rather than letting terminal creation fail later due to missing paths.

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

22-46: LGTM! Clean separation of concerns.

The hook correctly delegates terminal setup to WorkspaceInitEffects via the global Zustand store, which survives dialog unmounts. The race condition handling documentation is clear and the implementation correctly relies on WorkspaceInitEffects to reconcile pending setups with progress state.

apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx (1)

164-194: LGTM! Clean async flow with immediate modal close.

The try/catch pattern provides good error handling, and the conditional toast messaging gives appropriate feedback based on whether async initialization is in progress. Closing the modal immediately improves perceived responsiveness.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx (2)

18-33: LGTM! Robust incomplete init detection.

The logic correctly handles three scenarios:

  1. Active initialization (in-memory progress exists)
  2. Failed initialization
  3. Interrupted initialization (app restart during init - detected via gitStatus === null/undefined on worktree workspaces)

The explicit null/undefined check avoids lint issues while correctly identifying workspaces that never completed setup.


180-188: LGTM! Conditional rendering logic is correct.

The isInterrupted prop correctly evaluates to true only when there's incomplete init (hasIncompleteInit) AND no active initialization in progress (!isInitializing), ensuring the interrupted UI is shown only for post-restart recovery scenarios.

apps/desktop/src/main/lib/workspace-init-manager.ts (1)

25-33: LGTM! Clean in-memory state management design.

The separation of concerns is well-done:

  • jobs for active job state
  • projectLocks/projectLockResolvers for mutex coordination
  • donePromises/doneResolvers for completion waiting
  • cancellations Set for durable cancellation signals

The EventEmitter pattern is appropriate for streaming progress to subscribers.

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

67-70: LGTM! Clean integration with workspace init store.

The hooks are correctly used to derive UI state from the global Zustand store. This ensures consistent state across all components consuming initialization progress.


281-338: LGTM! Clear visual feedback for initialization states.

The status indicators provide good UX:

  • Spinning loader icon during initialization
  • Red indicator with error tooltip on failure
  • Blue pulsing indicator with progress message during init
  • Existing needsAttention indicator preserved as fallback

The tooltip content provides helpful context for each state.


343-374: LGTM! Proper guard against delete during initialization.

Disabling the delete button and updating the tooltip text during initialization prevents user confusion and potential race conditions with the init process.

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

261-327: LGTM! Clean progress visualization.

The step list with dynamic icons (check/spinner/circle) and styling provides clear feedback on initialization progress. The DISPLAY_STEPS filter correctly excludes pending and ready from the UI, and the helper text sets appropriate user expectations.

apps/desktop/src/shared/types/workspace-init.ts (1)

1-72: LGTM! Well-designed shared types for initialization progress.

The type definitions are comprehensive and well-documented:

  • WorkspaceInitStep covers all states including the terminal failed state
  • INIT_STEP_ORDER correctly excludes failed since it's not part of the normal flow
  • getStepIndex and isStepComplete handle the failed edge case appropriately
  • INIT_STEP_MESSAGES provides user-friendly strings for all steps

The shared location ensures consistency between main process, renderer, and UI components.

apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx (2)

1-42: LGTM - Clean setup following project conventions.

The imports are well-organized, using Zustand for state management as per coding guidelines. The component correctly uses tRPC for mutations and queries.


107-113: Verify createOrAttach mutation stability in dependency array.

The createOrAttach mutation object is included in the dependency array. While tRPC's useMutation typically returns a stable reference, including the entire mutation object (rather than just createOrAttach.mutate) could cause unnecessary re-renders if the mutation state changes (loading, error states).

Consider using createOrAttach.mutate directly if stability issues arise, though the current approach should work correctly.

apps/desktop/src/lib/trpc/routers/workspaces/utils/usability.ts (3)

6-16: LGTM - Clean type definitions.

The WorkspaceUsabilityReason union type and WorkspaceUsabilityCheck interface provide clear, discriminated types for handling different workspace states.


27-56: LGTM - Well-structured usability check.

The check order is logical: initialization state is checked before filesystem access, and the function returns detailed information for UI rendering. Using existsSync is acceptable here given the quick operation and main process context.


65-98: LGTM - Proper TRPCError usage.

Error codes are appropriate: PRECONDITION_FAILED for state issues, NOT_FOUND for missing paths. The cause object provides frontend-consumable details for UI rendering. Based on learnings, this follows the recommended TRPCError patterns.

apps/desktop/src/renderer/stores/workspace-init.ts (1)

85-102: LGTM - Clean convenience hooks.

The selector hooks are focused and efficient. The logic correctly identifies initializing state (any step except ready/failed) and failed state.

apps/desktop/plans/done/20260103-0901-workspace-creation-streaming-ux.md (1)

1-728: LGTM - Comprehensive planning documentation.

This plan document provides excellent context for the streaming workspace creation UX implementation. The decision log, known limitations, and architecture notes will be valuable for future maintainers. The minor style issues flagged by static analysis (successive sentences starting with the same word) are acceptable in technical documentation.

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

10-17: LGTM - Appropriate imports for streaming workspace creation.

The imports correctly use observable from @trpc/server/observable as required by coding guidelines for tRPC subscriptions with trpc-electron. The WorkspaceInitProgress type import from shared types ensures type consistency.


45-70: LGTM - Well-documented function with proper object parameter pattern.

The function signature follows coding guidelines by using an object parameter for the 8 parameters. The JSDoc clearly documents the "does not throw" contract, which is enforced by the try/catch structure.


306-338: LGTM - Robust error handling and cleanup.

The error handling properly:

  1. Logs errors with context prefix [workspace-init]
  2. Attempts best-effort cleanup of partial worktrees
  3. Updates progress to "failed" state with error message
  4. Always finalizes the job and releases the project lock in finally

This ensures no orphaned locks or stuck init states.


454-477: LGTM - Clean fast-path implementation.

The create mutation correctly:

  1. Starts the background job via workspaceInitManager.startJob()
  2. Fires off initializeWorkspaceWorktree() without awaiting
  3. Returns immediately with isInitializing: true

This achieves the "instant modal close" UX goal while initialization continues asynchronously.


1111-1120: LGTM - Proper cancellation handling in delete.

The delete mutation correctly waits for any ongoing initialization to complete before proceeding. The 30-second timeout is reasonable, and cancellation ensures the init loop will exit cleanly.


1681-1714: LGTM - Correct observable subscription pattern.

The onInitProgress subscription correctly uses the observable pattern from @trpc/server/observable as required by coding guidelines for trpc-electron. The implementation:

  1. Sends current state for all matching workspaces on subscribe
  2. Attaches event listener for live updates
  3. Returns cleanup function to remove listener

Based on learnings, this follows the required pattern that trpc-electron explicitly checks for.


1720-1776: LGTM - Well-designed retry mechanism.

The retryInit mutation:

  1. Validates all required entities exist
  2. Clears the failed state before restarting
  3. Sets baseBranchWasExplicit: true to prevent further auto-updates (good reasoning in comment)
  4. Uses appropriate fallback chain for baseBranch

The design correctly reuses the existing worktree record rather than recreating it.

Comment on lines +43 to +55
// For memory hygiene, clear "ready" progress after 5 minutes
// (long enough that WorkspaceInitEffects will have processed it)
if (progress.step === "ready") {
setTimeout(
() => {
const current = get().initProgress[progress.workspaceId];
if (current?.step === "ready") {
get().clearProgress(progress.workspaceId);
}
},
5 * 60 * 1000,
); // 5 minutes
}

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

Potential memory leak: setTimeout cleanup timer not cancelled.

When updateProgress is called with step === "ready", a 5-minute cleanup timer is set. However, if clearProgress is called before the timer fires (e.g., during normal cleanup flow), the timer still runs and calls clearProgress again unnecessarily. Additionally, if updateProgress is called multiple times with "ready" for the same workspace, multiple timers are created.

Consider tracking and cancelling timers:

🔎 Proposed fix
+// Track cleanup timers to prevent memory leaks
+const cleanupTimers = new Map<string, NodeJS.Timeout>();
+
 export const useWorkspaceInitStore = create<WorkspaceInitState>()(
   devtools(
     (set, get) => ({
       initProgress: {},
       pendingTerminalSetups: {},

       updateProgress: (progress) => {
         set((state) => ({
           initProgress: {
             ...state.initProgress,
             [progress.workspaceId]: progress,
           },
         }));

         // For memory hygiene, clear "ready" progress after 5 minutes
         if (progress.step === "ready") {
+          // Cancel any existing timer for this workspace
+          const existingTimer = cleanupTimers.get(progress.workspaceId);
+          if (existingTimer) {
+            clearTimeout(existingTimer);
+          }
-          setTimeout(
+          const timer = setTimeout(
             () => {
               const current = get().initProgress[progress.workspaceId];
               if (current?.step === "ready") {
                 get().clearProgress(progress.workspaceId);
               }
+              cleanupTimers.delete(progress.workspaceId);
             },
             5 * 60 * 1000,
           );
+          cleanupTimers.set(progress.workspaceId, timer);
         }
       },

       clearProgress: (workspaceId) => {
+        // Cancel cleanup timer if exists
+        const timer = cleanupTimers.get(workspaceId);
+        if (timer) {
+          clearTimeout(timer);
+          cleanupTimers.delete(workspaceId);
+        }
         set((state) => {
           const { [workspaceId]: _, ...rest } = state.initProgress;
           return { initProgress: rest };
         });
       },

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

🤖 Prompt for AI Agents
In apps/desktop/src/renderer/stores/workspace-init.ts around lines 43–55, the
setTimeout used to clear a "ready" progress can leak and produce duplicate
timers; before creating a new 5-minute timer, cancel any existing timer for the
same workspaceId and store the new timer id in a Map (or an object keyed by
workspaceId); update clearProgress to clearTimeout the stored timer and remove
it from the Map when invoked; use clearTimeout (or window.clearTimeout) to
cancel timers and ensure timer ids are properly typed/normalized for the
runtime.

… of string matching

- Use Node's execFileAsync directly to get reliable exit codes
- Add ExecFileException type guard for safe error property access
- Handle spawn errors (ENOENT, ETIMEDOUT) separately from git exit codes
- Handle process kill/timeout signals explicitly
- Use stderr for error categorization (more reliable than error.message)
- Add patterns for SSH auth failures and remote-not-configured errors
- Trust exit code 0 = branch exists without checking stdout

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx (1)

322-324: Extract hardcoded duration message to a constant.

The helper text about expected duration is hardcoded. Consider extracting it to a named constant at the module top for easier maintenance and potential future localization.

🔎 Suggested change

Add near the top of the file (after DISPLAY_STEPS):

 const DISPLAY_STEPS: WorkspaceInitStep[] = INIT_STEP_ORDER.filter(
   (step) => step !== "pending" && step !== "ready",
 );
+
+const INIT_DURATION_MESSAGE =
+  "Takes 10s to a few minutes depending on the size of your repo";

Then use it:

 {/* Helper text */}
-<p className="text-xs text-muted-foreground/60">
-  Takes 10s to a few minutes depending on the size of your repo
-</p>
+<p className="text-xs text-muted-foreground/60">
+  {INIT_DURATION_MESSAGE}
+</p>

Based on coding guidelines, "Extract magic numbers and hardcoded values to named constants at module top."

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c248ffa and c3123b7.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
🧰 Additional context used
📓 Path-based instructions (6)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
🧬 Code graph analysis (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx (5)
apps/desktop/src/shared/types/workspace-init.ts (4)
  • WorkspaceInitStep (6-15)
  • INIT_STEP_ORDER (41-50)
  • isStepComplete (64-72)
  • INIT_STEP_MESSAGES (25-35)
apps/desktop/src/renderer/stores/workspace-init.ts (2)
  • useWorkspaceInitProgress (85-86)
  • useHasWorkspaceFailed (98-102)
packages/ui/src/components/ui/button.tsx (1)
  • Button (60-60)
packages/ui/src/components/ui/alert-dialog.tsx (6)
  • AlertDialog (146-146)
  • AlertDialogContent (150-150)
  • AlertDialogHeader (151-151)
  • AlertDialogTitle (153-153)
  • AlertDialogDescription (154-154)
  • AlertDialogFooter (152-152)
packages/ui/src/lib/utils.ts (1)
  • cn (4-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx (1)

1-36: LGTM!

Imports are well-organized, the interface is clearly defined, and the DISPLAY_STEPS constant appropriately filters the step list for UI display.

Comment on lines +51 to +72
const handleRetry = () => {
retryMutation.mutate(
{ workspaceId },
{
onSuccess: () => {
utils.workspaces.invalidate();
},
},
);
};

const handleDelete = () => {
setShowDeleteConfirm(false);
deleteMutation.mutate(
{ id: workspaceId },
{
onSuccess: () => {
utils.workspaces.invalidate();
},
},
);
};

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

Overly broad query invalidation and missing error handling.

Two issues:

  1. Overly broad invalidation: utils.workspaces.invalidate() invalidates all workspace queries. Consider invalidating only the specific workspace or using more targeted invalidation like utils.workspaces.get.invalidate() or utils.workspaces.list.invalidate().

  2. Missing error handling: Both mutations lack onError callbacks. If a mutation fails, users won't receive feedback about what went wrong.

🔎 Suggested improvements
 const handleRetry = () => {
   retryMutation.mutate(
     { workspaceId },
     {
       onSuccess: () => {
-        utils.workspaces.invalidate();
+        utils.workspaces.list.invalidate();
+        utils.workspaces.get.invalidate({ id: workspaceId });
       },
+      onError: (error) => {
+        console.error('[WorkspaceInitializingView/retry] Retry failed:', error);
+        // Consider showing a toast notification here
+      },
     },
   );
 };

 const handleDelete = () => {
   setShowDeleteConfirm(false);
   deleteMutation.mutate(
     { id: workspaceId },
     {
       onSuccess: () => {
-        utils.workspaces.invalidate();
+        utils.workspaces.list.invalidate();
       },
+      onError: (error) => {
+        console.error('[WorkspaceInitializingView/delete] Delete failed:', error);
+        // Consider showing a toast notification here
+      },
     },
   );
 };
🤖 Prompt for AI Agents
In
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
around lines 51-72, the code currently calls utils.workspaces.invalidate() after
mutations (overly broad) and omits error handling; change the invalidation to be
targeted (e.g., invalidate the specific workspace query and/or the workspace
list such as utils.workspaces.get.invalidate({ id: workspaceId }) and
utils.workspaces.list.invalidate() as appropriate) and add onError callbacks to
both retryMutation.mutate and deleteMutation.mutate that surface the error to
the user (e.g., set an error state or call a toast/notification) so failures are
handled and user-visible.

Comment on lines +77 to +259
if (isInterrupted && !progress) {
return (
<>
<div className="flex flex-col items-center justify-center h-full px-8">
<div className="flex flex-col items-center max-w-sm text-center space-y-6">
{/* Warning icon */}
<div className="flex items-center justify-center size-16 rounded-full bg-yellow-500/10">
<HiExclamationTriangle className="size-8 text-yellow-500" />
</div>

{/* Title and description */}
<div className="space-y-2">
<h2 className="text-lg font-medium text-foreground">
Setup was interrupted
</h2>
<p className="text-sm text-muted-foreground">{workspaceName}</p>
<p className="text-xs text-muted-foreground/80 mt-2">
The app was closed before workspace setup completed. You can
retry the setup or delete this workspace.
</p>
</div>

{/* Action buttons */}
<div className="flex gap-3">
<Button
variant="outline"
size="sm"
onClick={() => setShowDeleteConfirm(true)}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Deleting..." : "Delete Workspace"}
</Button>
<Button
size="sm"
onClick={handleRetry}
disabled={retryMutation.isPending}
>
{retryMutation.isPending ? (
<>
<LuLoader className="mr-2 size-4 animate-spin" />
Retrying...
</>
) : (
"Retry Setup"
)}
</Button>
</div>
</div>
</div>

{/* Delete confirmation dialog */}
<AlertDialog
open={showDeleteConfirm}
onOpenChange={setShowDeleteConfirm}
>
<AlertDialogContent className="max-w-[340px] gap-0 p-0">
<AlertDialogHeader className="px-4 pt-4 pb-2">
<AlertDialogTitle className="font-medium">
Delete workspace "{workspaceName}"?
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="text-muted-foreground">
This workspace was not fully set up. Deleting will clean up
any partial files that were created.
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="px-4 pb-4 pt-2 flex-row justify-end gap-2">
<Button
variant="ghost"
size="sm"
className="h-7 px-3 text-xs"
onClick={() => setShowDeleteConfirm(false)}
>
Cancel
</Button>
<Button
variant="destructive"
size="sm"
className="h-7 px-3 text-xs"
onClick={handleDelete}
>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

// Failed state
if (hasFailed) {
return (
<>
<div className="flex flex-col items-center justify-center h-full px-8">
<div className="flex flex-col items-center max-w-sm text-center space-y-6">
{/* Error icon */}
<div className="flex items-center justify-center size-16 rounded-full bg-destructive/10">
<HiExclamationTriangle className="size-8 text-destructive" />
</div>

{/* Title and description */}
<div className="space-y-2">
<h2 className="text-lg font-medium text-foreground">
Workspace setup failed
</h2>
<p className="text-sm text-muted-foreground">{workspaceName}</p>
{progress?.error && (
<p className="text-xs text-destructive/80 mt-2 bg-destructive/5 rounded-md px-3 py-2">
{progress.error}
</p>
)}
</div>

{/* Action buttons */}
<div className="flex gap-3">
<Button
variant="outline"
size="sm"
onClick={() => setShowDeleteConfirm(true)}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Deleting..." : "Delete Workspace"}
</Button>
<Button
size="sm"
onClick={handleRetry}
disabled={retryMutation.isPending}
>
{retryMutation.isPending ? (
<>
<LuLoader className="mr-2 size-4 animate-spin" />
Retrying...
</>
) : (
"Retry"
)}
</Button>
</div>
</div>
</div>

{/* Delete confirmation dialog */}
<AlertDialog
open={showDeleteConfirm}
onOpenChange={setShowDeleteConfirm}
>
<AlertDialogContent className="max-w-[340px] gap-0 p-0">
<AlertDialogHeader className="px-4 pt-4 pb-2">
<AlertDialogTitle className="font-medium">
Delete workspace "{workspaceName}"?
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="text-muted-foreground">
This workspace failed to initialize. Deleting will clean up
any partial files that were created.
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="px-4 pb-4 pt-2 flex-row justify-end gap-2">
<Button
variant="ghost"
size="sm"
className="h-7 px-3 text-xs"
onClick={() => setShowDeleteConfirm(false)}
>
Cancel
</Button>
<Button
variant="destructive"
size="sm"
className="h-7 px-3 text-xs"
onClick={handleDelete}
>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

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

Significant code duplication between error states.

The interrupted state (lines 77-166) and failed state (lines 169-259) contain substantial duplication:

  • The delete confirmation dialog is identical (lines 128-163 vs 221-256)
  • The layout structure and button handling are nearly identical
  • Only the icon colors, messaging, and error display differ

This duplication makes maintenance harder and increases the bundle size.

🔎 Suggested refactor to reduce duplication

Extract a shared error state component or helper function:

// Extract to a separate helper component or use a shared render function
interface ErrorStateConfig {
  iconColor: string;
  bgColor: string;
  title: string;
  description?: string;
  deleteDialogDescription: string;
  retryLabel?: string;
}

function renderErrorState(
  config: ErrorStateConfig,
  workspaceName: string,
  handlers: {
    onRetry: () => void;
    onDelete: () => void;
    showDeleteConfirm: boolean;
    setShowDeleteConfirm: (show: boolean) => void;
    retryPending: boolean;
    deletePending: boolean;
  }
) {
  // Single implementation of the error state UI
}

// Then use it:
if (isInterrupted && !progress) {
  return renderErrorState(
    {
      iconColor: "text-yellow-500",
      bgColor: "bg-yellow-500/10",
      title: "Setup was interrupted",
      description: "The app was closed before workspace setup completed...",
      deleteDialogDescription: "This workspace was not fully set up...",
      retryLabel: "Retry Setup",
    },
    workspaceName,
    { /* handlers */ }
  );
}

Alternatively, consider extracting the delete confirmation dialog to a separate component that can be reused.

🤖 Prompt for AI Agents
In
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView/WorkspaceInitializingView.tsx
around lines 77-259, the interrupted and failed UI branches duplicate layout,
buttons, and the delete confirmation dialog; extract a shared ErrorState
component (or shared render function) that accepts props for icon, bg color,
title, description, optional error text, retry label, and handlers (onRetry,
onDelete, showDeleteConfirm, setShowDeleteConfirm, retryPending, deletePending)
and replace both branches with calls to that component; alternatively extract
the AlertDialog delete confirmation into its own reusable component and reuse it
in both branches to remove the duplicated markup and logic.

andreasasprou added a commit to andreasasprou/superset that referenced this pull request Jan 4, 2026
@Kitenite
Kitenite merged commit f77b47d into ParadevAI:main Jan 5, 2026
10 of 13 checks passed
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.

2 participants