Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 2 additions & 14 deletions apps/app/src/app/lib/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,9 @@ declare global {
forward?: () => Promise<void>;
reload?: () => Promise<void>;
setBounds?: (bounds: { x: number; y: number; width: number; height: number }) => Promise<void>;
getState?: () => Promise<{
url: string;
title: string;
canGoBack: boolean;
canGoForward: boolean;
isLoading: boolean;
} | null>;
getState?: () => Promise<{ url: string; title: string; canGoBack: boolean; canGoForward: boolean; isLoading: boolean } | null>;
destroy?: () => Promise<void>;
onStateChange?: (callback: (state: {
url: string;
title: string;
canGoBack: boolean;
canGoForward: boolean;
isLoading: boolean;
}) => void) => () => void;
onStateChange?: (callback: (state: { url: string; title: string; canGoBack: boolean; canGoForward: boolean; isLoading: boolean }) => void) => () => void;
};
meta?: {
initialDeepLinks?: string[];
Expand Down
10 changes: 4 additions & 6 deletions apps/app/src/react-app/domains/connections/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,14 +499,12 @@ export function createConnectionsStore(options: {
}

// For chrome-devtools in Electron, resolve the bundled binary so we
// don't need npx/npm at runtime.
// don't need npx/npm at runtime. Only carry over -- prefixed flags
// from the original command (skip npx flags like -y).
let resolvedCommand = entry.command;
if (slug === CHROME_DEVTOOLS_MCP_ID && isElectronRuntime()) {
const bundled = await resolveChromeDevtoolsMcpCommand();
// Preserve any extra args (e.g. --autoConnect) from the original
const extraArgs = entry.command.filter(
(arg) => arg.startsWith("--") || arg.startsWith("-"),
);
const extraArgs = entry.command.filter((arg) => arg.startsWith("--"));
resolvedCommand = [...bundled, ...extraArgs];
}
mcpEntryConfig["command"] = resolvedCommand;
Expand Down Expand Up @@ -589,7 +587,7 @@ export function createConnectionsStore(options: {
}
: {
type: "local" as const,
command: entry.command!,
command: (mcpEntryConfig["command"] as string[]) ?? entry.command!,
enabled: true,
...(mcpEnvironment ? { environment: mcpEnvironment } : {}),
};
Expand Down
179 changes: 53 additions & 126 deletions apps/app/src/react-app/domains/session/browser/browser-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
/** @jsxImportSource react */
import { useCallback, useEffect, useRef, useState } from "react";
import {
ArrowLeft,
ArrowRight,
Globe,
Loader2,
RotateCw,
X,
} from "lucide-react";
import { ArrowLeft, ArrowRight, Globe, Loader2, RotateCw, X } from "lucide-react";
import { isElectronRuntime } from "../../../../app/utils";

type BrowserState = {
Expand All @@ -18,119 +11,93 @@ type BrowserState = {
isLoading: boolean;
};

type BrowserPanelProps = {
onClose: () => void;
};
type BrowserPanelProps = { onClose: () => void };

const EMPTY_STATE: BrowserState = {
url: "",
title: "",
canGoBack: false,
canGoForward: false,
isLoading: false,
};
const EMPTY_STATE: BrowserState = { url: "", title: "", canGoBack: false, canGoForward: false, isLoading: false };
const TOOLBAR_HEIGHT = 44;

function getElectronBrowser() {
if (!isElectronRuntime()) return null;
return (window as Window).__OPENWORK_ELECTRON__?.browser ?? null;
}

function computeBounds(el: HTMLElement) {
const rect = el.getBoundingClientRect();
return {
x: Math.round(rect.x),
y: Math.round(rect.y + TOOLBAR_HEIGHT),
width: Math.round(rect.width),
height: Math.round(rect.height - TOOLBAR_HEIGHT),
};
}

export function BrowserPanel({ onClose }: BrowserPanelProps) {
const [state, setState] = useState<BrowserState>(EMPTY_STATE);
const [urlInput, setUrlInput] = useState("");
const [urlFocused, setUrlFocused] = useState(false);
const panelRef = useRef<HTMLDivElement>(null);
const urlInputRef = useRef<HTMLInputElement>(null);
const shownRef = useRef(false);

// Subscribe to state changes from the main process
useEffect(() => {
const browser = getElectronBrowser();
if (!browser) return;

const unsub = browser.onStateChange((newState: BrowserState) => {
setState(newState);
if (!urlFocused) {
setUrlInput(newState.url);
}
const unsub = browser.onStateChange?.((s: BrowserState) => {
setState(s);
if (!urlFocused) setUrlInput(s.url);
});

// Get initial state
browser.getState().then((initial: BrowserState | null) => {
if (initial) {
setState(initial);
setUrlInput(initial.url);
}
browser.getState?.().then((s: BrowserState | null) => {
if (s) { setState(s); setUrlInput(s.url); }
});

return unsub;
}, [urlFocused]);

// Show the browser view when the panel mounts, hide on unmount.
// Also update bounds when the panel resizes.
// Show the browser view when the panel mounts, keep bounds in sync, hide on unmount.
useEffect(() => {
const browser = getElectronBrowser();
if (!browser || !panelRef.current) return;

const updateBounds = () => {
const tryShow = () => {
if (!panelRef.current) return;
const rect = panelRef.current.getBoundingClientRect();
// The toolbar is ~44px, leave space at top for it
const toolbarHeight = 44;
const bounds = {
x: Math.round(rect.x),
y: Math.round(rect.y + toolbarHeight),
width: Math.round(rect.width),
height: Math.round(rect.height - toolbarHeight),
};
browser.setBounds(bounds);
const bounds = computeBounds(panelRef.current);
if (bounds.width < 1 || bounds.height < 1) return; // not laid out yet
if (!shownRef.current) {
browser.show?.(bounds);
shownRef.current = true;
} else {
browser.setBounds?.(bounds);
}
};

// Show with initial bounds
const rect = panelRef.current.getBoundingClientRect();
const toolbarHeight = 44;
browser.show({
x: Math.round(rect.x),
y: Math.round(rect.y + toolbarHeight),
width: Math.round(rect.width),
height: Math.round(rect.height - toolbarHeight),
});
// Initial show (may be zero-dimension if layout hasn't settled)
tryShow();

// Observe resize
const observer = new ResizeObserver(updateBounds);
const observer = new ResizeObserver(tryShow);
observer.observe(panelRef.current);

// Also update on window resize
window.addEventListener("resize", updateBounds);
window.addEventListener("resize", tryShow);

return () => {
observer.disconnect();
window.removeEventListener("resize", updateBounds);
browser.hide();
window.removeEventListener("resize", tryShow);
browser.hide?.();
shownRef.current = false;
};
}, []);

const navigate = useCallback(
(url?: string) => {
const browser = getElectronBrowser();
if (!browser) return;
browser.navigate(url ?? urlInput);
},
[urlInput],
);
const navigate = useCallback((url?: string) => {
getElectronBrowser()?.navigate?.(url ?? urlInput);
}, [urlInput]);

const handleUrlKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
navigate();
urlInputRef.current?.blur();
}
},
[navigate],
);
const handleUrlKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
navigate();
urlInputRef.current?.blur();
}
}, [navigate]);

const browser = getElectronBrowser();

if (!isElectronRuntime() || !browser) {
return (
<div className="flex h-full items-center justify-center p-4 text-center text-dls-secondary">
Expand All @@ -141,44 +108,16 @@ export function BrowserPanel({ onClose }: BrowserPanelProps) {

return (
<div ref={panelRef} className="flex h-full flex-col">
{/* Toolbar */}
<div className="flex h-[44px] shrink-0 items-center gap-1 border-b border-dls-border px-2">
{/* Navigation buttons */}
<button
type="button"
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text disabled:opacity-40"
onClick={() => browser.back()}
disabled={!state.canGoBack}
title="Back"
aria-label="Go back"
>
<button type="button" className="inline-flex h-7 w-7 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text disabled:opacity-40" onClick={() => browser.back?.()} disabled={!state.canGoBack} title="Back" aria-label="Go back">
<ArrowLeft className="h-4 w-4" />
</button>
<button
type="button"
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text disabled:opacity-40"
onClick={() => browser.forward()}
disabled={!state.canGoForward}
title="Forward"
aria-label="Go forward"
>
<button type="button" className="inline-flex h-7 w-7 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text disabled:opacity-40" onClick={() => browser.forward?.()} disabled={!state.canGoForward} title="Forward" aria-label="Go forward">
<ArrowRight className="h-4 w-4" />
</button>
<button
type="button"
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text"
onClick={() => browser.reload()}
title="Reload"
aria-label="Reload page"
>
{state.isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RotateCw className="h-4 w-4" />
)}
<button type="button" className="inline-flex h-7 w-7 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text" onClick={() => browser.reload?.()} title="Reload" aria-label="Reload page">
{state.isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RotateCw className="h-4 w-4" />}
</button>

{/* URL bar */}
<div className="relative mx-1 flex min-w-0 flex-1 items-center">
<Globe className="absolute left-2 h-3.5 w-3.5 text-dls-secondary" />
<input
Expand All @@ -188,29 +127,17 @@ export function BrowserPanel({ onClose }: BrowserPanelProps) {
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
onKeyDown={handleUrlKeyDown}
onFocus={() => {
setUrlFocused(true);
urlInputRef.current?.select();
}}
onFocus={() => { setUrlFocused(true); urlInputRef.current?.select(); }}
onBlur={() => setUrlFocused(false)}
placeholder="Enter URL..."
spellCheck={false}
autoComplete="off"
/>
</div>

{/* Close button */}
<button
type="button"
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text"
onClick={onClose}
title="Close browser"
aria-label="Close browser panel"
>
<button type="button" className="inline-flex h-7 w-7 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text" onClick={onClose} title="Close browser" aria-label="Close browser panel">
<X className="h-4 w-4" />
</button>
</div>

{/* WebContentsView renders in this area (managed by Electron main process) */}
<div className="min-h-0 flex-1" />
</div>
Expand Down
12 changes: 2 additions & 10 deletions apps/app/src/react-app/domains/session/chat/session-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,9 @@ export function SessionPage(props: SessionPageProps) {
const [deleteBusy, setDeleteBusy] = useState(false);
const [todoExpanded, setTodoExpanded] = useState(true);
const [browserPanelOpen, setBrowserPanelOpen] = useState(false);
const toggleBrowserPanel = useCallback(() => setBrowserPanelOpen((p) => !p), []);
const [showDelayedSessionLoadingState, setShowDelayedSessionLoadingState] = useState(false);

const toggleBrowserPanel = useCallback(() => {
setBrowserPanelOpen((prev) => !prev);
}, []);

const selectedSessionTitle = useMemo(
() => sessionTitleForId(props.sidebar.workspaceSessionGroups, props.selectedSessionId),
[props.selectedSessionId, props.sidebar.workspaceSessionGroups],
Expand Down Expand Up @@ -350,11 +347,7 @@ export function SessionPage(props: SessionPageProps) {
{isElectronRuntime() ? (
<button
type="button"
className={`flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-[13px] font-medium transition-colors ${
browserPanelOpen
? "bg-dls-accent/10 text-dls-accent"
: "text-gray-10 hover:bg-gray-2/70 hover:text-dls-text"
}`}
className={`flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-[13px] font-medium transition-colors ${browserPanelOpen ? "bg-dls-accent/10 text-dls-accent" : "text-gray-10 hover:bg-gray-2/70 hover:text-dls-text"}`}
onClick={toggleBrowserPanel}
title="Toggle browser panel"
aria-label="Toggle browser panel"
Expand Down Expand Up @@ -560,7 +553,6 @@ export function SessionPage(props: SessionPageProps) {
/>
</main>

{/* Embedded browser panel */}
{browserPanelOpen ? (
<aside
className="hidden min-h-0 shrink-0 overflow-hidden rounded-[24px] border border-dls-border bg-dls-surface shadow-[var(--dls-shell-shadow)] lg:flex lg:flex-col"
Expand Down
Loading
Loading