diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 19b3ef9ea..b7ec45512 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -1,5 +1,6 @@ import { forwardRef, + memo, useCallback, useEffect, useImperativeHandle, @@ -63,6 +64,7 @@ import { MAX_CONTENT_WIDTH } from "@/constants/layout"; import { getMarkdownListMarker } from "@/utils/markdown-list"; import { normalizeInlinePathTarget } from "@/utils/inline-path"; import { prepareWorkspaceTab } from "@/utils/workspace-navigation"; +import { useStableEvent } from "@/hooks/use-stable-event"; import { getWorkingIndicatorDotStrength, WORKING_INDICATOR_CYCLE_MS, @@ -88,7 +90,7 @@ export interface AgentStreamViewProps { onOpenWorkspaceFile?: (input: { filePath: string }) => void; } -export const AgentStreamView = forwardRef( +const AgentStreamViewComponent = forwardRef( function AgentStreamView( { agentId, @@ -136,6 +138,11 @@ export const AgentStreamView = forwardRef( + handler: (...args: TArgs) => TResult, +): (...args: TArgs) => TResult { + const handlerRef = useRef(handler); + handlerRef.current = handler; + + return useCallback((...args: TArgs) => handlerRef.current(...args), []); +} diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index cbc792264..c32116953 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -22,6 +22,7 @@ import { useArchiveAgent } from "@/hooks/use-archive-agent"; import { useDelayedHistoryRefreshToast } from "@/hooks/use-delayed-history-refresh-toast"; import { useAgentInputDraft } from "@/hooks/use-agent-input-draft"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; +import { useStableEvent } from "@/hooks/use-stable-event"; import { usePaneContext } from "@/panels/pane-context"; import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry"; import { @@ -103,12 +104,13 @@ function useAgentPanelDescriptor( function AgentPanel() { const { serverId, target, isPaneFocused, openFileInWorkspace } = usePaneContext(); invariant(target.kind === "agent", "AgentPanel requires agent target"); - const handleOpenWorkspaceFile = useCallback( - (input: { filePath: string }) => { - openFileInWorkspace(input.filePath); - }, - [openFileInWorkspace], - ); + + function openWorkspaceFile(input: { filePath: string }) { + openFileInWorkspace(input.filePath); + } + + const handleOpenWorkspaceFile = useStableEvent(openWorkspaceFile); + return ( ); + const buildDesktopPaneContentModel = useCallback( + function buildDesktopPaneContentModel(input: { + paneId: string; + tab: WorkspaceTabDescriptor; + isPaneFocused: boolean; + }) { + return buildPaneContentModel({ + tab: input.tab, + paneId: input.paneId, + isPaneFocused: input.isPaneFocused, + focusPaneBeforeOpen: true, + }); + }, + [buildPaneContentModel], + ); + + const handleFocusPane = useCallback( + function handleFocusPane(paneId: string) { + if (!persistenceKey) { + return; + } + focusWorkspacePane(persistenceKey, paneId); + }, + [focusWorkspacePane, persistenceKey], + ); + + const handleSplitPane = useCallback( + function handleSplitPane(input: { + tabId: string; + targetPaneId: string; + position: "left" | "right" | "top" | "bottom"; + }) { + if (!persistenceKey) { + return; + } + splitWorkspacePane(persistenceKey, input); + }, + [persistenceKey, splitWorkspacePane], + ); + + const handleMoveTabToPane = useCallback( + function handleMoveTabToPane(tabId: string, toPaneId: string) { + if (!persistenceKey) { + return; + } + moveWorkspaceTabToPane(persistenceKey, tabId, toPaneId); + }, + [moveWorkspaceTabToPane, persistenceKey], + ); + + const handleResizePaneSplit = useCallback( + function handleResizePaneSplit(groupId: string, sizes: number[]) { + if (!persistenceKey) { + return; + } + resizeWorkspaceSplit(persistenceKey, groupId, sizes); + }, + [persistenceKey, resizeWorkspaceSplit], + ); + + const handleReorderTabsInPane = useCallback( + function handleReorderTabsInPane(paneId: string, tabIds: string[]) { + if (!persistenceKey) { + return; + } + reorderWorkspaceTabsInPane(persistenceKey, paneId, tabIds); + }, + [persistenceKey, reorderWorkspaceTabsInPane], + ); + + const renderSplitPaneEmptyState = useCallback(function renderSplitPaneEmptyState() { + return ( + + No tabs in this pane. + + ); + }, []); + return ( {Platform.OS === "web" && activeTabDescriptor ? ( @@ -2025,38 +2103,15 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) onCloseOtherTabs={handleCloseOtherTabsInPane} onSelectNewTabOption={handleSelectNewTabOption} newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID} - buildPaneContentModel={({ paneId, tab, isPaneFocused }) => - buildPaneContentModel({ - tab, - paneId, - isPaneFocused, - focusPaneBeforeOpen: true, - }) - } - onFocusPane={(paneId) => { - focusWorkspacePane(persistenceKey, paneId); - }} + buildPaneContentModel={buildDesktopPaneContentModel} + onFocusPane={handleFocusPane} onNewTerminalTab={handleCreateTerminal} - onSplitPane={(input) => { - splitWorkspacePane(persistenceKey, input); - }} - onSplitPaneEmpty={(input) => { - handleCreateDraftSplit(input); - }} - onMoveTabToPane={(tabId, toPaneId) => { - moveWorkspaceTabToPane(persistenceKey, tabId, toPaneId); - }} - onResizeSplit={(groupId, sizes) => { - resizeWorkspaceSplit(persistenceKey, groupId, sizes); - }} - onReorderTabsInPane={(paneId, tabIds) => { - reorderWorkspaceTabsInPane(persistenceKey, paneId, tabIds); - }} - renderPaneEmptyState={() => ( - - No tabs in this pane. - - )} + onSplitPane={handleSplitPane} + onSplitPaneEmpty={handleCreateDraftSplit} + onMoveTabToPane={handleMoveTabToPane} + onResizeSplit={handleResizePaneSplit} + onReorderTabsInPane={handleReorderTabsInPane} + renderPaneEmptyState={renderSplitPaneEmptyState} /> ) : ( content