diff --git a/apps/desktop/src/lib/errors.ts b/apps/desktop/src/lib/errors.ts new file mode 100644 index 00000000000..3636fba9c5a --- /dev/null +++ b/apps/desktop/src/lib/errors.ts @@ -0,0 +1,6 @@ +export class SessionDisposedError extends Error { + constructor() { + super("TypeScript session disposed"); + this.name = "SessionDisposedError"; + } +} diff --git a/apps/desktop/src/lib/trpc/index.ts b/apps/desktop/src/lib/trpc/index.ts index 281adbe8a17..2ebbe01081f 100644 --- a/apps/desktop/src/lib/trpc/index.ts +++ b/apps/desktop/src/lib/trpc/index.ts @@ -1,6 +1,7 @@ import { createTRPCReact } from "@trpc/react-query"; import { initTRPC } from "@trpc/server"; import superjson from "superjson"; +import { SessionDisposedError } from "../errors"; import type { AppRouter } from "./routers"; import { NotGitRepoError } from "./routers/workspaces/utils/git"; import { WorktreePathMissingError } from "./routers/workspaces/utils/git-client"; @@ -40,8 +41,10 @@ const sentryMiddleware = t.middleware(async ({ next, path, type }) => { if ( originalError instanceof NotGitRepoError || originalError instanceof WorktreePathMissingError || + originalError instanceof SessionDisposedError || errorName === "NotGitRepoError" || - errorName === "WorktreePathMissingError" + errorName === "WorktreePathMissingError" || + errorName === "SessionDisposedError" ) { return result; } diff --git a/apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts b/apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts index 8e9fa7577cc..c0a620653e0 100644 --- a/apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts +++ b/apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts @@ -2,6 +2,7 @@ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; +import { SessionDisposedError } from "lib/errors"; import { resolveShikiLanguageFromFilePath } from "shared/language-registry"; import { languageDiagnosticsStore } from "../../diagnostics-store"; import type { @@ -531,7 +532,7 @@ export class TypeScriptLanguageProvider implements LanguageServiceProvider { } for (const request of session.requestResolvers.values()) { - request.reject(new Error("TypeScript session disposed")); + request.reject(new SessionDisposedError()); } session.requestResolvers.clear(); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserAutomationList/BrowserAutomationList.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserAutomationList/BrowserAutomationList.tsx index 69cd0be9efd..a2b314dab76 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserAutomationList/BrowserAutomationList.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserAutomationList/BrowserAutomationList.tsx @@ -8,6 +8,7 @@ import { } from "@superset/ui/dialog"; import { cn } from "@superset/ui/utils"; import { useMemo } from "react"; +import { LuArrowLeft } from "react-icons/lu"; import { useBrowserAutomationData } from "renderer/hooks/useBrowserAutomationData"; import { useBrowserAutomationStore } from "renderer/stores/browser-automation"; import { useTabsStore } from "renderer/stores/tabs/store"; @@ -41,81 +42,112 @@ export function BrowserAutomationList({ ); }, [panes, tabs, workspaceId]); - // A binding only counts as "connected" if the bound session is still in - // the live session list. Stale bindings render as `Unassigned` in the - // row below, so summing raw bindings would show a misleading higher - // count. + // Stale bindings (bound session no longer live) count as Unassigned. const liveSessionIds = new Set(sessions.map((s) => s.id)); const connectedCount = browserPanes.filter((p) => { const sid = bindingsByPane[p.id]; return sid && liveSessionIds.has(sid); }).length; + const unassignedCount = browserPanes.length - connectedCount; const needsSetupCount = sessions.filter( (s) => s.mcpStatus === "missing", ).length; return ( - + - Browser Automation + + Browser Automation — workspace overview + - All browser panes in this workspace and their bound sessions. + Every browser pane and which LLM session is driving it. -
- - - +
+ + {browserPanes.length}{" "} + panes + + + {connectedCount} connected + + + {unassignedCount}{" "} + unassigned + + {needsSetupCount > 0 && ( + + {needsSetupCount} needs + setup + + )}
-
+
{browserPanes.length === 0 && (
No browser panes in this workspace.
)} - {browserPanes.map((pane) => { + {browserPanes.map((pane, index) => { const sessionId = bindingsByPane[pane.id]; const session = sessionId ? (sessions.find((s) => s.id === sessionId) ?? null) : null; const url = pane.browser?.currentUrl ?? pane.url ?? "about:blank"; + const isConnected = session !== null; return (
-
-
-
- {pane.userTitle || pane.name} -
-
- {url} -
+ +
+
+ {pane.userTitle || pane.name} +
+
+ {url}
- - {session ? "Connected" : "Unassigned"} -
-
- {session - ? `${session.displayName} · ${session.provider} · ${session.mcpStatus === "ready" ? "MCP ready" : "MCP missing"}` - : "Pick any running LLM session"} + +
+ {session ? ( + <> +
+ {session.displayName} +
+
+ {session.provider} ·{" "} + {session.mcpStatus === "ready" + ? "MCP ready" + : "MCP missing"} +
+ + ) : ( +
+ Unassigned — pick any running LLM session +
+ )}
-
+
@@ -153,14 +183,3 @@ export function BrowserAutomationList({
); } - -function Metric({ label, value }: { label: string; value: number }) { - return ( -
-
{value}
-
- {label} -
-
- ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/SessionConnectModal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/SessionConnectModal.tsx index b60f8f01999..64cd9d49808 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/SessionConnectModal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/SessionConnectModal.tsx @@ -10,7 +10,14 @@ import { import { toast } from "@superset/ui/sonner"; import { cn } from "@superset/ui/utils"; import { useEffect, useMemo, useState } from "react"; -import { LuList, LuShield } from "react-icons/lu"; +import { + LuArrowLeft, + LuLayoutGrid, + LuList, + LuPlug, + LuShield, + LuTerminal, +} from "react-icons/lu"; import { useBrowserAutomationData } from "renderer/hooks/useBrowserAutomationData"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { @@ -20,10 +27,34 @@ import { useBrowserAutomationStore, } from "renderer/stores/browser-automation"; import { useTabsStore } from "renderer/stores/tabs/store"; -import { CdpEndpointCard } from "./components/CdpEndpointCard"; +import { + CdpEndpointCard, + PlaceholderSetupCommandsCard, +} from "./components/CdpEndpointCard"; import { McpInstallPanel } from "./components/McpInstallPanel"; import { PermissionsTab } from "./components/PermissionsTab"; +/** + * pane.name / userTitle は長大な URL (特に Google 検索結果の query + * 付き) になることがあり、pill / DetailItem / footer 文言にそのまま流すと + * モーダルからはみ出す。URL は host + 先頭パスだけに縮め、それ以外は + * 40 文字でカットしてツールチップに全文を出すための補助。 + */ +function shortenPaneLabel(raw: string, max = 40): string { + if (!raw) return raw; + try { + if (raw.startsWith("http://") || raw.startsWith("https://")) { + const url = new URL(raw); + const tail = `${url.pathname}${url.search ? "?…" : ""}`; + const short = `${url.host}${tail}`; + return short.length > max ? `${short.slice(0, max - 1)}…` : short; + } + } catch { + // fall through to generic truncation + } + return raw.length > max ? `${raw.slice(0, max - 1)}…` : raw; +} + interface SessionConnectModalProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -33,9 +64,12 @@ export function SessionConnectModal({ open, onOpenChange, }: SessionConnectModalProps) { - const [activeTab, setActiveTab] = useState<"sessions" | "permissions">( - "sessions", - ); + const [activeTab, setActiveTab] = useState< + "sessions" | "workspace" | "permissions" + >("sessions"); + // Incremented each time the user asks for setup commands from the + // summary bar; CdpEndpointCard listens and expands its section. + const [setupRevealToken, setSetupRevealToken] = useState(0); const paneId = useBrowserAutomationStore((s) => s.connectModal.paneId); const selectedSessionId = useBrowserAutomationStore( (s) => s.connectModal.selectedSessionId, @@ -43,7 +77,6 @@ export function SessionConnectModal({ const setSelectedSession = useBrowserAutomationStore( (s) => s.setSelectedSession, ); - const setListViewOpen = useBrowserAutomationStore((s) => s.setListViewOpen); const { sessions, bindingsByPane, mcpStatus } = useBrowserAutomationData({ enabled: open, @@ -56,6 +89,11 @@ export function SessionConnectModal({ const pane = useTabsStore((s) => (paneId ? s.panes[paneId] : null)); const panes = useTabsStore((s) => s.panes); + const tabs = useTabsStore((s) => s.tabs); + const workspaceId = useMemo(() => { + if (!pane) return null; + return tabs.find((t) => t.id === pane.tabId)?.workspaceId ?? null; + }, [pane, tabs]); const session = selectedSessionId ? (sessions.find((s) => s.id === selectedSessionId) ?? null) @@ -166,8 +204,8 @@ export function SessionConnectModal({ return ( - - + + Connect browser automation @@ -182,6 +220,13 @@ export function SessionConnectModal({ Sessions + setActiveTab("workspace")} + > + + Workspace + setActiveTab("permissions")} @@ -192,36 +237,41 @@ export function SessionConnectModal({ + {/* Always render the summary bar so the right-side "Show setup + commands" button stays in a fixed position across tabs. */} + { + setActiveTab("sessions"); + setSetupRevealToken((n) => n + 1); + }} + /> + {activeTab === "permissions" ? ( - +
+ +
+ ) : activeTab === "workspace" ? ( +
+ onOpenChange(false)} + onSwitchToSessions={() => setActiveTab("sessions")} + /> +
) : ( -
+
-
-
- ◎ -
-
-
- {paneName} -
-
- {paneUrl} -
-
- -
+
Running sessions @@ -256,7 +306,13 @@ export function SessionConnectModal({ )}
-
+
+ {setupRevealToken > 0 && session?.id !== currentBinding && ( + setSetupRevealToken(0)} + /> + )} {session ? ( session.mcpStatus === "ready" ? ( ) : ( -
+
{session?.mcpStatus === "ready" ? assignedPaneIdForSelected - ? `Connecting will reassign ${session.displayName} from ${panes[assignedPaneIdForSelected]?.name ?? "another pane"} to ${paneName}.` + ? `Connecting will reassign ${session.displayName} from "${shortenPaneLabel( + panes[assignedPaneIdForSelected]?.name ?? "another pane", + 32, + )}" to "${shortenPaneLabel(paneName, 32)}".` : "Connecting binds this browser pane to the selected session only." : session ? "Add the MCP entry first, then reopen or restart this session." @@ -305,17 +365,6 @@ export function SessionConnectModal({ : "Select a session from the left."}
- {currentBinding && ( - - )} +
+ ); +} + +function PaneIdentityCard({ + paneName, + paneUrl, + currentSession, + hasBinding, + onDisconnect, + disconnectPending, +}: { + paneName: string; + paneUrl: string; + currentSession: AutomationSession | null; + hasBinding: boolean; + onDisconnect?: () => void; + disconnectPending?: boolean; +}) { + const isConnected = currentSession !== null; + // Stale binding: binding record exists but the target session is no + // longer in the live set (e.g. the claude/codex process exited). + // Surface this explicitly and keep Disconnect available so the user + // can clear it without first rebinding to another session. + const isStale = hasBinding && !isConnected; + const statusLabel = isConnected + ? "Connected" + : isStale + ? "Session ended" + : "Unassigned"; + const statusClass = isConnected + ? "bg-emerald-500/15 text-emerald-300" + : isStale + ? "bg-amber-500/15 text-amber-300" + : "bg-muted text-muted-foreground"; + return ( +
+
+
+ +
+
+
+
+ {shortenPaneLabel(paneName, 50)} +
+ + {statusLabel} + +
+
+ {paneUrl} +
+
+ {isConnected ? ( + <> + Driven by: + + {currentSession.displayName} + + + ({currentSession.provider}) + + + ) : isStale ? ( + + Previous session has ended — disconnect to clear, or pick a new + one below. + + ) : ( + + No session is driving this pane yet. + + )} +
+
+ {hasBinding && onDisconnect && ( + + )} +
+
+ ); +} + +function WorkspacePanesTab({ + workspaceId, + onClose, + onSwitchToSessions, +}: { + workspaceId: string | null; + onClose: () => void; + onSwitchToSessions: () => void; +}) { + const panes = useTabsStore((s) => s.panes); + const tabs = useTabsStore((s) => s.tabs); + const setFocusedPane = useTabsStore((s) => s.setFocusedPane); + const setActiveTabInStore = useTabsStore((s) => s.setActiveTab); + const openConnectModal = useBrowserAutomationStore((s) => s.openConnectModal); + const { sessions, bindingsByPane } = useBrowserAutomationData({ + enabled: true, + }); + + const browserPanes = useMemo(() => { + const tabById = new Map(tabs.map((t) => [t.id, t])); + return Object.values(panes).filter((p) => { + if (p.type !== "webview") return false; + const tab = tabById.get(p.tabId); + if (!tab) return false; + if (workspaceId && tab.workspaceId !== workspaceId) return false; + return true; + }); + }, [panes, tabs, workspaceId]); + + return ( +
+ {browserPanes.length === 0 ? ( +
+ No browser panes in this workspace. +
+ ) : ( + browserPanes.map((pane, index) => { + const sessionId = bindingsByPane[pane.id]; + const session = sessionId + ? (sessions.find((s) => s.id === sessionId) ?? null) + : null; + const url = pane.browser?.currentUrl ?? pane.url ?? "about:blank"; + // Three-state status, matching PaneIdentityCard: + // connected (live session), stale (binding exists but session + // dropped out of the live set), unassigned (no binding). + const isConnected = session !== null; + const isStale = sessionId != null && session === null; + const dotClass = isConnected + ? "bg-emerald-400" + : isStale + ? "bg-amber-400" + : "bg-muted-foreground/40"; + const paneDisplay = pane.userTitle || pane.name; + return ( +
+ +
+
+ {shortenPaneLabel(paneDisplay, 50)} +
+
+ {url} +
+
+ +
+ {session ? ( + <> +
+ {session.displayName} +
+
+ {session.provider} ·{" "} + {session.mcpStatus === "ready" + ? "MCP ready" + : "MCP missing"} +
+ + ) : isStale ? ( +
+ Session ended — rebind or disconnect +
+ ) : ( +
+ Unassigned — pick any running LLM session +
+ )} +
+
+ + +
+
+ ); + }) + )} +
+ ); +} + function SessionCard({ session, isSelected, @@ -396,24 +772,28 @@ function SessionCard({ : session.mcpStatus === "missing" ? "bg-amber-500/15 text-amber-300" : "bg-muted text-muted-foreground"; + const shortOtherPane = assignedElsewherePaneName + ? shortenPaneLabel(assignedElsewherePaneName, 28) + : null; const pillLabel = attachedToThisPane - ? "Attached" - : assignedElsewherePaneName - ? "Reassign" + ? "● Driving this pane" + : shortOtherPane + ? `Driving: ${shortOtherPane}` : session.mcpStatus === "ready" - ? "Ready" + ? "Ready · Free" : session.mcpStatus === "missing" ? "Needs MCP" : "Unknown"; + const pillTooltip = assignedElsewherePaneName ?? pillLabel; const note = attachedToThisPane - ? "This session is currently driving this browser pane." + ? null : assignedElsewherePaneName - ? `${session.displayName} is currently controlling ${assignedElsewherePaneName}. Connecting here moves ownership.` - : session.mcpStatus === "ready" - ? "Browser MCP is configured. Connect will be immediate." - : session.mcpStatus === "missing" - ? "This session does not currently expose the required browser automation MCP entry." - : "Could not verify MCP status for this session."; + ? `Connecting here will move ownership from "${shortenPaneLabel(assignedElsewherePaneName, 60)}".` + : session.mcpStatus === "missing" + ? "This session does not currently expose the required browser automation MCP entry." + : session.mcpStatus === "unknown" + ? "Could not verify MCP status for this session." + : null; return (
{pillLabel}
-
- {session.kind} - {session.branchOrContextLabel} - Last active {session.lastActiveAt} -
-
- {note} -
+ {note && ( +
+ {note} +
+ )} ); } -function Tag({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} - function ReadyPanel({ session, paneName, reassigning, previousPaneName, attachedToThisPane, + revealSetupToken, }: { session: AutomationSession; paneName: string; reassigning: boolean; previousPaneName: string | null; attachedToThisPane: boolean; + revealSetupToken?: number; }) { return (
@@ -495,15 +875,22 @@ function ReadyPanel({ {session.displayName} / {session.provider} Exclusive control - {paneName} + + {shortenPaneLabel(paneName, 60)} + {reassigning - ? `Moves from ${previousPaneName ?? "another pane"}` + ? `Moves from ${shortenPaneLabel(previousPaneName ?? "another pane", 32)}` : "Toolbar badge updates instantly"}
- {attachedToThisPane && } + {attachedToThisPane && ( + + )}
); } @@ -511,16 +898,21 @@ function ReadyPanel({ function DetailItem({ label, children, + title, }: { label: string; children: React.ReactNode; + title?: string; }) { return ( -
+
{label} - + {children}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/CdpEndpointCard/CdpEndpointCard.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/CdpEndpointCard/CdpEndpointCard.tsx index 5d0a34329db..3c02b6b3ca4 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/CdpEndpointCard/CdpEndpointCard.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/CdpEndpointCard/CdpEndpointCard.tsx @@ -1,10 +1,34 @@ import { Button } from "@superset/ui/button"; import { toast } from "@superset/ui/sonner"; -import { LuCopy, LuExternalLink } from "react-icons/lu"; +import { useEffect, useState } from "react"; +import { + LuChevronDown, + LuChevronUp, + LuCopy, + LuExternalLink, +} from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; +/** + * POSIX shell single-quote a value so copy-pasted commands survive + * arbitrary characters (spaces in `~/Library/Application Support/…`, + * `'`, `$`, etc). We single-quote the whole string and break out for + * embedded single quotes via the canonical `'\''` trick. + */ +function shellQuote(value: string): string { + return `'${value.replace(/'/g, "'\\''")}'`; +} + interface CdpEndpointCardProps { sessionId: string; + /** + * Increment to force the Example setup section open from outside + * (e.g., the summary-bar "Show setup commands" button). The card + * defaults to collapsed because once a session is bound the MCP + * registration is a one-shot task; keeping four command blocks + * permanently visible drowns out the actual status info. + */ + revealSetupToken?: number; } /** @@ -15,13 +39,26 @@ interface CdpEndpointCardProps { * delegate actual browser control to those tools, so this is the * primary success-state UI once a pane is attached. */ -export function CdpEndpointCard({ sessionId }: CdpEndpointCardProps) { +export function CdpEndpointCard({ + sessionId, + revealSetupToken, +}: CdpEndpointCardProps) { const { data, isLoading } = electronTrpc.browserAutomation.getCdpEndpointForSession.useQuery( { sessionId }, { refetchInterval: 5_000 }, ); + // Setup commands stay hidden by default. They re-appear when the + // user explicitly asks via the summary-bar button (revealSetupToken + // bumps) or the inline toggle. + const [setupOpen, setSetupOpen] = useState(false); + useEffect(() => { + if (revealSetupToken !== undefined && revealSetupToken > 0) { + setSetupOpen(true); + } + }, [revealSetupToken]); + const copy = async (value: string, label: string): Promise => { try { await navigator.clipboard.writeText(value); @@ -57,7 +94,10 @@ export function CdpEndpointCard({ sessionId }: CdpEndpointCardProps) { ); } - const chromeDevtoolsCmd = `claude mcp add chrome-devtools-mcp -s user -- npx -y chrome-devtools-mcp --browser-url ${data.httpBase}`; + const httpBaseArg = shellQuote(data.httpBase); + const configPathArg = shellQuote(data.browserUseConfigPath); + const chromeDevtoolsCmdClaude = `claude mcp add chrome-devtools-mcp -s user -- npx -y chrome-devtools-mcp --browser-url ${httpBaseArg}`; + const chromeDevtoolsCmdCodex = `codex mcp add chrome-devtools-mcp -- npx -y chrome-devtools-mcp --browser-url ${httpBaseArg}`; // browser-use's `--mcp` branch intentionally ignores `--cdp-url` // (skill_cli/main.py ~2280 routes straight to the MCP main without // forwarding the flag). The only officially supported injection @@ -65,7 +105,8 @@ export function CdpEndpointCard({ sessionId }: CdpEndpointCardProps) { // (see browser_use/config.py and mcp/server.py). The desktop app // writes that file per session at `data.browserUseConfigPath` and // we point browser-use at it here. - const browserUseCmd = `claude mcp add browser-use -s user -e BROWSER_USE_CONFIG_PATH=${data.browserUseConfigPath} -- uvx --from "browser-use[cli]" browser-use --mcp`; + const browserUseCmdClaude = `claude mcp add browser-use -s user -e BROWSER_USE_CONFIG_PATH=${configPathArg} -- uvx --from 'browser-use[cli]' browser-use --mcp`; + const browserUseCmdCodex = `codex mcp add browser-use --env BROWSER_USE_CONFIG_PATH=${configPathArg} -- uvx --from 'browser-use[cli]' browser-use --mcp`; return (
@@ -97,19 +138,56 @@ export function CdpEndpointCard({ sessionId }: CdpEndpointCardProps) { />
-
+
- copy(chromeDevtoolsCmd, "chrome-devtools-mcp command")} - /> - copy(browserUseCmd, "browser-use command")} - /> + {!setupOpen && ( + + (一度だけ実行すれば OK — 必要なら開いて参照) + + )} + + {setupOpen && ( +
+ + copy(chromeDevtoolsCmdClaude, "chrome-devtools-mcp command") + } + /> + + copy( + chromeDevtoolsCmdCodex, + "chrome-devtools-mcp (codex) command", + ) + } + /> + copy(browserUseCmdClaude, "browser-use command")} + /> + + copy(browserUseCmdCodex, "browser-use (codex) command") + } + /> +
+ )}
@@ -123,6 +201,110 @@ export function CdpEndpointCard({ sessionId }: CdpEndpointCardProps) { ); } +/** + * Standalone version of the "Example setup" section that works even + * when no session is bound yet — the WebSocket/HTTP base are not + * known until a binding exists, so the commands are rendered with + * placeholder tokens that the user substitutes after binding. + * Intended use: the "Show setup commands" button in the summary bar + * wants to reveal setup instructions even before the user has bound + * a session. + */ +export function PlaceholderSetupCommandsCard({ + revealToken, + onDismiss, +}: { + revealToken?: number; + onDismiss?: () => void; +}) { + const [open, setOpen] = useState(true); + useEffect(() => { + if (revealToken !== undefined && revealToken > 0) setOpen(true); + }, [revealToken]); + + const copy = async (value: string, label: string): Promise => { + try { + await navigator.clipboard.writeText(value); + toast.success(`${label} copied`); + } catch { + toast.error(`Failed to copy ${label.toLowerCase()}`); + } + }; + + const HTTP = "http://127.0.0.1:"; + const CFG = ""; + const chromeClaude = `claude mcp add chrome-devtools-mcp -s user -- npx -y chrome-devtools-mcp --browser-url ${HTTP}`; + const chromeCodex = `codex mcp add chrome-devtools-mcp -- npx -y chrome-devtools-mcp --browser-url ${HTTP}`; + const useClaude = `claude mcp add browser-use -s user -e BROWSER_USE_CONFIG_PATH=${CFG} -- uvx --from "browser-use[cli]" browser-use --mcp`; + const useCodex = `codex mcp add browser-use --env BROWSER_USE_CONFIG_PATH=${CFG} -- uvx --from "browser-use[cli]" browser-use --mcp`; + + return ( +
+
+
+
Setup commands (template)
+
+ 外部ブラウザ MCP (chrome-devtools-mcp / browser-use) を登録する + ためのテンプレートです。プレースホルダ部分 ( + {HTTP} /{" "} + {CFG}) + は、セッションを bind すると実際の値に置き換わって CDP endpoint + カードに表示されます。 +
+
+ {onDismiss && ( + + )} +
+ + + {open && ( +
+ copy(chromeClaude, "chrome-devtools-mcp command")} + /> + + copy(chromeCodex, "chrome-devtools-mcp (codex) command") + } + /> + copy(useClaude, "browser-use command")} + /> + copy(useCodex, "browser-use (codex) command")} + /> +
+ )} +
+ ); +} + function UrlRow({ label, value, diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/CdpEndpointCard/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/CdpEndpointCard/index.ts index 4a345557039..6ad60dad55b 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/CdpEndpointCard/index.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/CdpEndpointCard/index.ts @@ -1 +1,4 @@ -export { CdpEndpointCard } from "./CdpEndpointCard"; +export { + CdpEndpointCard, + PlaceholderSetupCommandsCard, +} from "./CdpEndpointCard"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/McpInstallPanel/McpInstallPanel.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/McpInstallPanel/McpInstallPanel.tsx index 4e1338df6dd..6415ff51fa8 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/McpInstallPanel/McpInstallPanel.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/McpInstallPanel/McpInstallPanel.tsx @@ -2,7 +2,7 @@ import { Button } from "@superset/ui/button"; import { Checkbox } from "@superset/ui/checkbox"; import { toast } from "@superset/ui/sonner"; import { useState } from "react"; -import { LuInfo } from "react-icons/lu"; +import { LuCheck, LuChevronDown, LuInfo } from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; import type { ServerCommand } from "renderer/stores/browser-automation"; @@ -32,6 +32,23 @@ export function McpInstallPanel({ serverCommand }: McpInstallPanelProps) { const [claudeChecked, setClaudeChecked] = useState(true); const [codexChecked, setCodexChecked] = useState(false); + const [expanded, setExpanded] = useState(false); + + // Collapse to a single "all good" banner when every CLI-found runtime is + // already installed with a matching command. Showing the full install UI + // in that case reads as "something is wrong" even though nothing is. + const claudeReady = + !state?.claude.cliFound || + (state.claude.installed && state.claude.matchesExpected); + const codexReady = + !state?.codex.cliFound || + (state.codex.installed && state.codex.matchesExpected); + const anyCliFound = canInstallClaude || canInstallCodex; + const allInstalled = anyCliFound && claudeReady && codexReady; + const readyLabels = [ + canInstallClaude ? "Claude Code" : null, + canInstallCodex ? "Codex" : null, + ].filter((v): v is string => v !== null); if (serverCommand && !serverCommand.available) { return ( @@ -80,6 +97,34 @@ export function McpInstallPanel({ serverCommand }: McpInstallPanelProps) { } }; + if (allInstalled && !expanded) { + return ( +
+
+ +
+
+ Browser MCP is installed — ready to connect +
+ {readyLabels.length > 0 && ( +
+ {readyLabels.join(" · ")} +
+ )} +
+ +
+
+ ); + } + return (
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/PermissionsTab/PermissionsTab.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/PermissionsTab/PermissionsTab.tsx index 2afc5a1a1af..0dfb9c32f84 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/PermissionsTab/PermissionsTab.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/components/PermissionsTab/PermissionsTab.tsx @@ -166,7 +166,7 @@ export function PermissionsTab() { }; return ( -
+
@@ -218,95 +218,108 @@ export function PermissionsTab() {
-
+
{!selected ? ( -
+
Select a preset on the left.
) : ( -
-
-
- - { - setDraftName(e.target.value); - setDirty(true); - }} - className="mt-1 h-8 text-[13px]" - /> -
- - {!isBuiltin && ( + <> + {/* Sticky header: name input + duplicate/delete stay visible. */} +
+
+
+ + { + setDraftName(e.target.value); + setDirty(true); + }} + className="mt-1 h-8 text-[13px]" + /> +
+ {!isBuiltin && ( + + )} +
+ {isBuiltin && ( +
+ Built-in preset. Duplicate to customize. +
)}
- {isBuiltin && ( -
- Built-in preset. Duplicate to customize. -
- )} - -
- {toggleKeys.map((key) => { - const m = meta[key]; - const value = draftToggles[key] === true; - return ( -
-
-
{m.label}
-
- {m.description} -
-
- {m.methods.join(", ")} + {/* Scrollable toggle list — only this region scrolls so the + header and the Save/Activate action bar stay on screen + even with many toggle rows. */} +
+
+ {toggleKeys.map((key) => { + const m = meta[key]; + const value = draftToggles[key] === true; + return ( +
+
+
{m.label}
+
+ {m.description} +
+
+ {m.methods.join(", ")} +
+ handleToggle(key, v)} + />
- handleToggle(key, v)} - /> -
- ); - })} + ); + })} +
-
-
+ {/* Sticky action bar. */} +
+
{isActive - ? "This preset is currently active. Changes to toggles apply to all live MCP sessions after save (connections are force-reconnected)." + ? "Active preset. Changes apply to all live MCP sessions after save." : "Save, then click Activate to apply to MCP sessions."}
-
+
{!isBuiltin && (
-
+ )}
diff --git a/mock.html b/mock.html new file mode 100644 index 00000000000..40a9fcc78b8 --- /dev/null +++ b/mock.html @@ -0,0 +1,753 @@ + + + + +Connect Browser Automation — current UI mock + + + + +
+ + +
+

① Connect browser automation — Sessions タブ (現状)

+
+ +
+
Connect browser automation
+
Choose which running LLM session should control this browser pane.
+
+ + +
+
+ + +
+ +
+ +
+
+
+
Browser · GitHub
+
https://github.com/MocA-Love/superset/pull/371
+
+ +
+ +
Running sessions
+ +
+ + + + + + + + + + + +
+
+ + +
+
+
+
Session is attached to this pane
+
+ Drive the pane from this session by pointing an external browser MCP at the CDP endpoint below. +
+
+
+ Owner + claude · feature/browser-mcp-target-filter / Claude +
+
+ Binding mode + Exclusive control +
+
+ Pane + Browser · GitHub +
+
+ Expected + Toolbar badge updates instantly +
+
+
+ + +
+
+
CDP endpoint
+ Ready +
+
Point an external MCP / puppeteer at this URL.
+
+ ws://127.0.0.1:47891/devtools/browser/abc123-filtered + +
+
+
+
+
+ + +
+
Connecting binds this browser pane to the selected session only.
+
+ + + +
+
+
+
+ + +
+

② Connect browser automation — 選択セッションが Needs MCP (現状 / McpInstallPanel)

+
+
+
Connect browser automation
+
Choose which running LLM session should control this browser pane.
+
+ +
+
+
+
+
+
Browser · localhost
+
http://localhost:3000
+
+ +
+
Running sessions
+ +
+ +
+ +
+
Install Superset Browser MCP
+
+ Pick which LLM runtime(s) should be able to drive the browser pane. + Installing is a one-shot operation; after this you just bind panes + from the Connect dialog. Already-installed runtimes are kept + idempotent — re-installing corrects stale paths. +
+
+
+ + + Claude Code + ✓ installed and up to date + +
+
+ + + Codex + codex CLI found, not yet installed + +
+
+
+ +
+
+ ⓘ Will register the command /Applications/Superset.app/Contents/Resources/app.asar.unpacked/resources/superset-browser-mcp. +
+
+
+
+ +
+
Add the MCP entry first, then reopen or restart this session.
+
+ + +
+
+
+
+ + +
+

③ All panes ボタン押下時 (BrowserAutomationList / 現状)

+
+
+
Browser Automation
+
All browser panes in this workspace and their bound sessions.
+
+ +
+
+
4
+
Browser panes
+
+
+
2
+
Connected
+
+
+
1
+
Needs setup
+
+
+ +
+
+
+
+
Browser · GitHub
+
https://github.com/MocA-Love/superset/pull/371
+
+ Connected +
+
claude · feature/browser-mcp-target-filter · Claude · MCP ready
+
+ + +
+
+ +
+
+
+
Browser · Docs
+
https://docs.superset.sh/
+
+ Unassigned +
+
Pick any running LLM session
+
+ + +
+
+ +
+
+
+
Browser · localhost
+
http://localhost:3000
+
+ Connected +
+
codex · main · Codex · MCP ready
+
+ + +
+
+ +
+
+
+
Browser · Jira
+
about:blank
+
+ Unassigned +
+
Pick any running LLM session
+
+ + +
+
+
+
+
+ + +
+

ユーザーが感じている問題点

+
    +
  • どれがどのタブに繋がっているかわかりづらい: Session 側は「Attached / Reassign」で自 pane / 他 pane を示すが、右カラムの detail は 1 件ずつしか見えない。"All panes" を開くまで全体像が掴めない。
  • +
  • すでに紐づいているものがどれかわかりづらい: pane header に現在 bound なセッション名が直接表示されていない ( + pane 名 + URL のみ)。Session カードの pill で初めて分かる。
  • +
  • MCP 追加済みなのに毎回インストール用スニペット/パネルが見える: Needs MCP セッションを選ぶたびに McpInstallPanel が右カラム全域を占有。全 runtime が ✓ 済みでも "Install Superset Browser MCP" の見出しが残り、未設定かと勘違いする。
  • +
+
+ +
+ +
+

改善案 (Proposed)

+

上の3つの Pain Point を解消する方針。元の情報構造はできるだけ保ちつつ、"いま何が繋がっているか" をモーダルを開いた瞬間に一目で伝える。

+
    +
  • Pain 1/2 を解消: 左ペイン上部に「この pane の現在の binding」を専用カードで常時固定表示 + 全 session カードに "→ どの pane を駆動しているか" を明記。"All panes" のミニサマリもヘッダー直下に inline 展開 (開かずに全 pane の状態が見える)。
  • +
  • Pain 3 を解消: MCP が「全 runtime ✓ 済み」の場合は右カラムをグリーンの 1 行バナー ("Browser MCP is installed — ready to connect") に畳む。"Manage installations" リンクを添えて必要時だけ詳細展開。未インストール runtime がある場合のみ現状の Install パネルを表示。
  • +
  • ついでの改善: Session カードの pill に"pane 名"を添える (例: "Driving: Browser · Docs")。detail 右カラムは "Switch / Disconnect / Copy CDP" の3アクションに整理し情報密度を下げる。
  • +
+
+ + +
+

④ 改善案: Connect browser automation — Sessions タブ

+
+ +
+
Connect browser automation
+
Choose which running LLM session should control this browser pane.
+
+ + +
+
+ + +
+ Workspace bindings + 2 connected + 2 unassigned + 1 needs setup + +
+ + +
+ +
+ +
+
+
+
+
+
Browser · GitHub
+ Connected +
+
https://github.com/MocA-Love/superset/pull/371
+
+ Driven by: + claude · feature/browser-mcp-target-filter + (Claude) +
+
+ +
+
+ +
+
Running sessions (4)
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+
+ + +
+ +
+ +
+
Browser MCP is installed — ready to connect
+
Claude Code · Codex
+
+ +
+ +
+
+
+
claude · feature/browser-mcp-target-filter
+ Driving this pane +
+
Already bound. Use the CDP endpoint below for external tools.
+
+ ws://127.0.0.1:47891/devtools/browser/abc123-filtered + +
+
+ + +
+
+
+
+
+ +
+
This session is already driving this pane. Pick a different one to reassign.
+
+ +
+
+
+
+ + +
+

⑤ 改善案: Needs MCP セッション選択時 (全 runtime インストール済みならバナー、未済み runtime のみ展開)

+
+
+
Connect browser automation
+
Choose which running LLM session should control this browser pane.
+
+
+
+
+
Browser · localhost
+
http://localhost:3000
+
No session is driving this pane yet.
+
+ +
+
+ +
+ +
Claude Code / Codex: MCP ready
+ +
+ +
+
+ 1 missing +
TODO-Agent is missing the Browser MCP entry
+
+
This session type does not auto-install. Register it once, then restart the agent.
+
+ + +
+
+
+
+
+
Install the MCP entry, then reopen this session.
+
+ + +
+
+
+
+ + +
+

⑥ 改善案: All panes

+
+
+
Browser Automation — workspace overview
+
Every browser pane and which LLM session is driving it.
+
+ + +
+ 4 panes + 2 connected + 2 unassigned + 1 needs setup + +
+ + +
+ +
+
+
+
Browser · GitHub
+
https://github.com/MocA-Love/superset/pull/371
+
+
+
+
claude · feature/browser-mcp-target-filter
+
Claude · MCP ready
+
+
+ + + +
+
+ +
+
+
+
Browser · localhost
+
http://localhost:3000
+
+
+
+
codex · main
+
Codex · MCP ready
+
+
+ + + +
+
+ + +
+
+
+
Browser · Docs
+
https://docs.superset.sh/
+
+
+
+
Unassigned — pick any running LLM session
+
+
+ + +
+
+ +
+
+
+
Browser · Jira
+
about:blank
+
+
+
+
Unassigned
+
+
+ + +
+
+
+
+
+ +
+

改善ポイントまとめ (現状 ⇄ 改善後)

+
+
+
Pain 1/2: 「どれがどのタブに繋がっているか」
+
    +
  • pane header を "Driven by: claude · …" 明示表示に変更 + Connected pill
  • +
  • Reassign カードに "Driving: Browser · Docs" を pill で同居
  • +
  • ヘッダー直下に workspace 全体の bindings サマリーバーを常駐 (2 connected / 2 unassigned / 1 needs setup)
  • +
+
+
+
Pain 3: 「MCP 追加済みなのに毎回スニペット」
+
    +
  • 全 runtime ✓ なら 1 行のグリーンバナーに折り畳み
  • +
  • "Manage ▾" で従来の Install パネルに展開可能
  • +
  • 欠けている runtime がある場合のみ、その runtime 名入りの amber パネルを表示
  • +
+
+
+
All panes の可読性
+
    +
  • カード羅列 → テーブル風の 1 行レイアウト (pane ← session を矢印で示す)
  • +
  • ステータスドット (emerald / muted / amber) で一目判別
  • +
+
+
+
情報密度の最適化
+
    +
  • Session カードの 3 Tag 行 (kind / branch / last active) を sub-line 1 行に統合
  • +
  • 右カラム detail は Copy CDP / Disconnect / Move to another pane の3アクションに絞る
  • +
+
+
+
+ +
+ +