Skip to content

feat(desktop): add Workbench/Review mode with FileViewer panes#540

Closed
andreasasprou wants to merge 14 commits intosuperset-sh:mainfrom
andreasasprou:feat/desktop-workbench-review-mode
Closed

feat(desktop): add Workbench/Review mode with FileViewer panes#540
andreasasprou wants to merge 14 commits intosuperset-sh:mainfrom
andreasasprou:feat/desktop-workbench-review-mode

Conversation

@andreasasprou
Copy link
Copy Markdown
Contributor

@andreasasprou andreasasprou commented Dec 29, 2025

Why

Users need to view code, diffs, and docs while keeping terminals visible in the same window. The existing Changes page is great for focused review, but doesn't support in-flow work where you want terminals and file viewers side-by-side.

What / How

Introduces a workspace-level Workbench | Review view mode toggle:

  • Workbench mode: Mosaic panes layout with terminals + file viewers for in-flow work. Clicking a file in the sidebar opens a FileViewer pane next to your terminals.
  • Review mode: Dedicated Changes page for focused code review (existing behavior preserved).

Key implementation details:

  1. ViewModeToggle - Prominent segmented control in workspace header for switching modes
  2. FileViewerPane - New pane type with Raw/Rendered/Diff view modes, lock/unlock (prevents file replacement), and split support
  3. GroupStrip - Group switching strip above Mosaic content (moves group tabs out of sidebar)
  4. Unified sidebar - Full ChangesView in both modes; in Workbench, file clicks trigger onFileOpen callback to open FileViewer panes
  5. workspace-view-mode store - Per-workspace persistence via Zustand + localStorage
  6. readWorkingFile tRPC - Safe file reads with size cap (2MB) and binary detection
  7. MRU pane reuse - File clicks reuse most-recently-used unlocked FileViewer pane; locked panes are preserved
  8. ⌘+T behavior - In Review mode, switches to Workbench first, then creates terminal

Inline Editing Support

FileViewerPane now supports full editing in both Raw and Diff modes:

  • Raw mode - Monaco Editor with syntax highlighting, replacing the previous read-only div
  • Diff mode - Enabled editing on the modified side of the diff viewer
  • ⌘+S to save - Standard keyboard shortcut saves changes to disk
  • Dirty state tracking - Orange dot (●) indicator before filename when unsaved changes exist
  • Editable badge - Shows "⌘S" hint (changes to "Saving..." during save operation)
  • Auto-refresh - Queries invalidated after save to keep UI in sync

Configurable Terminal File Link Behavior

Added a global setting to control how Cmd+clicking file paths in the terminal behaves:

  • Setting location: Settings → Behavior → "Terminal file links"
  • Options:
    • "External editor" (default) - Opens file in configured external editor (existing behavior)
    • "File viewer" - Opens file in the in-app FileViewerPane
  • Implementation details:
    • Uses ref pattern to avoid terminal recreation when setting changes
    • Setting changes apply immediately without terminal restart
    • Migration auto-runs on app start (SQLite local-db)

Future Work

  • Keyboard shortcut for mode toggle - Add ⌘+\ or similar to toggle between Workbench/Review (currently mouse-only)
  • Shortcut hints in UI - Add tooltip to ViewModeToggle showing keyboard shortcut once implemented
  • Edge case hardening - Additional testing for binary files, large files (>2MB), and empty workspaces
  • Visual feedback on mode switch - Consider brief animation/flash when switching modes to reinforce the change
  • Line number support in FileViewerPane from terminal links - Currently opens at top of file; could scroll to clicked line

Out of Scope

  • Per-workspace terminal link behavior override - Currently global setting only; per-workspace could be added if requested
  • Terminal link detection for more path formats - Current regex may not catch all edge cases (Windows paths, URLs with line numbers, etc.)

Files

New

  • GroupStrip/GroupStrip.tsx - Group switching strip component
  • FileViewerPane/FileViewerPane.tsx - File viewer with Raw/Rendered/Diff modes + inline editing
  • ViewModeToggle/ViewModeToggle.tsx - Segmented control for mode toggle
  • workspace-view-mode.ts - Per-workspace view mode store
  • 0004_add_terminal_link_behavior_setting.sql - Migration for terminal link behavior setting

