Stabilize workspace stream render boundaries
This commit is contained in:
@@ -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<AgentStreamViewHandle, AgentStreamViewProps>(
|
||||
const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(
|
||||
function AgentStreamView(
|
||||
{
|
||||
agentId,
|
||||
@@ -136,6 +138,11 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
||||
workspaceId,
|
||||
workspaceRoot,
|
||||
});
|
||||
const openWorkspaceFile = useStableEvent(function openWorkspaceFile(input: {
|
||||
filePath: string;
|
||||
}) {
|
||||
onOpenWorkspaceFile?.(input);
|
||||
});
|
||||
// Keep entry/exit animations off on Android due to RN dispatchDraw crashes
|
||||
// tracked in react-native-reanimated#8422.
|
||||
const shouldDisableEntryExitAnimations = Platform.OS === "android";
|
||||
@@ -164,7 +171,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
||||
|
||||
if (normalized.file) {
|
||||
if (onOpenWorkspaceFile) {
|
||||
onOpenWorkspaceFile({ filePath: normalized.file });
|
||||
openWorkspaceFile({ filePath: normalized.file });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,7 +204,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
||||
resolvedServerId,
|
||||
router,
|
||||
setExplorerTabForCheckout,
|
||||
onOpenWorkspaceFile,
|
||||
openWorkspaceFile,
|
||||
workspaceId,
|
||||
],
|
||||
);
|
||||
@@ -657,6 +664,9 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
||||
},
|
||||
);
|
||||
|
||||
export const AgentStreamView = memo(AgentStreamViewComponent);
|
||||
AgentStreamView.displayName = "AgentStreamView";
|
||||
|
||||
function WorkingIndicator() {
|
||||
const progress = useSharedValue(0);
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
export function useStableEvent<TArgs extends unknown[], TResult>(
|
||||
handler: (...args: TArgs) => TResult,
|
||||
): (...args: TArgs) => TResult {
|
||||
const handlerRef = useRef(handler);
|
||||
handlerRef.current = handler;
|
||||
|
||||
return useCallback((...args: TArgs) => handlerRef.current(...args), []);
|
||||
}
|
||||
@@ -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 (
|
||||
<AgentPanelContent
|
||||
serverId={serverId}
|
||||
|
||||
@@ -1774,6 +1774,84 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
<WorkspacePaneContent content={activePaneContent!} />
|
||||
);
|
||||
|
||||
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 (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyStateText}>No tabs in this pane.</Text>
|
||||
</View>
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: mainBackgroundColor }]}>
|
||||
{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={() => (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyStateText}>No tabs in this pane.</Text>
|
||||
</View>
|
||||
)}
|
||||
onSplitPane={handleSplitPane}
|
||||
onSplitPaneEmpty={handleCreateDraftSplit}
|
||||
onMoveTabToPane={handleMoveTabToPane}
|
||||
onResizeSplit={handleResizePaneSplit}
|
||||
onReorderTabsInPane={handleReorderTabsInPane}
|
||||
renderPaneEmptyState={renderSplitPaneEmptyState}
|
||||
/>
|
||||
) : (
|
||||
content
|
||||
|
||||
Reference in New Issue
Block a user