) => {
+ const value = e.target.value;
+ setLocalTitle(value); // Immediate update for responsive typing
+ debouncedSetTitle(value); // Debounced update for derived state
+ };
+
const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery();
const { data: recentProjects = [] } = trpc.projects.getRecents.useQuery();
const {
@@ -124,6 +146,7 @@ export function NewWorkspaceModal() {
const resetForm = () => {
setSelectedProjectId(null);
+ setLocalTitle("");
setTitle("");
setBranchName("");
setBranchNameEdited(false);
@@ -170,7 +193,8 @@ export function NewWorkspaceModal() {
const handleCreateWorkspace = async () => {
if (!selectedProjectId) return;
- const workspaceName = title.trim() || undefined;
+ // Use localTitle for the actual value (in case debounce hasn't fired yet)
+ const workspaceName = localTitle.trim() || undefined;
const customBranchName = branchName.trim() || undefined;
try {
@@ -266,15 +290,15 @@ export function NewWorkspaceModal() {
id="title"
className="h-9 text-sm"
placeholder="Feature name (press Enter to create)"
- value={title}
- onChange={(e) => setTitle(e.target.value)}
+ value={localTitle}
+ onChange={handleTitleChange}
/>
- {title && !showAdvanced && (
+ {localTitle && !showAdvanced && (
- {branchName || generateBranchFromTitle(title)}
+ {branchName || generateBranchFromTitle(localTitle)}
from {effectiveBaseBranch}
@@ -304,8 +328,8 @@ export function NewWorkspaceModal() {
id="branch"
className="h-8 text-sm font-mono"
placeholder={
- title
- ? generateBranchFromTitle(title)
+ localTitle
+ ? generateBranchFromTitle(localTitle)
: "auto-generated"
}
value={branchName}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
index 7ef3c5db38b..94fd04421cf 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
@@ -2,7 +2,7 @@ import type * as Monaco from "monaco-editor";
import { useCallback, useEffect, useRef, useState } from "react";
import type { MosaicBranch } from "react-mosaic-component";
import { useTabsStore } from "renderer/stores/tabs/store";
-import type { Pane, Tab } from "renderer/stores/tabs/types";
+import type { Tab } from "renderer/stores/tabs/types";
import type { FileViewerMode } from "shared/tabs-types";
import { BasePaneWindow } from "../components";
import { FileViewerContent } from "./components/FileViewerContent";
@@ -14,7 +14,6 @@ import { UnsavedChangesDialog } from "./UnsavedChangesDialog";
interface FileViewerPaneProps {
paneId: string;
path: MosaicBranch[];
- pane: Pane;
isActive: boolean;
tabId: string;
worktreePath: string;
@@ -44,7 +43,6 @@ interface FileViewerPaneProps {
export function FileViewerPane({
paneId,
path,
- pane,
isActive,
tabId,
worktreePath,
@@ -57,6 +55,9 @@ export function FileViewerPane({
onMoveToTab,
onMoveToNewTab,
}: FileViewerPaneProps) {
+ // Use granular selector to only get this pane's fileViewer data
+ const fileViewer = useTabsStore((s) => s.panes[paneId]?.fileViewer);
+
const editorRef = useRef(null);
const [isDirty, setIsDirty] = useState(false);
const originalContentRef = useRef("");
@@ -66,8 +67,6 @@ export function FileViewerPane({
const [showUnsavedDialog, setShowUnsavedDialog] = useState(false);
const [isSavingAndSwitching, setIsSavingAndSwitching] = useState(false);
const pendingModeRef = useRef(null);
-
- const fileViewer = pane.fileViewer;
const filePath = fileViewer?.filePath ?? "";
const viewMode = fileViewer?.viewMode ?? "raw";
const isPinned = fileViewer?.isPinned ?? false;
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
index 77a85e0dfeb..8e82a2d5289 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
@@ -4,8 +4,9 @@ import {
registerPaneRef,
unregisterPaneRef,
} from "renderer/stores/tabs/pane-refs";
+import { useTabsStore } from "renderer/stores/tabs/store";
import { useTerminalCallbacksStore } from "renderer/stores/tabs/terminal-callbacks";
-import type { Pane, Tab } from "renderer/stores/tabs/types";
+import type { Tab } from "renderer/stores/tabs/types";
import { TabContentContextMenu } from "../TabContentContextMenu";
import { Terminal } from "../Terminal";
import { DirectoryNavigator } from "../Terminal/DirectoryNavigator";
@@ -14,7 +15,6 @@ import { BasePaneWindow, PaneToolbarActions } from "./components";
interface TabPaneProps {
paneId: string;
path: MosaicBranch[];
- pane: Pane;
isActive: boolean;
tabId: string;
workspaceId: string;
@@ -44,7 +44,6 @@ interface TabPaneProps {
export function TabPane({
paneId,
path,
- pane,
isActive,
tabId,
workspaceId,
@@ -57,6 +56,10 @@ export function TabPane({
onMoveToTab,
onMoveToNewTab,
}: TabPaneProps) {
+ // Use granular selector to only get this pane's cwd data
+ const paneCwd = useTabsStore((s) => s.panes[paneId]?.cwd);
+ const paneCwdConfirmed = useTabsStore((s) => s.panes[paneId]?.cwdConfirmed);
+
const terminalContainerRef = useRef(null);
const getClearCallback = useTerminalCallbacksStore((s) => s.getClearCallback);
const getScrollToBottomCallback = useTerminalCallbacksStore(
@@ -95,8 +98,8 @@ export function TabPane({
;
}
-export function TabView({ tab, panes }: TabViewProps) {
+export function TabView({ tab }: TabViewProps) {
const updateTabLayout = useTabsStore((s) => s.updateTabLayout);
const removePane = useTabsStore((s) => s.removePane);
const removeTab = useTabsStore((s) => s.removeTab);
const { splitPaneAuto, splitPaneHorizontal, splitPaneVertical } =
useTabsWithPresets();
const setFocusedPane = useTabsStore((s) => s.setFocusedPane);
- const focusedPaneIds = useTabsStore((s) => s.focusedPaneIds);
+ const focusedPaneId = useTabsStore((s) => s.focusedPaneIds[tab.id]);
const movePaneToTab = useTabsStore((s) => s.movePaneToTab);
const movePaneToNewTab = useTabsStore((s) => s.movePaneToNewTab);
const allTabs = useTabsStore((s) => s.tabs);
+ const allPanes = useTabsStore((s) => s.panes);
// Get worktree path for file viewer panes
const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery();
@@ -46,9 +45,25 @@ export function TabView({ tab, panes }: TabViewProps) {
(t) => t.workspaceId === tab.workspaceId,
);
- const focusedPaneId = focusedPaneIds[tab.id];
+ // Extract pane IDs from layout
+ const layoutPaneIds = useMemo(
+ () => extractPaneIdsFromLayout(tab.layout),
+ [tab.layout],
+ );
+
+ // Memoize the filtered panes to avoid creating new objects on every render
+ const tabPanes = useMemo(() => {
+ const result: Record = {};
+ for (const paneId of layoutPaneIds) {
+ const pane = allPanes[paneId];
+ if (pane?.tabId === tab.id) {
+ result[paneId] = { tabId: pane.tabId, type: pane.type };
+ }
+ }
+ return result;
+ }, [layoutPaneIds, allPanes, tab.id]);
- const validPaneIds = new Set(getPaneIdsForTab(panes, tab.id));
+ const validPaneIds = new Set(Object.keys(tabPanes));
const cleanedLayout = cleanLayout(tab.layout, validPaneIds);
// Auto-remove tab when all panes are gone
@@ -85,10 +100,10 @@ export function TabView({ tab, panes }: TabViewProps) {
const renderPane = useCallback(
(paneId: string, path: MosaicBranch[]) => {
- const pane = panes[paneId];
+ const paneInfo = tabPanes[paneId];
const isActive = paneId === focusedPaneId;
- if (!pane) {
+ if (!paneInfo) {
return (
Pane not found: {paneId}
@@ -97,7 +112,7 @@ export function TabView({ tab, panes }: TabViewProps) {
}
// Route file-viewer panes to FileViewerPane component
- if (pane.type === "file-viewer") {
+ if (paneInfo.type === "file-viewer") {
if (!worktreePath) {
return (
@@ -109,7 +124,6 @@ export function TabView({ tab, panes }: TabViewProps) {
{
const paneId = tabId;
- const panes = useTabsStore((s) => s.panes);
- const pane = panes[paneId];
+ // Use granular selectors to avoid re-renders when other panes change
+ const pane = useTabsStore((s) => s.panes[paneId]);
const paneInitialCommands = pane?.initialCommands;
const paneInitialCwd = pane?.initialCwd;
const clearPaneInitialData = useTabsStore((s) => s.clearPaneInitialData);
@@ -48,7 +48,10 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => {
const setFocusedPane = useTabsStore((s) => s.setFocusedPane);
const setTabAutoTitle = useTabsStore((s) => s.setTabAutoTitle);
const updatePaneCwd = useTabsStore((s) => s.updatePaneCwd);
- const focusedPaneIds = useTabsStore((s) => s.focusedPaneIds);
+ // Use granular selector - only subscribe to this tab's focused pane
+ const focusedPaneId = useTabsStore(
+ (s) => s.focusedPaneIds[pane?.tabId ?? ""],
+ );
const addFileViewerPane = useTabsStore((s) => s.addFileViewerPane);
const setPaneStatus = useTabsStore((s) => s.setPaneStatus);
const terminalTheme = useTerminalTheme();
@@ -56,7 +59,7 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => {
// Ref for initial theme to avoid recreating terminal on theme change
const initialThemeRef = useRef(terminalTheme);
- const isFocused = pane?.tabId ? focusedPaneIds[pane.tabId] === paneId : false;
+ const isFocused = focusedPaneId === paneId;
// Refs avoid effect re-runs when these values change
const isFocusedRef = useRef(isFocused);
@@ -155,10 +158,29 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => {
}
}, [paneInitialCwd, workspaceCwd, terminalCwd]);
- // Sync terminal cwd to store for DirectoryNavigator
+ // Debounced CWD update to reduce store updates during rapid directory changes
+ const debouncedUpdatePaneCwdRef = useRef(
+ debounce((id: string, cwd: string | null, confirmed: boolean) => {
+ updatePaneCwd(id, cwd, confirmed);
+ }, 150),
+ );
+
+ // Sync terminal cwd to store for DirectoryNavigator (debounced)
useEffect(() => {
- updatePaneCwd(paneId, terminalCwd, cwdConfirmed);
- }, [terminalCwd, cwdConfirmed, paneId, updatePaneCwd]);
+ debouncedUpdatePaneCwdRef.current(
+ paneId,
+ terminalCwd,
+ cwdConfirmed ?? false,
+ );
+ }, [terminalCwd, cwdConfirmed, paneId]);
+
+ // Cleanup debounced function on unmount
+ useEffect(() => {
+ const debouncedFn = debouncedUpdatePaneCwdRef.current;
+ return () => {
+ debouncedFn.cancel();
+ };
+ }, []);
// Parse terminal data for cwd (OSC 7 sequences)
const updateCwdFromData = useCallback((data: string) => {
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
index 06e4cab2f7b..ae8087438e5 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
@@ -15,7 +15,6 @@ export function TabsContent() {
const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery();
const activeWorkspaceId = activeWorkspace?.id;
const allTabs = useTabsStore((s) => s.tabs);
- const panes = useTabsStore((s) => s.panes);
const activeTabIds = useTabsStore((s) => s.activeTabIds);
const {
@@ -37,11 +36,7 @@ export function TabsContent() {
return (
- {tabToRender ? (
-
- ) : (
-
- )}
+ {tabToRender ? : }
{isSidebarOpen && (