Modified

  • file-contents.ts - Added readWorkingFile procedure
  • ContentView/index.tsx - Routes to TabsContent or ChangesContent based on mode
  • ChangesView.tsx - Added onFileOpen callback prop
  • Sidebar/index.tsx - Passes onFileOpen in Workbench mode
  • WorkspaceActionBar.tsx - Added ViewModeToggle to header
  • WorkspaceView/index.tsx - View mode reactivity and ⌘+T behavior
  • tabs/store.ts - Added addFileViewerPane action
  • tabs/types.ts - Added FileViewerPane types
  • tabs/utils.ts - Added pane utility functions
  • shared/tabs-types.ts - Added file-viewer pane type
  • Terminal/helpers.ts - Refactored to accept onFileLinkClick callback
  • Terminal/Terminal.tsx - Wired up terminal link behavior setting
  • BehaviorSettings.tsx - Added terminal link behavior dropdown
  • settings/index.ts (tRPC) - Added getter/setter for terminal link behavior
  • schema.ts / zod.ts (local-db) - Added terminalLinkBehavior column

Testing

Core Features

  • Typecheck passes (bun run typecheck)
  • Manual: Toggle between Workbench/Review modes
  • Manual: File clicks open FileViewer panes in Workbench
  • Manual: ⌘+T switches to Workbench from Review mode
  • Manual: FileViewer Raw/Rendered/Diff modes work
  • Manual: Lock/unlock and split pane work
  • Manual: GroupStrip switches between groups
  • Manual: Edit files in Raw mode and save with ⌘+S
  • Manual: Edit files in Diff mode and save with ⌘+S
  • Manual: Dirty indicator appears when changes are unsaved

Terminal Link Behavior

  • Manual: Settings → Behavior shows "Terminal file links" dropdown
  • Manual: Default is "External editor" (existing behavior preserved)
  • Manual: With "External editor": Cmd+click file path → opens in external editor
  • Manual: With "File viewer": Cmd+click file path → opens FileViewerPane
  • Manual: Changing setting applies immediately (no terminal restart needed)
  • Manual: Setting persists after app restart

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 29, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This PR adds file viewer functionality to the desktop app, introducing a Monaco-based file editor with diff support, a new per-workspace view mode system switching between workbench and review modes, backend file reading with path validation and security checks, and UI components for tab management and view mode toggling.

Changes

Cohort / File(s) Summary
Backend File Reading
apps/desktop/src/lib/trpc/routers/changes/file-contents.ts
Adds readWorkingFile procedure with path validation to prevent symlink escapes, file-size enforcement (MAX_FILE_SIZE), binary-content detection, and structured result type differentiating success/failure modes (not-found, outside-worktree, too-large, binary). Includes helper utilities validatePathInWorktree and isBinaryContent.
File Viewer Component
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx, FileViewerPane/index.ts
Introduces new FileViewerPane component with Monaco editor integration, split-pane auto-sizing via ResizeObserver, three view modes (raw/rendered/diff), dirty tracking, save-on-Cmd/Ctrl+S support, and error handling for large/binary/out-of-worktree files. Fetches content via TRPC and supports in-place editing with persistence.
Tab Group Management
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx, GroupStrip/index.ts
Adds new GroupStrip component rendering a horizontal strip of tab-like items for the active workspace, with support for selecting, closing, and adding tabs, attention detection from pane state, and tooltip display.
Per-Workspace View Mode Store
apps/desktop/src/renderer/stores/workspace-view-mode.ts, apps/desktop/src/renderer/stores/index.ts
Introduces new Zustand-based store useWorkspaceViewModeStore managing per-workspace UI view modes ("workbench" | "review") with persistence and devtools integration. Methods: getWorkspaceViewMode, setWorkspaceViewMode.
Tab Store Extensions
apps/desktop/src/renderer/stores/tabs/store.ts, apps/desktop/src/renderer/stores/tabs/types.ts, apps/desktop/src/renderer/stores/tabs/utils.ts
Extends tabs subsystem with addFileViewerPane method to create/reuse file-viewer panes in the active tab, including smart pane reuse logic for unlocked file-viewer panes. Introduces new types AddFileViewerPaneOptions and CreateFileViewerPaneOptions, plus factory createFileViewerPane.
Shared Type Definitions
apps/desktop/src/shared/tabs-types.ts
Extends PaneType to include "file-viewer" and adds new types: FileViewerMode ("rendered" | "raw" | "diff"), DiffLayout ("inline" | "side-by-side"), and FileViewerState interface with fields for file path, view mode, lock state, diff layout, and optional change metadata.
View Mode Toggle Component
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/ViewModeToggle.tsx, ViewModeToggle/index.ts
New component providing UI toggle between Workbench and Review view modes, reading/updating per-workspace mode via store and TRPC.
Integration & Layout Updates
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx, TabView/index.tsx, ContentView/index.tsx, Sidebar/ChangesView.tsx, Sidebar/index.tsx, WorkspaceActionBar.tsx, WorkspaceView/index.tsx
Consolidates per-workspace view mode throughout the UI: TabsContent wraps TabView with GroupStrip; TabView routes file-viewer panes to FileViewerPane; ContentView switches between ChangesContent (review) and TabsContent (workbench); Sidebar wires file-open handlers to populate FileViewerPane when in workbench mode; WorkspaceActionBar centers ViewModeToggle; main WorkspaceView respects mode on terminal creation.

