Add browser pane element picker (#670)
* feat: add browser pane element picker Port the browser pane from PR #198 and route picked elements through workspace attachments so the context follows every agent composer in a workspace. Co-authored-by: Jason Kneen <jason.kneen@bouncingfish.com> * docs: document electron platform overlays * fix: harden browser pane navigation * fix: polish browser tab interactions * fix: route browser pane focus shortcuts --------- Co-authored-by: Jason Kneen <jason.kneen@bouncingfish.com>
This commit is contained in:
co-authored by
Jason Kneen
parent
34b95e7f01
commit
906205f97a
@@ -24,6 +24,7 @@ import {
|
||||
Copy,
|
||||
RotateCw,
|
||||
Rows2,
|
||||
Globe,
|
||||
SquarePen,
|
||||
SquareTerminal,
|
||||
X,
|
||||
@@ -72,6 +73,7 @@ const ThemedArrowRightToLine = withUnistyles(ArrowRightToLine);
|
||||
const ThemedCopyX = withUnistyles(CopyX);
|
||||
const ThemedSquarePen = withUnistyles(SquarePen);
|
||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||
const ThemedGlobe = withUnistyles(Globe);
|
||||
const ThemedColumns2 = withUnistyles(Columns2);
|
||||
const ThemedRows2 = withUnistyles(Rows2);
|
||||
|
||||
@@ -153,6 +155,8 @@ interface WorkspaceDesktopTabsRowProps {
|
||||
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
||||
onCreateDraftTab: (input: { paneId?: string }) => void;
|
||||
onCreateTerminalTab: (input: { paneId?: string }) => void;
|
||||
onCreateBrowserTab: (input: { paneId?: string }) => void;
|
||||
showCreateBrowserTab?: boolean;
|
||||
disableCreateTerminal?: boolean;
|
||||
isWaitingOnTerminalReadiness?: boolean;
|
||||
onReorderTabs: (nextTabs: WorkspaceTabDescriptor[]) => void;
|
||||
@@ -468,6 +472,8 @@ export function WorkspaceDesktopTabsRow({
|
||||
onCloseOtherTabs,
|
||||
onCreateDraftTab,
|
||||
onCreateTerminalTab,
|
||||
onCreateBrowserTab,
|
||||
showCreateBrowserTab = false,
|
||||
disableCreateTerminal = false,
|
||||
isWaitingOnTerminalReadiness = false,
|
||||
onReorderTabs,
|
||||
@@ -549,6 +555,10 @@ export function WorkspaceDesktopTabsRow({
|
||||
onCreateTerminalTab({ paneId });
|
||||
}, [onCreateTerminalTab, paneId]);
|
||||
|
||||
const handleCreateBrowser = useCallback(() => {
|
||||
onCreateBrowserTab({ paneId });
|
||||
}, [onCreateBrowserTab, paneId]);
|
||||
|
||||
const terminalDisabled = disableCreateTerminal || isWaitingOnTerminalReadiness;
|
||||
const newTerminalActionButtonStyle = useCallback(
|
||||
({ hovered, pressed }: PressableStateCallbackType) => [
|
||||
@@ -707,6 +717,24 @@ export function WorkspaceDesktopTabsRow({
|
||||
</View>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{showCreateBrowserTab ? (
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger
|
||||
testID="workspace-new-browser"
|
||||
onPress={handleCreateBrowser}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="New browser tab"
|
||||
style={newTabActionButtonStyle}
|
||||
>
|
||||
<ThemedGlobe size={14} uniProps={mutedColorMapping} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="center" offset={8}>
|
||||
<View style={styles.newTabTooltipRow}>
|
||||
<Text style={styles.newTabTooltipText}>New browser tab</Text>
|
||||
</View>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{showPaneSplitActions ? (
|
||||
<>
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
|
||||
@@ -342,6 +342,9 @@ export function WorkspaceDraftAgentTab({
|
||||
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
|
||||
const handleOpenWorkspaceAttachment = useCallback(
|
||||
(attachment: WorkspaceComposerAttachment) => {
|
||||
if (attachment.kind !== "review") {
|
||||
return;
|
||||
}
|
||||
const checkout = {
|
||||
serverId,
|
||||
cwd: attachment.attachment.cwd,
|
||||
|
||||
@@ -110,11 +110,13 @@ describe("WorkspacePaneContent", () => {
|
||||
isWorkspaceFocused: true,
|
||||
isPaneFocused: false,
|
||||
isInteractive: false,
|
||||
focusPane: expect.any(Function),
|
||||
});
|
||||
expect(snapshots[1]?.focus).toEqual({
|
||||
isWorkspaceFocused: true,
|
||||
isPaneFocused: true,
|
||||
isInteractive: true,
|
||||
focusPane: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,12 +58,14 @@ export interface WorkspacePaneContentProps {
|
||||
content: WorkspacePaneContentModel;
|
||||
isWorkspaceFocused: boolean;
|
||||
isPaneFocused: boolean;
|
||||
onFocusPane?: () => void;
|
||||
}
|
||||
|
||||
export function WorkspacePaneContent({
|
||||
content,
|
||||
isWorkspaceFocused,
|
||||
isPaneFocused,
|
||||
onFocusPane,
|
||||
}: WorkspacePaneContentProps) {
|
||||
const { Component, key, paneContextValue } = content;
|
||||
const paneFocusValue = useMemo(
|
||||
@@ -71,8 +73,9 @@ export function WorkspacePaneContent({
|
||||
createPaneFocusContextValue({
|
||||
isWorkspaceFocused,
|
||||
isPaneFocused,
|
||||
onFocusPane,
|
||||
}),
|
||||
[isPaneFocused, isWorkspaceFocused],
|
||||
[isPaneFocused, isWorkspaceFocused, onFocusPane],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Copy,
|
||||
Ellipsis,
|
||||
EllipsisVertical,
|
||||
Globe,
|
||||
PanelRight,
|
||||
RotateCw,
|
||||
Settings,
|
||||
@@ -80,6 +81,7 @@ import { upsertTerminalListEntry } from "@/utils/terminal-list";
|
||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
import { createWorkspaceBrowser } from "@/stores/browser-store";
|
||||
import { buildProviderCommand } from "@/utils/provider-command-templates";
|
||||
import { generateDraftId } from "@/stores/draft-keys";
|
||||
import {
|
||||
@@ -128,7 +130,7 @@ import {
|
||||
import { findAdjacentPane } from "@/utils/split-navigation";
|
||||
import { isAbsolutePath } from "@/utils/path";
|
||||
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
||||
import { isWeb, isNative } from "@/constants/platform";
|
||||
import { getIsElectron, isNative, isWeb } from "@/constants/platform";
|
||||
import { useContainerWidthBelow } from "@/hooks/use-container-width";
|
||||
|
||||
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
||||
@@ -149,6 +151,7 @@ const ThemedCopyX = withUnistyles(CopyX);
|
||||
const ThemedX = withUnistyles(X);
|
||||
const ThemedSquarePen = withUnistyles(SquarePen);
|
||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||
const ThemedGlobe = withUnistyles(Globe);
|
||||
const ThemedSettings = withUnistyles(Settings);
|
||||
const ThemedPanelRight = withUnistyles(PanelRight);
|
||||
const ThemedSourceControlPanelIcon = withUnistyles(SourceControlPanelIcon);
|
||||
@@ -160,6 +163,7 @@ const sourceControlPanelStrokeWidth15 = { strokeWidth: 1.5 };
|
||||
|
||||
const MENU_NEW_AGENT_ICON = <ThemedSquarePen size={16} uniProps={mutedColorMapping} />;
|
||||
const MENU_NEW_TERMINAL_ICON = <ThemedSquareTerminal size={16} uniProps={mutedColorMapping} />;
|
||||
const MENU_NEW_BROWSER_ICON = <ThemedGlobe size={16} uniProps={mutedColorMapping} />;
|
||||
const MENU_COPY_ICON = <ThemedCopy size={16} uniProps={mutedColorMapping} />;
|
||||
const MENU_SETTINGS_ICON = <ThemedSettings size={16} uniProps={mutedColorMapping} />;
|
||||
|
||||
@@ -199,6 +203,9 @@ function getFallbackTabOptionLabel(tab: WorkspaceTabDescriptor): string {
|
||||
if (tab.target.kind === "terminal") {
|
||||
return "Terminal";
|
||||
}
|
||||
if (tab.target.kind === "browser") {
|
||||
return "Browser";
|
||||
}
|
||||
if (tab.target.kind === "file") {
|
||||
return tab.target.path.split("/").findLast(Boolean) ?? tab.target.path;
|
||||
}
|
||||
@@ -218,6 +225,9 @@ function getFallbackTabOptionDescription(tab: WorkspaceTabDescriptor): string {
|
||||
if (tab.target.kind === "terminal") {
|
||||
return "Terminal";
|
||||
}
|
||||
if (tab.target.kind === "browser") {
|
||||
return "Browser";
|
||||
}
|
||||
return tab.target.path;
|
||||
}
|
||||
|
||||
@@ -723,14 +733,17 @@ interface WorkspaceHeaderMenuProps {
|
||||
normalizedWorkspaceId: string;
|
||||
currentBranchName: string | null;
|
||||
showWorkspaceSetup: boolean;
|
||||
showCreateBrowserTab: boolean;
|
||||
isMobile: boolean;
|
||||
createTerminalDisabled: boolean;
|
||||
menuNewAgentIcon: ReactElement;
|
||||
menuNewTerminalIcon: ReactElement;
|
||||
menuNewBrowserIcon: ReactElement;
|
||||
menuCopyIcon: ReactElement;
|
||||
menuSettingsIcon: ReactElement;
|
||||
onCreateDraftTab: () => void;
|
||||
onCreateTerminal: () => void;
|
||||
onCreateBrowser: () => void;
|
||||
onCopyWorkspacePath: () => void;
|
||||
onCopyBranchName: () => void;
|
||||
onOpenSetupTab: () => void;
|
||||
@@ -754,14 +767,17 @@ function WorkspaceHeaderMenu({
|
||||
normalizedWorkspaceId,
|
||||
currentBranchName,
|
||||
showWorkspaceSetup,
|
||||
showCreateBrowserTab,
|
||||
isMobile,
|
||||
createTerminalDisabled,
|
||||
menuNewAgentIcon,
|
||||
menuNewTerminalIcon,
|
||||
menuNewBrowserIcon,
|
||||
menuCopyIcon,
|
||||
menuSettingsIcon,
|
||||
onCreateDraftTab,
|
||||
onCreateTerminal,
|
||||
onCreateBrowser,
|
||||
onCopyWorkspacePath,
|
||||
onCopyBranchName,
|
||||
onOpenSetupTab,
|
||||
@@ -799,6 +815,15 @@ function WorkspaceHeaderMenu({
|
||||
>
|
||||
New terminal
|
||||
</DropdownMenuItem>
|
||||
{showCreateBrowserTab ? (
|
||||
<DropdownMenuItem
|
||||
testID="workspace-header-new-browser"
|
||||
leading={menuNewBrowserIcon}
|
||||
onSelect={onCreateBrowser}
|
||||
>
|
||||
New browser tab
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
testID="workspace-header-copy-path"
|
||||
leading={menuCopyIcon}
|
||||
@@ -843,14 +868,17 @@ interface WorkspaceHeaderTitleBarProps {
|
||||
normalizedServerId: string;
|
||||
normalizedWorkspaceId: string;
|
||||
showWorkspaceSetup: boolean;
|
||||
showCreateBrowserTab: boolean;
|
||||
isMobile: boolean;
|
||||
createTerminalDisabled: boolean;
|
||||
menuNewAgentIcon: ReactElement;
|
||||
menuNewTerminalIcon: ReactElement;
|
||||
menuNewBrowserIcon: ReactElement;
|
||||
menuCopyIcon: ReactElement;
|
||||
menuSettingsIcon: ReactElement;
|
||||
onCreateDraftTab: () => void;
|
||||
onCreateTerminal: () => void;
|
||||
onCreateBrowser: () => void;
|
||||
onCopyWorkspacePath: () => void;
|
||||
onCopyBranchName: () => void;
|
||||
onOpenSetupTab: () => void;
|
||||
@@ -866,14 +894,17 @@ function WorkspaceHeaderTitleBar({
|
||||
normalizedServerId,
|
||||
normalizedWorkspaceId,
|
||||
showWorkspaceSetup,
|
||||
showCreateBrowserTab,
|
||||
isMobile,
|
||||
createTerminalDisabled,
|
||||
menuNewAgentIcon,
|
||||
menuNewTerminalIcon,
|
||||
menuNewBrowserIcon,
|
||||
menuCopyIcon,
|
||||
menuSettingsIcon,
|
||||
onCreateDraftTab,
|
||||
onCreateTerminal,
|
||||
onCreateBrowser,
|
||||
onCopyWorkspacePath,
|
||||
onCopyBranchName,
|
||||
onOpenSetupTab,
|
||||
@@ -908,14 +939,17 @@ function WorkspaceHeaderTitleBar({
|
||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||
currentBranchName={currentBranchName}
|
||||
showWorkspaceSetup={showWorkspaceSetup}
|
||||
showCreateBrowserTab={showCreateBrowserTab}
|
||||
isMobile={isMobile}
|
||||
createTerminalDisabled={createTerminalDisabled}
|
||||
menuNewAgentIcon={menuNewAgentIcon}
|
||||
menuNewTerminalIcon={menuNewTerminalIcon}
|
||||
menuNewBrowserIcon={menuNewBrowserIcon}
|
||||
menuCopyIcon={menuCopyIcon}
|
||||
menuSettingsIcon={menuSettingsIcon}
|
||||
onCreateDraftTab={onCreateDraftTab}
|
||||
onCreateTerminal={onCreateTerminal}
|
||||
onCreateBrowser={onCreateBrowser}
|
||||
onCopyWorkspacePath={onCopyWorkspacePath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onOpenSetupTab={onOpenSetupTab}
|
||||
@@ -1928,6 +1962,20 @@ function WorkspaceScreenContent({
|
||||
toast.show("Preparing workspace, opening terminal when ready...");
|
||||
});
|
||||
|
||||
const handleCreateBrowserTab = useCallback(
|
||||
(input?: { paneId?: string }) => {
|
||||
if (!persistenceKey || !getIsElectron()) {
|
||||
return;
|
||||
}
|
||||
if (input?.paneId) {
|
||||
focusWorkspacePane(persistenceKey, input.paneId);
|
||||
}
|
||||
const { browserId } = createWorkspaceBrowser();
|
||||
openWorkspaceTabFocused(persistenceKey, { kind: "browser", browserId });
|
||||
},
|
||||
[focusWorkspacePane, openWorkspaceTabFocused, persistenceKey],
|
||||
);
|
||||
|
||||
const handleSelectSwitcherTab = useCallback(
|
||||
(key: string) => {
|
||||
navigateToTabId(key);
|
||||
@@ -2823,6 +2871,7 @@ function WorkspaceScreenContent({
|
||||
() => createTerminalMutation.isPending || pendingTerminalCreateInput !== null,
|
||||
[createTerminalMutation.isPending, pendingTerminalCreateInput],
|
||||
);
|
||||
const showCreateBrowserTab = getIsElectron();
|
||||
const focusedPaneIdOrUndefined = useMemo(() => focusedPaneId ?? undefined, [focusedPaneId]);
|
||||
const desktopFocusModeEnabled = useMemo(
|
||||
() => isFocusModeEnabled && !isMobile,
|
||||
@@ -2855,6 +2904,8 @@ function WorkspaceScreenContent({
|
||||
onCloseOtherTabs={handleCloseOtherTabsInPane}
|
||||
onCreateDraftTab={handleCreateDraftTab}
|
||||
onCreateTerminalTab={handleCreateTerminal}
|
||||
onCreateBrowserTab={handleCreateBrowserTab}
|
||||
showCreateBrowserTab={showCreateBrowserTab}
|
||||
buildPaneContentModel={buildDesktopPaneContentModel}
|
||||
onFocusPane={handleFocusPane}
|
||||
onSplitPane={handleSplitPane}
|
||||
@@ -2887,6 +2938,8 @@ function WorkspaceScreenContent({
|
||||
handleCloseOtherTabsInPane,
|
||||
handleCreateDraftTab,
|
||||
handleCreateTerminal,
|
||||
handleCreateBrowserTab,
|
||||
showCreateBrowserTab,
|
||||
buildDesktopPaneContentModel,
|
||||
handleFocusPane,
|
||||
handleSplitPane,
|
||||
@@ -2932,14 +2985,17 @@ function WorkspaceScreenContent({
|
||||
normalizedServerId={normalizedServerId}
|
||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||
showWorkspaceSetup={showWorkspaceSetup}
|
||||
showCreateBrowserTab={showCreateBrowserTab}
|
||||
isMobile={isMobile}
|
||||
createTerminalDisabled={createTerminalDisabled}
|
||||
menuNewAgentIcon={menuNewAgentIcon}
|
||||
menuNewTerminalIcon={menuNewTerminalIcon}
|
||||
menuNewBrowserIcon={MENU_NEW_BROWSER_ICON}
|
||||
menuCopyIcon={menuCopyIcon}
|
||||
menuSettingsIcon={menuSettingsIcon}
|
||||
onCreateDraftTab={handleCreateDraftTab}
|
||||
onCreateTerminal={handleCreateTerminal}
|
||||
onCreateBrowser={handleCreateBrowserTab}
|
||||
onCopyWorkspacePath={handleCopyWorkspacePath}
|
||||
onCopyBranchName={handleCopyBranchName}
|
||||
onOpenSetupTab={handleOpenSetupTab}
|
||||
@@ -2989,6 +3045,8 @@ function WorkspaceScreenContent({
|
||||
onCloseOtherTabs={handleCloseOtherTabs}
|
||||
onCreateDraftTab={handleCreateDraftTab}
|
||||
onCreateTerminalTab={handleCreateTerminal}
|
||||
onCreateBrowserTab={handleCreateBrowserTab}
|
||||
showCreateBrowserTab={showCreateBrowserTab}
|
||||
disableCreateTerminal={createTerminalMutation.isPending}
|
||||
isWaitingOnTerminalReadiness={pendingTerminalCreateInput !== null}
|
||||
onReorderTabs={handleReorderTabsInFocusedPane}
|
||||
|
||||
@@ -81,6 +81,9 @@ function getCloseButtonTestId(tab: WorkspaceTabDescriptor): string {
|
||||
if (tab.target.kind === "draft") {
|
||||
return `workspace-draft-close-${tab.target.draftId}`;
|
||||
}
|
||||
if (tab.target.kind === "browser") {
|
||||
return `workspace-browser-close-${tab.target.browserId}`;
|
||||
}
|
||||
if (tab.target.kind === "setup") {
|
||||
return `workspace-setup-close-${encodeFilePathForPathSegment(tab.target.workspaceId)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user