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
@@ -314,6 +314,7 @@ async function renderAgentPanel(
|
||||
isWorkspaceFocused: true,
|
||||
isPaneFocused: false,
|
||||
isInteractive: false,
|
||||
focusPane: vi.fn(),
|
||||
},
|
||||
) {
|
||||
const AgentPanel = agentPanelRegistration.component;
|
||||
@@ -462,6 +463,7 @@ describe("AgentPanel render isolation", () => {
|
||||
isWorkspaceFocused: false,
|
||||
isPaneFocused: true,
|
||||
isInteractive: false,
|
||||
focusPane: vi.fn(),
|
||||
});
|
||||
|
||||
expect(latestComposerIsPaneFocused.current).toBe(false);
|
||||
@@ -545,6 +547,7 @@ describe("AgentPanel render isolation", () => {
|
||||
isWorkspaceFocused: true,
|
||||
isPaneFocused: true,
|
||||
isInteractive: true,
|
||||
focusPane: vi.fn(),
|
||||
});
|
||||
|
||||
const timeoutAt = Date.now() + 300;
|
||||
|
||||
@@ -1264,6 +1264,9 @@ function ActiveAgentComposer({
|
||||
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
|
||||
const handleOpenWorkspaceAttachment = useCallback(
|
||||
(attachment: WorkspaceComposerAttachment) => {
|
||||
if (attachment.kind !== "review") {
|
||||
return;
|
||||
}
|
||||
const checkout = {
|
||||
serverId,
|
||||
cwd: attachment.attachment.cwd,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useMemo } from "react";
|
||||
import { Image } from "react-native";
|
||||
import { Globe } from "lucide-react-native";
|
||||
import invariant from "tiny-invariant";
|
||||
import { BrowserPane } from "@/components/browser-pane";
|
||||
import { usePaneContext, usePaneFocus } from "@/panels/pane-context";
|
||||
import type { PanelDescriptor, PanelIconProps, PanelRegistration } from "@/panels/panel-registry";
|
||||
import { useBrowserStore } from "@/stores/browser-store";
|
||||
import { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks";
|
||||
|
||||
function getBrowserLabel(input: { title: string; url: string }): string {
|
||||
const title = input.title.trim();
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(input.url);
|
||||
return parsed.hostname || input.url;
|
||||
} catch {
|
||||
return input.url;
|
||||
}
|
||||
}
|
||||
|
||||
function createBrowserTabIcon(faviconUrl: string | null) {
|
||||
return function BrowserTabIcon({ size, color }: PanelIconProps) {
|
||||
const source = useMemo(() => (faviconUrl ? { uri: faviconUrl } : undefined), []);
|
||||
const imageStyle = useMemo(() => ({ width: size, height: size, borderRadius: 3 }), [size]);
|
||||
|
||||
if (faviconUrl) {
|
||||
return <Image accessibilityIgnoresInvertColors source={source} style={imageStyle} />;
|
||||
}
|
||||
|
||||
return <Globe size={size} color={color} />;
|
||||
};
|
||||
}
|
||||
|
||||
function useBrowserPanelDescriptor(target: {
|
||||
kind: "browser";
|
||||
browserId: string;
|
||||
}): PanelDescriptor {
|
||||
const browser = useBrowserStore((state) => state.browsersById[target.browserId] ?? null);
|
||||
const url = browser?.url ?? "https://example.com";
|
||||
const icon = createBrowserTabIcon(browser?.faviconUrl ?? null);
|
||||
|
||||
return {
|
||||
label: getBrowserLabel({ title: browser?.title ?? "", url }),
|
||||
subtitle: url,
|
||||
titleState: "ready",
|
||||
icon,
|
||||
statusBucket: browser?.isLoading ? "running" : null,
|
||||
};
|
||||
}
|
||||
|
||||
function BrowserPanel() {
|
||||
const { serverId, workspaceId, target } = usePaneContext();
|
||||
const { focusPane, isInteractive } = usePaneFocus();
|
||||
const workspaceAuthority = useWorkspaceExecutionAuthority(serverId, workspaceId)!;
|
||||
const cwd = workspaceAuthority.ok ? workspaceAuthority.authority.workspaceDirectory : null;
|
||||
invariant(target.kind === "browser", "BrowserPanel requires browser target");
|
||||
return (
|
||||
<BrowserPane
|
||||
browserId={target.browserId}
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
cwd={cwd}
|
||||
isInteractive={isInteractive}
|
||||
onFocusPane={focusPane}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const browserPanelRegistration: PanelRegistration<"browser"> = {
|
||||
kind: "browser",
|
||||
component: BrowserPanel,
|
||||
useDescriptor: useBrowserPanelDescriptor,
|
||||
};
|
||||
@@ -12,6 +12,7 @@ describe("createPaneFocusContextValue", () => {
|
||||
isWorkspaceFocused: true,
|
||||
isPaneFocused: true,
|
||||
isInteractive: true,
|
||||
focusPane: expect.any(Function),
|
||||
});
|
||||
expect(
|
||||
createPaneFocusContextValue({
|
||||
@@ -22,6 +23,7 @@ describe("createPaneFocusContextValue", () => {
|
||||
isWorkspaceFocused: false,
|
||||
isPaneFocused: true,
|
||||
isInteractive: false,
|
||||
focusPane: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,19 +17,23 @@ export interface PaneFocusContextValue {
|
||||
isWorkspaceFocused: boolean;
|
||||
isPaneFocused: boolean;
|
||||
isInteractive: boolean;
|
||||
focusPane(): void;
|
||||
}
|
||||
|
||||
const PaneContext = createContext<PaneContextValue | null>(null);
|
||||
const PaneFocusContext = createContext<PaneFocusContextValue | null>(null);
|
||||
const noopFocusPane = () => {};
|
||||
|
||||
export function createPaneFocusContextValue(input: {
|
||||
isWorkspaceFocused: boolean;
|
||||
isPaneFocused: boolean;
|
||||
onFocusPane?: () => void;
|
||||
}): PaneFocusContextValue {
|
||||
return {
|
||||
isWorkspaceFocused: input.isWorkspaceFocused,
|
||||
isPaneFocused: input.isPaneFocused,
|
||||
isInteractive: input.isWorkspaceFocused && input.isPaneFocused,
|
||||
focusPane: input.onFocusPane ?? noopFocusPane,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { agentPanelRegistration } from "@/panels/agent-panel";
|
||||
import { browserPanelRegistration } from "@/panels/browser-panel";
|
||||
import { draftPanelRegistration } from "@/panels/draft-panel";
|
||||
import { filePanelRegistration } from "@/panels/file-panel";
|
||||
import { registerPanel } from "@/panels/panel-registry";
|
||||
@@ -15,6 +16,7 @@ export function ensurePanelsRegistered(): void {
|
||||
registerPanel(agentPanelRegistration);
|
||||
registerPanel(setupPanelRegistration);
|
||||
registerPanel(terminalPanelRegistration);
|
||||
registerPanel(browserPanelRegistration);
|
||||
registerPanel(filePanelRegistration);
|
||||
panelsRegistered = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user