Sequence Diagrams

sequenceDiagram
    participant User
    participant ChangesView
    participant FileViewerPane
    participant TRPC
    participant Store as TabsStore
    participant Editor as Monaco Editor

    User->>ChangesView: Click file in changes
    ChangesView->>Store: addFileViewerPane(workspaceId, filePath, ...)
    
    rect rgb(200, 220, 240)
        note over Store: Pane Creation/Reuse Logic
        Store->>Store: Check for active tab
        alt No active tab
            Store->>Store: Create new tab
        end
        alt Unlocked file-viewer pane exists
            Store->>Store: Reuse pane, update fileViewer state
        else No reusable pane
            Store->>Store: Create new file-viewer pane
            Store->>Store: Update tab layout (side-by-side)
        end
    end
    
    Store->>FileViewerPane: Render with filePath, workspaceId
    FileViewerPane->>TRPC: readWorkingFile(worktreePath, filePath)
    
    rect rgb(240, 200, 200)
        note over TRPC: Backend Validation & Security
        TRPC->>TRPC: validatePathInWorktree (symlink check)
        TRPC->>TRPC: Check file size (MAX_FILE_SIZE)
        TRPC->>TRPC: Detect binary content
        TRPC->>FileViewerPane: Return { ok, content, reason }
    end
    
    FileViewerPane->>Editor: Load content
    User->>Editor: Edit file
    User->>Editor: Cmd/Ctrl+S
    Editor->>FileViewerPane: Save content
    FileViewerPane->>TRPC: Save mutation
    TRPC->>FileViewerPane: Success, invalidate queries
    FileViewerPane->>Editor: Reset dirty state
Loading
sequenceDiagram
    participant User
    participant ViewModeToggle
    participant Store as ViewModeStore
    participant ContentView
    participant TabsContent
    participant ChangesContent

    User->>ViewModeToggle: Click Workbench/Review
    ViewModeToggle->>Store: setWorkspaceViewMode(workspaceId, mode)
    Store->>Store: Update viewModeByWorkspaceId[workspaceId]
    
    Note over Store: Persist & notify subscribers
    
    ContentView->>Store: getWorkspaceViewMode(workspaceId)
    Store->>ContentView: Return "workbench" or "review"
    
    alt mode === "review"
        ContentView->>ChangesContent: Render changes/diffs
    else mode === "workbench"
        ContentView->>TabsContent: Render tabs & file viewer
        TabsContent->>TabsContent: Render GroupStrip (tab list)
        TabsContent->>TabsContent: Render TabView (pane content)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat(desktop): add Kaleidoscope-style git diff viewer in Changes tab #310: Modifies the changes TRPC router and adds Monaco-backed file/diff viewer UI components that share the same file-reading endpoint surface and feature integration.
  • tabs interaction #111: Updates the tabs subsystem (store.ts, types.ts, utils.ts) with drag/grouping refactors and pane helpers that directly overlap with new file-viewer pane creation and type definitions.
  • carousel #74: Refactors mode-switching and ModeCarousel API in the sidebar, related to the replacement of ModeCarousel-based handling with per-workspace view-mode store integration in this PR.

Poem

🐰 A file viewer blooms with Monaco's grace,
Per-workspace modes set each tab's place,
From worktree paths validated with care,
Tab strips and toggles dance in the air—
The changes view opens files with flair! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% 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 clearly summarizes the main feature: adding Workbench/Review mode toggle with FileViewer panes, which aligns with the core objectives.
Description check ✅ Passed The PR description is comprehensive, well-structured, and addresses all required template sections including Why, What/How, Testing, and Future Work with clear detail.

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/lib/trpc/routers/changes/file-contents.ts (1)

111-123: Path traversal vulnerability in saveFile - missing path validation.

The saveFile procedure joins worktreePath and filePath without validation. A malicious filePath like ../../../etc/cron.d/malicious could write files outside the worktree. This is inconsistent with readWorkingFile which properly validates paths.

🔎 Proposed fix
		saveFile: publicProcedure
			.input(
				z.object({
					worktreePath: z.string(),
					filePath: z.string(),
					content: z.string(),
				}),
			)
			.mutation(async ({ input }): Promise<{ success: boolean }> => {
-				const fullPath = join(input.worktreePath, input.filePath);
-				await writeFile(fullPath, input.content, "utf-8");
-				return { success: true };
+				// Validate path is within worktree
+				const validation = await validatePathInWorktree(
+					input.worktreePath,
+					input.filePath,
+				);
+
+				if (!validation.valid || !validation.resolvedPath) {
+					throw new Error(
+						validation.reason === "outside-worktree"
+							? "Cannot write to files outside worktree"
+							: "File path validation failed",
+					);
+				}
+
+				await writeFile(validation.resolvedPath, input.content, "utf-8");
+				return { success: true };
			}),
