Stabilize workspace stream render boundaries
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
@@ -63,6 +64,7 @@ import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
|||||||
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
||||||
import { normalizeInlinePathTarget } from "@/utils/inline-path";
|
import { normalizeInlinePathTarget } from "@/utils/inline-path";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import {
|
import {
|
||||||
getWorkingIndicatorDotStrength,
|
getWorkingIndicatorDotStrength,
|
||||||
WORKING_INDICATOR_CYCLE_MS,
|
WORKING_INDICATOR_CYCLE_MS,
|
||||||
@@ -88,7 +90,7 @@ export interface AgentStreamViewProps {
|
|||||||
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
|
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(
|
const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(
|
||||||
function AgentStreamView(
|
function AgentStreamView(
|
||||||
{
|
{
|
||||||
agentId,
|
agentId,
|
||||||
@@ -136,6 +138,11 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
workspaceRoot,
|
workspaceRoot,
|
||||||
});
|
});
|
||||||
|
const openWorkspaceFile = useStableEvent(function openWorkspaceFile(input: {
|
||||||
|
filePath: string;
|
||||||
|
}) {
|
||||||
|
onOpenWorkspaceFile?.(input);
|
||||||
|
});
|
||||||
// Keep entry/exit animations off on Android due to RN dispatchDraw crashes
|
// Keep entry/exit animations off on Android due to RN dispatchDraw crashes
|
||||||
// tracked in react-native-reanimated#8422.
|
// tracked in react-native-reanimated#8422.
|
||||||
const shouldDisableEntryExitAnimations = Platform.OS === "android";
|
const shouldDisableEntryExitAnimations = Platform.OS === "android";
|
||||||
@@ -164,7 +171,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
|
|
||||||
if (normalized.file) {
|
if (normalized.file) {
|
||||||
if (onOpenWorkspaceFile) {
|
if (onOpenWorkspaceFile) {
|
||||||
onOpenWorkspaceFile({ filePath: normalized.file });
|
openWorkspaceFile({ filePath: normalized.file });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +204,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
resolvedServerId,
|
resolvedServerId,
|
||||||
router,
|
router,
|
||||||
setExplorerTabForCheckout,
|
setExplorerTabForCheckout,
|
||||||
onOpenWorkspaceFile,
|
openWorkspaceFile,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -657,6 +664,9 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const AgentStreamView = memo(AgentStreamViewComponent);
|
||||||
|
AgentStreamView.displayName = "AgentStreamView";
|
||||||
|
|
||||||
function WorkingIndicator() {
|
function WorkingIndicator() {
|
||||||
const progress = useSharedValue(0);
|
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 { useDelayedHistoryRefreshToast } from "@/hooks/use-delayed-history-refresh-toast";
|
||||||
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
|
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
|
||||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||||
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import { usePaneContext } from "@/panels/pane-context";
|
import { usePaneContext } from "@/panels/pane-context";
|
||||||
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
|
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
|
||||||
import {
|
import {
|
||||||
@@ -103,12 +104,13 @@ function useAgentPanelDescriptor(
|
|||||||
function AgentPanel() {
|
function AgentPanel() {
|
||||||
const { serverId, target, isPaneFocused, openFileInWorkspace } = usePaneContext();
|
const { serverId, target, isPaneFocused, openFileInWorkspace } = usePaneContext();
|
||||||
invariant(target.kind === "agent", "AgentPanel requires agent target");
|
invariant(target.kind === "agent", "AgentPanel requires agent target");
|
||||||
const handleOpenWorkspaceFile = useCallback(
|
|
||||||
(input: { filePath: string }) => {
|
function openWorkspaceFile(input: { filePath: string }) {
|
||||||
openFileInWorkspace(input.filePath);
|
openFileInWorkspace(input.filePath);
|
||||||
},
|
}
|
||||||
[openFileInWorkspace],
|
|
||||||
);
|
const handleOpenWorkspaceFile = useStableEvent(openWorkspaceFile);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgentPanelContent
|
<AgentPanelContent
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
|
|||||||
@@ -1774,6 +1774,84 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
<WorkspacePaneContent content={activePaneContent!} />
|
<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 (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: mainBackgroundColor }]}>
|
<View style={[styles.container, { backgroundColor: mainBackgroundColor }]}>
|
||||||
{Platform.OS === "web" && activeTabDescriptor ? (
|
{Platform.OS === "web" && activeTabDescriptor ? (
|
||||||
@@ -2025,38 +2103,15 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
onCloseOtherTabs={handleCloseOtherTabsInPane}
|
onCloseOtherTabs={handleCloseOtherTabsInPane}
|
||||||
onSelectNewTabOption={handleSelectNewTabOption}
|
onSelectNewTabOption={handleSelectNewTabOption}
|
||||||
newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID}
|
newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID}
|
||||||
buildPaneContentModel={({ paneId, tab, isPaneFocused }) =>
|
buildPaneContentModel={buildDesktopPaneContentModel}
|
||||||
buildPaneContentModel({
|
onFocusPane={handleFocusPane}
|
||||||
tab,
|
|
||||||
paneId,
|
|
||||||
isPaneFocused,
|
|
||||||
focusPaneBeforeOpen: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onFocusPane={(paneId) => {
|
|
||||||
focusWorkspacePane(persistenceKey, paneId);
|
|
||||||
}}
|
|
||||||
onNewTerminalTab={handleCreateTerminal}
|
onNewTerminalTab={handleCreateTerminal}
|
||||||
onSplitPane={(input) => {
|
onSplitPane={handleSplitPane}
|
||||||
splitWorkspacePane(persistenceKey, input);
|
onSplitPaneEmpty={handleCreateDraftSplit}
|
||||||
}}
|
onMoveTabToPane={handleMoveTabToPane}
|
||||||
onSplitPaneEmpty={(input) => {
|
onResizeSplit={handleResizePaneSplit}
|
||||||
handleCreateDraftSplit(input);
|
onReorderTabsInPane={handleReorderTabsInPane}
|
||||||
}}
|
renderPaneEmptyState={renderSplitPaneEmptyState}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
content
|
content
|
||||||
|
|||||||
Reference in New Issue
Block a user