Make the workspace right pane a first-class tab host (#3287)

* feat(app): add persistent workspace side panes

Keep the common review and file-browsing workflow beside the active agent without tearing down pane contents. Workspace tabs now share one compact chip language, assistant file links route into the retained right pane, and diff/file trees share stable geometry.

* fix(app): localize pull request empty state

* test(app): update explorer pane coverage

* fix(app): preserve explorer pane workflows

* test(app): make explorer tab selection idempotent

* fix(app): persist workspace review pins

* fix(app): preserve sidebar scrim drag hook

* fix(app): restore companion pane interactions

* test(app): follow selected sidebar surface
This commit is contained in:
Mohamed Boudra
2026-08-16 18:18:32 +02:00
committed by GitHub
parent 6dcf6a4b08
commit ab274d635e
81 changed files with 2900 additions and 1051 deletions
+5 -42
View File
@@ -5,9 +5,7 @@ import { isNative, isWeb } from "@/constants/platform";
import { MarkdownTextSpan } from "@/components/markdown-text";
import { MarkdownLinkText } from "@/components/markdown/link-text";
import { AssistantLinkPressProvider, type AssistantLinkPress } from "./link-press-context";
import { Shortcut } from "@/components/ui/shortcut";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useStableEvent } from "@/hooks/use-stable-event";
import { CODE_SURFACE_DATASET } from "@/styles/code-surface";
import { markdownCopyDataSet } from "@/assistant-selection-copy/markup";
import { useAssistantFileLinkResolverContext } from "./provider";
@@ -32,21 +30,13 @@ export function AssistantMarkdownLink({
monoSurface,
children,
}: AssistantMarkdownLinkProps) {
const { target, onHoverIn, onPress, onAuxPress } = useFileLink(source);
const { target, onHoverIn, onPress } = useFileLink(source);
const { configRef } = useAssistantFileLinkResolverContext();
const workspaceRoot = configRef.current.workspaceRoot;
const tooltipPath = useMemo(
() => (target ? formatInlinePathTargetForTooltip(target, workspaceRoot) : null),
[target, workspaceRoot],
);
const handleAnchorClickCapture = useStableEvent((event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
if (!isModifiedOpenEvent(event)) {
return;
}
event.stopPropagation();
onAuxPress();
});
const linkPress = useMemo<AssistantLinkPress>(
() => ({ onPress, accessibilityRole: "link" }),
[onPress],
@@ -93,7 +83,7 @@ export function AssistantMarkdownLink({
{...(unwrapForMarkdownCopy ? { "data-paseo-markdown-unwrap": "true" } : {})}
href={source.href}
title={source.title}
onClickCapture={handleAnchorClickCapture}
onClickCapture={preventAnchorNavigation}
onAuxClickCapture={preventAnchorNavigation}
style={LINK_ANCHOR_STYLE}
>
@@ -209,8 +199,6 @@ const FILE_LINK_TOOLTIP_TRIGGER_STYLE: ViewStyle = {
display: "inline-flex" as ViewStyle["display"],
};
const FILE_LINK_TOOLTIP_MOD_KEYS = ["mod"];
function FileLinkHoverTooltip({
filePath,
children,
@@ -228,17 +216,9 @@ function FileLinkHoverTooltip({
</TooltipTrigger>
{filePath ? (
<TooltipContent side="top" align="start" maxWidth={520}>
<View style={styles.tooltipBody}>
<Text selectable={false} style={styles.tooltipPath}>
{filePath}
</Text>
<View style={styles.tooltipHintRow}>
<Shortcut keys={FILE_LINK_TOOLTIP_MOD_KEYS} />
<Text selectable={false} style={styles.tooltipHintText}>
click for side pane
</Text>
</View>
</View>
<Text selectable={false} style={styles.tooltipPath}>
{filePath}
</Text>
</TooltipContent>
) : null}
</Tooltip>
@@ -255,27 +235,10 @@ function preventAnchorNavigation(event: MouseEvent<HTMLAnchorElement>): void {
event.preventDefault();
}
function isModifiedOpenEvent(event: MouseEvent<HTMLElement>): boolean {
return event.metaKey || event.ctrlKey;
}
const styles = StyleSheet.create((theme) => ({
tooltipBody: {
gap: theme.spacing[1],
},
tooltipPath: {
color: theme.colors.foreground,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
},
tooltipHintRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
tooltipHintText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
},
}));
@@ -136,7 +136,6 @@ describe("useFileLink", () => {
expect(result.current).toBe(first);
expect(result.current.onHoverIn).toBe(first.onHoverIn);
expect(result.current.onPress).toBe(first.onPress);
expect(result.current.onAuxPress).toBe(first.onAuxPress);
expect(result.current.open).toBe(first.open);
});
@@ -177,7 +176,7 @@ describe("useFileLink", () => {
lineStart: undefined,
lineEnd: undefined,
},
disposition: "main",
disposition: "side",
},
]);
});
@@ -20,7 +20,6 @@ export interface UseFileLinkResult {
target: InlinePathTarget | null;
onHoverIn: () => void;
onPress: () => void;
onAuxPress: () => void;
open: (source: AssistantFileLinkSource, disposition: OpenFileDisposition) => void;
}
@@ -118,9 +117,6 @@ export function useFileLink(source: AssistantFileLinkSource): UseFileLinkResult
});
const onPress = useStableEvent(() => {
open(stableSource, "main");
});
const onAuxPress = useStableEvent(() => {
open(stableSource, "side");
});
@@ -131,10 +127,7 @@ export function useFileLink(source: AssistantFileLinkSource): UseFileLinkResult
return query.data ?? null;
}, [query.data, resolution]);
return useMemo(
() => ({ target, onHoverIn, onPress, onAuxPress, open }),
[target, onHoverIn, onPress, onAuxPress, open],
);
return useMemo(() => ({ target, onHoverIn, onPress, open }), [target, onHoverIn, onPress, open]);
}
export function useAssistantFileLinkActions(): AssistantFileLinkActions {