🧹 Nitpick comments (6)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/ViewModeToggle.tsx (1)

27-53: Consider adding aria-pressed for accessibility.

The toggle buttons function as a segmented control. Adding aria-pressed would improve screen reader support by announcing the selected state.

🔎 Suggested enhancement
 <button
 	type="button"
 	onClick={() => handleModeChange("workbench")}
+	aria-pressed={currentMode === "workbench"}
 	className={cn(
 		"px-3 py-1 text-sm font-medium rounded-md transition-all",
 		currentMode === "workbench"
 			? "bg-background text-foreground shadow-sm"
 			: "text-muted-foreground hover:text-foreground",
 	)}
 >
 	Workbench
 </button>
 <button
 	type="button"
 	onClick={() => handleModeChange("review")}
+	aria-pressed={currentMode === "review"}
 	className={cn(
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx (1)

52-61: Close button has a small touch target.

The close button with p-0.5 padding results in a small hit area that may be difficult to tap on touch devices. Consider increasing the padding or hit area while keeping the visual size small.

🔎 Suggested approach
 <button
 	type="button"
 	onClick={(e) => {
 		e.stopPropagation();
 		onClose();
 	}}
-	className="absolute -right-1 -top-1 p-0.5 rounded-full bg-muted opacity-0 group-hover:opacity-100 transition-opacity hover:bg-destructive hover:text-destructive-foreground"
+	className="absolute -right-1.5 -top-1.5 p-1 rounded-full bg-muted opacity-0 group-hover:opacity-100 transition-opacity hover:bg-destructive hover:text-destructive-foreground"
 >
-	<HiMiniXMark className="size-2.5" />
+	<HiMiniXMark className="size-2" />
 </button>
apps/desktop/src/renderer/stores/tabs/utils.ts (1)

117-122: Consider including .mdx files for rendered view mode.

The logic defaults to "rendered" for .md and .markdown files. If MDX files are used in the project, they might also benefit from the rendered view.

🔎 Suggested enhancement
 } else if (
 	options.filePath.endsWith(".md") ||
-	options.filePath.endsWith(".markdown")
+	options.filePath.endsWith(".markdown") ||
+	options.filePath.endsWith(".mdx")
 ) {
 	defaultViewMode = "rendered";
 }
apps/desktop/src/renderer/stores/tabs/store.ts (1)

389-398: Consider extracting view mode determination logic to reduce duplication.

The view mode determination logic (checking for diffCategory and markdown extensions) appears to be duplicated here and likely in createFileViewerPane. Consider extracting a shared helper function like determineDefaultViewMode(filePath, diffCategory) to ensure consistent behavior.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx (2)

262-303: Consider adding dedicated store actions for pane updates.

The direct useTabsStore.getState() / setState() pattern works but bypasses the action-based updates used elsewhere in the store (e.g., setFocusedPane, removePane). For consistency and testability, consider adding toggleFileViewerLock(paneId) and setFileViewerMode(paneId, mode) actions to the store.


31-69: Duplicated detectLanguage function - consider reusing existing utility.

This function duplicates logic from apps/desktop/src/lib/trpc/routers/changes/utils/parse-status.ts (which is exported at line 199). The shared utility has more comprehensive language mappings (e.g., toml, ruby, php, makefile, dockerfile, csharp). Consider importing and reusing it for consistency and maintainability.

-/** Client-side language detection for Monaco editor */
-function detectLanguage(filePath: string): string {
-	const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
-	const languageMap: Record<string, string> = {
-		ts: "typescript",
-		// ... truncated
-	};
-	return languageMap[ext] ?? "plaintext";
-}
+import { detectLanguage } from "lib/trpc/routers/changes/utils/parse-status";
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85064bf and 77e3cac.

📒 Files selected for processing (20)
  • apps/desktop/src/lib/trpc/routers/changes/file-contents.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/WorkspaceActionBar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/ViewModeToggle.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/stores/tabs/store.ts
  • apps/desktop/src/renderer/stores/tabs/types.ts
  • apps/desktop/src/renderer/stores/tabs/utils.ts
  • apps/desktop/src/renderer/stores/workspace-view-mode.ts
  • apps/desktop/src/shared/tabs-types.ts
🧰 Additional context used
📓 Path-based instructions (5)
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/WorkspaceActionBar/components/ViewModeToggle/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/ViewModeToggle.tsx
  • apps/desktop/src/renderer/stores/workspace-view-mode.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/stores/tabs/utils.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx
  • apps/desktop/src/shared/tabs-types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/index.ts
  • apps/desktop/src/lib/trpc/routers/changes/file-contents.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/stores/tabs/types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/WorkspaceActionBar.tsx
  • apps/desktop/src/renderer/stores/tabs/store.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use type safety and avoid any types unless absolutely necessary in TypeScript files

Files:

  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/ViewModeToggle.tsx
  • apps/desktop/src/renderer/stores/workspace-view-mode.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/stores/tabs/utils.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx
  • apps/desktop/src/shared/tabs-types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/index.ts
  • apps/desktop/src/lib/trpc/routers/changes/file-contents.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/stores/tabs/types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/WorkspaceActionBar.tsx
  • apps/desktop/src/renderer/stores/tabs/store.ts
apps/desktop/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Move Node.js functionality needed in Electron renderer to src/main/lib/ and communicate via IPC

Files:

  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/ViewModeToggle.tsx
  • apps/desktop/src/renderer/stores/workspace-view-mode.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/stores/tabs/utils.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx
  • apps/desktop/src/shared/tabs-types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/index.ts
  • apps/desktop/src/lib/trpc/routers/changes/file-contents.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/stores/tabs/types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/WorkspaceActionBar.tsx
  • apps/desktop/src/renderer/stores/tabs/store.ts
apps/desktop/src/{shared/ipc-channels.ts,main/**/*ipcs.ts,renderer/**/*.tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Define all Electron IPC channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/ViewModeToggle.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/WorkspaceActionBar.tsx
apps/**/src/**/**/[A-Z]*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Structure component folders with one component per file using format ComponentName/ComponentName.tsx with index.ts barrel export

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/ViewModeToggle.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/WorkspaceActionBar.tsx
🧠 Learnings (5)
📚 Learning: 2025-12-28T01:56:39.031Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T01:56:39.031Z
Learning: Applies to apps/desktop/src/**/*.{ts,tsx} : Move Node.js functionality needed in Electron renderer to `src/main/lib/` and communicate via IPC

Applied to files:

  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/index.ts
📚 Learning: 2025-12-28T01:56:39.031Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T01:56:39.031Z
Learning: Applies to apps/**/src/**/**/[A-Z]*.tsx : Structure component folders with one component per file using format `ComponentName/ComponentName.tsx` with `index.ts` barrel export

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
📚 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-view-mode.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 Electron interprocess communication, ALWAYS use tRPC as defined in `src/lib/trpc`

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
📚 Learning: 2025-12-28T01:56:39.031Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T01:56:39.031Z
Learning: Applies to apps/desktop/src/{shared/ipc-channels.ts,main/**/*ipcs.ts,renderer/**/*.tsx} : Define all Electron IPC channel types in `apps/desktop/src/shared/ipc-channels.ts` before implementing handlers

Applied to files:

  • apps/desktop/src/renderer/stores/tabs/utils.ts
  • apps/desktop/src/shared/tabs-types.ts
  • apps/desktop/src/renderer/stores/tabs/types.ts
🧬 Code graph analysis (12)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx (1)
apps/desktop/src/renderer/stores/workspace-view-mode.ts (1)
  • useWorkspaceViewModeStore (28-56)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx (2)
apps/desktop/src/renderer/stores/tabs/types.ts (1)
  • Tab (12-14)
apps/desktop/src/renderer/stores/tabs/utils.ts (1)
  • getTabDisplayName (17-23)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx (2)
apps/desktop/src/renderer/stores/workspace-view-mode.ts (1)
  • useWorkspaceViewModeStore (28-56)
apps/desktop/src/shared/hotkeys.ts (1)
  • HOTKEYS (65-237)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx (2)
apps/desktop/src/shared/tabs-types.ts (2)
  • Pane (46-59)
  • FileViewerMode (16-16)
apps/desktop/src/renderer/contexts/MonacoProvider.tsx (1)
  • monaco (107-107)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/index.tsx (3)
apps/desktop/src/renderer/stores/workspace-view-mode.ts (1)
  • useWorkspaceViewModeStore (28-56)
apps/desktop/src/shared/changes-types.ts (1)
  • ChangedFile (22-28)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx (1)
  • ChangesView (24-367)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx (1)
  • FileViewerPane (88-537)
apps/desktop/src/renderer/stores/tabs/utils.ts (3)
apps/desktop/src/shared/tabs-types.ts (4)
  • FileViewerMode (16-16)
  • DiffLayout (21-21)
  • Pane (46-59)
  • FileViewerState (26-41)
apps/desktop/src/shared/changes-types.ts (1)
  • ChangeCategory (15-19)
apps/desktop/src/renderer/stores/tabs/types.ts (1)
  • Pane (6-6)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx (2)
apps/desktop/src/shared/changes-types.ts (2)
  • ChangedFile (22-28)
  • ChangeCategory (15-19)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/PortsList.tsx (1)
  • PortsList (15-121)
apps/desktop/src/shared/tabs-types.ts (2)
apps/desktop/src/renderer/stores/tabs/types.ts (1)
  • PaneType (6-6)
apps/desktop/src/shared/changes-types.ts (1)
  • ChangeCategory (15-19)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx (1)
  • GroupStrip (66-151)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx (1)
  • TabView (27-169)
apps/desktop/src/renderer/stores/tabs/types.ts (1)
apps/desktop/src/shared/changes-types.ts (1)
  • ChangeCategory (15-19)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/WorkspaceActionBar.tsx (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/WorkspaceActionBarLeft/WorkspaceActionBarLeft.tsx (1)
  • WorkspaceActionBarLeft (5-41)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceActionBar/components/ViewModeToggle/ViewModeToggle.tsx (1)
  • ViewModeToggle (8-55)
⏰ 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 (28)
apps/desktop/src/renderer/stores/tabs/types.ts (1)

32-40: LGTM! Well-structured type definitions.

The AddFileViewerPaneOptions interface and the addFileViewerPane method signature are well-defined with appropriate required and optional fields. The integration with ChangeCategory from shared types maintains consistency across the codebase.

Also applies to: 65-68

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

6-6: LGTM! Comprehensive and well-documented type definitions.

The new file viewer types are thoughtfully designed:

  • PaneType extension follows the existing pattern
  • FileViewerMode and DiffLayout provide clear, appropriate options
  • FileViewerState includes all necessary metadata with helpful inline comments
  • Optional fields are correctly identified for context-dependent scenarios

Also applies to: 11-11, 16-21, 26-41, 58-58

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

1-1: LGTM! Follows the established barrel export pattern.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/index.ts (1)

1-1: LGTM! Follows the established barrel export pattern.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (1)

27-34: LGTM! Clean layout integration.

The new layout structure properly integrates GroupStrip above the existing TabView with appropriate flex container management. The use of flex-col, overflow-hidden, and min-h-0 ensures proper content behavior and prevents layout issues.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/index.ts (1)

1-1: LGTM! Follows the established barrel export pattern.

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

8-8: LGTM! Consistent with existing store export pattern.

The new workspace-view-mode store export follows the established pattern and makes the store properly accessible through the central stores module.

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

44-53: LGTM! Proper Zustand store integration.

The view mode state is correctly integrated by subscribing to the actual store data (viewModeByWorkspaceId) to ensure reactivity. The default to "workbench" mode is a sensible fallback.


56-64: LGTM! Intuitive mode-switching behavior.

The NEW_TERMINAL hotkey handler correctly implements the UX flow: when in Review mode, it switches to Workbench mode first before creating a terminal. This aligns with the PR's design goal that terminal panes belong to the Workbench experience. The dependency array is properly updated to include the new reactive values.

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

6-29: LGTM!

The component correctly subscribes to the viewModeByWorkspaceId map directly rather than using the getter function, ensuring proper Zustand reactivity when the view mode changes. The fallback to "workbench" is consistent with the store's default behavior.

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

12-23: LGTM!

The three-column layout correctly centers the ViewModeToggle while keeping left and right sections at the edges. The flex-1 container provides proper centering behavior.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx (1)

99-114: LGTM!

The routing logic correctly identifies file-viewer panes and passes the appropriate props. The simpler prop set compared to TabPane aligns with FileViewerPane's auto-split-only behavior based on container dimensions.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx (1)

66-118: LGTM!

The component properly derives workspace-specific tabs, tracks attention state, and handles tab operations. The memoization strategy is appropriate for the data transformations.

apps/desktop/src/renderer/stores/tabs/utils.ts (1)

104-144: LGTM!

The createFileViewerPane utility correctly constructs file-viewer panes with sensible defaults. The view mode detection logic and state construction are well-structured.

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

7-39: LGTM!

The sidebar correctly conditionalizes the onFileOpen handler based on view mode, ensuring file clicks only open FileViewerPane in Workbench mode. The Zustand subscription pattern is consistent with other components in this PR.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx (3)

16-24: LGTM!

The ChangesViewProps interface properly types the optional onFileOpen callback, and the component correctly uses optional chaining when invoking it.


137-147: LGTM!

Both file selection handlers correctly invoke onFileOpen with the appropriate arguments, including the commitHash for committed files.


363-364: Verify PortsList placement is intentional.

PortsList was imported from ../TabsView/PortsList and added to the bottom of ChangesView. This appears to support the unified sidebar mentioned in the PR objectives, making ports visible in both Workbench and Review modes.

If this is intentional, the import path suggests PortsList might be better co-located with ChangesView or in a shared components directory since it's no longer exclusive to TabsView.

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

1-56: LGTM! Clean Zustand store implementation.

The store follows best practices with proper TypeScript typing, uses persist for localStorage persistence, and devtools for debugging. The default fallback to "workbench" mode is handled correctly. Based on learnings, Zustand is the preferred state management choice.

apps/desktop/src/renderer/stores/tabs/store.ts (1)

7-18: LGTM!

Import additions are correctly structured for the new file viewer pane functionality.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx (4)

138-151: Good practice: Proper query invalidation after save.

The save mutation correctly invalidates readWorkingFile, getFileContents, and getStatus queries to ensure UI consistency after file modifications.


176-194: LGTM! Proper keybinding setup with cross-platform support.

The monaco.KeyMod.CtrlCmd correctly handles Cmd+S on macOS and Ctrl+S on Windows/Linux. The dependency array correctly includes handleSaveRaw.


210-231: LGTM! Conditional query fetching is well-structured.

The enabled conditions properly gate data fetching based on view mode and required parameters, avoiding unnecessary network requests.


381-410: LGTM! Well-configured Monaco Editor with sensible defaults.

The editor options provide a good user experience with disabled minimap, word wrap enabled, appropriate font settings, and reasonable padding/scrollbar sizes.

apps/desktop/src/lib/trpc/routers/changes/file-contents.ts (4)

9-23: LGTM! Well-defined constants and result type.

The 2 MiB file size limit and 8KB binary check window are reasonable defaults. The discriminated union type provides clear error categorization.


28-61: Robust path validation with symlink resolution - good security practice.

The validation correctly handles: absolute paths, path traversal attempts, and symlink escapes. The use of realpath() to resolve symlinks before checking containment is the right approach.


63-74: LGTM! Standard binary detection heuristic.

NUL byte scanning is a widely-used approach for binary detection. The 8KB window balances accuracy with performance.


130-185: LGTM! Well-implemented file read with proper validation chain.

The procedure correctly validates paths, checks file size before reading, and detects binary content. The resolved path from validatePathInWorktree is consistently used throughout, avoiding TOCTOU issues.

Comment thread apps/desktop/src/renderer/stores/tabs/store.ts
Introduces a workspace-level view mode toggle allowing users to switch between:
- **Workbench mode**: Mosaic panes layout with terminals + file viewers for in-flow work
- **Review mode**: Dedicated Changes page for focused code review

Key changes:
- Add ViewModeToggle component in workspace header (prominent segmented control)
- Add FileViewerPane with Raw/Rendered/Diff modes, lock/unlock, and split support
- Add GroupStrip for group switching above Mosaic content
- Unify sidebar to use full ChangesView in both modes (with onFileOpen callback)
- Add workspace-view-mode store with per-workspace persistence
- Add readWorkingFile tRPC procedure for safe file reads (size/binary checks)
- Wire file clicks to open/reuse FileViewer panes (MRU unlocked policy)
- Cmd+T in Review mode switches to Workbench first, then creates terminal
- Add !!worktreePath checks to FileViewerPane query enabled conditions
- Add !!worktreePath checks to save handler guards (handleSaveRaw, handleSaveDiff)
- Fix stale state reference after addTab() in addFileViewerPane action

Addresses CodeRabbit review feedback.
… and UX improvements

- Add validatePathForWrite() to prevent path traversal attacks in saveFile
- Add aria-pressed attribute to ViewModeToggle buttons for accessibility
- Increase close button touch target in GroupStrip for better UX
- Add .mdx file support for rendered view mode
P0 Security:
- Add path validation to getUnstagedVersions before readFile (prevents traversal)
- Key Editor/DiffViewer by filePath to force remount (fixes stale Cmd+S closure)

P1 Fixes:
- Update originalContentRef when raw content loads (dirty tracking fix)
- Add .mdx to isMarkdown check for toolbar consistency
- Gate diff editable to staged/unstaged only (against-main/committed now read-only)

P2 Performance:
- Add safeGitShow helper with 2MB size limit on all git show calls
- Add size check before reading working tree files in getUnstagedVersions
- P0-1: Add file-viewer type and fileViewer object to paneSchema for tab persistence
- P0-2: Fix symlink escape vulnerability in validatePathForWrite by checking
  if target is symlink and resolving parent directory paths
- P1-1: Pass defaultBranch to FileViewerPane for against-main diffs
- P1-2: Switch staged diff to unstaged after save (matches Review mode behavior)
- P2: Use Buffer.byteLength instead of string.length in safeGitShow for
  accurate UTF-8 byte counting
P0 (critical):
- Remove duplicate saveFile from git-operations.ts that was overwriting
  the hardened version in file-contents.ts (security vulnerability)

P1 (must fix):
- Use basename()/dirname() from node:path instead of split('/') for
  cross-platform Windows compatibility in validatePathForWrite
- Add preflight size check with git cat-file -s in safeGitShow to
  prevent memory spikes from large blobs before materializing content

P2 (UX):
- Fix split pane to clone file-viewer state instead of creating terminal
  when splitting a file-viewer pane (locked by default)

Question fix:
- Show GroupStrip with add button even when tabs.length === 0 so users
  have visible UI to create new terminal (not just hotkey)
…ocalDb

P0 (CRITICAL SECURITY):
- Add validateWorktreePathInDb() that verifies worktreePath exists in
  localDb.worktrees before any filesystem operations
- Without this, a compromised renderer could read/write arbitrary files
  by passing worktreePath='/' and filePath='.ssh/id_rsa'
- Applied to getFileContents, saveFile, and readWorkingFile procedures

P1 (correctness):
- Replace startsWith('..') checks with segment-aware containsPathTraversal()
  and isPathOutsideBase() helpers that use path.sep
- Fixes false positives on valid paths like '..foo/bar' (directories
  starting with '..')
- Cross-platform compatible (handles both / and \ separators)

P2 (performance):
- Guard killTerminalForPane() calls on pane.type === 'terminal'
- Prevents unnecessary IPC and warning logs when closing file-viewer panes
…ve editor drafts

P0 (security):
- Extract assertWorktreePathInDb to shared security.ts
- Returns worktree record to avoid duplicate queries
- Apply validation to ALL routes: git-operations, status, staging, branches

P1 (data loss):
- Preserve unsaved editor content across view mode switches
- Store draft in ref before switching away from raw mode
- Restore draft when returning to raw mode
- Clear draft on save and file change
- Update dirty state on editor mount with draft content
…otection

P0 Security (BLOCK):
- Add validatePathInWorktree() check before rm() in deleteUntracked
- Prevents path traversal (../) and symlink escape attacks
- Consolidated path validation utilities in security.ts

P1 Data Loss:
- Track save source (Raw vs Diff) with savingFromRawRef
- Only clear draft when saving from Raw mode
- Disable Diff editing when Raw draft exists (forces user to save/discard)

P2:
- Use ['--', filePath] in git.add() to handle paths starting with -
…th validation

- Reduce security.ts from 213 to 65 lines
- Remove async symlink/realpath checking (users own their repos)
- Remove segment-aware path traversal (..foo edge case not worth complexity)
- Keep worktreePath DB validation (the real security boundary)
- Keep simple .. and absolute path checks (sufficient for 99.99% of cases)
P0 (blocking):
- Add path validation (rejects .. and absolute) to applyUntrackedLineCount
- Add 1MB size cap to prevent OOM on large untracked files during polling

P2:
- Add type guard in updateTabLayout - only call killTerminalForPane for terminal panes
- Use ['--', branch] in switchBranch to ensure branch treated as refname
…lidation

- Add path-validation.ts with industry-standard containment check (path.relative)
- Add secure-fs.ts with self-validating FS wrappers (symlink escape protection)
- Add git-commands.ts with semantic helpers (gitSwitchBranch vs gitCheckoutFile)
- Fix switchBranch: use 'git switch' instead of 'git checkout --' (was file checkout)
- Fix path traversal check: segment-aware (allows ..foo, rejects ..)
- Fix symlink bypass: check parent dirs for new files, use stat not lstat
- Fix worktree root deletion: explicit allowRoot check
- All FS operations now go through secureFs with validation built-in
Add a setting to control whether Cmd+clicking file paths in the terminal
opens them in an external editor (default) or in the in-app FileViewerPane.

- Add terminalLinkBehavior setting to local-db schema with migration
- Add getTerminalLinkBehavior/setTerminalLinkBehavior tRPC procedures
- Refactor createTerminalInstance to accept onFileLinkClick callback
- Wire up setting in Terminal.tsx with ref pattern (avoids terminal recreation)
- Add Select UI in BehaviorSettings for choosing link behavior
Remove async symlink escape detection from path validation since the
threat model doesn't justify it: a compromised renderer already has
terminal access for arbitrary command execution.

Changes:
- Replace resolveSecurePath (async) with resolvePathInWorktree (sync)
- Remove assertNoSymlinkEscape and checkSymlinks option
- Add validateRelativePath for simple path safety checks
- Update secure-fs.ts to use new sync functions
- Update threat model documentation in path-validation.ts
@andreasasprou andreasasprou force-pushed the feat/desktop-workbench-review-mode branch from f2f4849 to 0bfdbe6 Compare January 2, 2026 06:10
@andreasasprou
Copy link
Copy Markdown
Contributor Author

Closing in favor of #559 which bundles this PR together with the workspace navigation sidebar feature. All commits from this PR are included in #559.

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