forked from superset-sh/superset
-
Notifications
You must be signed in to change notification settings - Fork 1
feat(desktop): browser pane の Connect 導線と session バインディング UI (Phase 1) #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
a22a516
feat(desktop): add browser pane Connect flow for LLM sessions (Phase …
MocA-Love f017f92
feat(desktop): wire browser-automation to real sessions & MCP detection
MocA-Love 73b6364
fix(desktop): address codex review on BrowserAutomationList
MocA-Love b75711b
fix(desktop): handle more session terminal states and stale bindings
MocA-Love 7376ba5
fix(desktop): restrict session list to live states and wire subscription
MocA-Love 3a2605a
fix(desktop): handle stale selection and EventEmitter listener cap
MocA-Love 88547a9
fix(desktop): stop polling sessions for every browser pane
MocA-Love b85c59e
feat(desktop): persist bindings in local-db and detect terminal claud…
MocA-Love 0765ed9
fix(desktop): gate session polling on dialog visibility
MocA-Love bfcd8a3
fix(desktop): derive Connect badge from liveness, centralize binding …
MocA-Love 549c227
Merge remote-tracking branch 'origin/main' into feature/browser-pane-…
MocA-Love 51f0936
chore: merge main and apply lint:fix formatting
MocA-Love 509e391
fix(desktop): separate todo-agent kind and count only live bindings
MocA-Love ca55400
fix(desktop): skip terminal probe when no terminal bindings exist
MocA-Love be0f062
fix(desktop): avoid modal re-render loops and BrowserPane selector churn
MocA-Love e6a7cfc
fix(desktop): activate target pane before opening connect modal
MocA-Love c967c80
fix(desktop): structured MCP detection and graceful terminal-host fai…
MocA-Love ea21b75
Merge remote-tracking branch 'origin/main' into feature/browser-pane-…
MocA-Love c7c41b1
chore: merge main and apply lint:fix formatting
MocA-Love 941983c
fix(desktop): check ~/.claude.json for Claude MCP server entry
MocA-Love 31c5d94
fix(desktop): project .mcp.json + quoted Codex TOML keys
MocA-Love a6db356
Merge remote-tracking branch 'origin/main' into feature/browser-pane-…
MocA-Love 0257cf0
fix(desktop): scope Claude MCP readiness per session workspace
MocA-Love ef6bd02
fix(desktop): recognize Claude local-scope MCP entries in ~/.claude.json
MocA-Love 8a3f4cb
fix(desktop): correct MCP setup command and instructions
MocA-Love File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
486 changes: 486 additions & 0 deletions
486
apps/desktop/src/lib/trpc/routers/browser-automation/index.ts
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
apps/desktop/src/renderer/hooks/useBrowserAutomationData.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { useMemo } from "react"; | ||
| import { electronTrpc } from "renderer/lib/electron-trpc"; | ||
| import { | ||
| type AutomationSession, | ||
| formatRelativeTime, | ||
| type McpStatus, | ||
| } from "renderer/stores/browser-automation"; | ||
| import { useTabsStore } from "renderer/stores/tabs/store"; | ||
|
|
||
| /** | ||
| * Aggregates browser-automation session and binding data from the | ||
| * main-process tRPC routers. Sessions come from two sources: | ||
| * | ||
| * 1. TODO-Agent sessions (Claude Code workers the supervisor is running). | ||
| * 2. Terminal panes that currently have a `claude` or `codex` child | ||
| * process. The binding key for this kind is the terminal's paneId so | ||
| * that it self-heals across shell re-spawns inside the same pane. | ||
| * | ||
| * MCP readiness is resolved against the user's Claude/Codex config files. | ||
| * | ||
| * `enabled` controls the expensive queries. The binding query and its | ||
| * subscription always run because `ConnectButton` is mounted for every | ||
| * browser pane and needs to reflect the binding state without polling. | ||
| */ | ||
| export function useBrowserAutomationData({ | ||
| enabled = true, | ||
| }: { | ||
| enabled?: boolean; | ||
| } = {}) { | ||
| const panes = useTabsStore((s) => s.panes); | ||
|
|
||
| const { data: todoSessions = [], refetch: refetchSessions } = | ||
| electronTrpc.todoAgent.listAll.useQuery(undefined, { | ||
| enabled, | ||
| refetchOnWindowFocus: enabled, | ||
| refetchInterval: enabled ? 15000 : false, | ||
| }); | ||
| const { data: terminalAgents = [] } = | ||
| electronTrpc.browserAutomation.listTerminalAgentSessions.useQuery( | ||
| undefined, | ||
| { | ||
| enabled, | ||
| refetchOnWindowFocus: enabled, | ||
| refetchInterval: enabled ? 10000 : false, | ||
| }, | ||
| ); | ||
| const { data: mcpStatus } = | ||
| electronTrpc.browserAutomation.getMcpStatus.useQuery(undefined, { | ||
| enabled, | ||
| refetchOnWindowFocus: enabled, | ||
| refetchInterval: enabled ? 30000 : false, | ||
| }); | ||
| const { data: bindings = [] } = | ||
| electronTrpc.browserAutomation.listBindings.useQuery(undefined, { | ||
| // Binding changes are pushed via onBindingsChanged, so no polling. | ||
| refetchOnWindowFocus: false, | ||
| }); | ||
| // The binding subscription is centralized in `useBrowserBindingsSync` | ||
| // (mounted once in ContentView), so this hook does not open one per | ||
| // consumer. | ||
|
|
||
| const sessions: AutomationSession[] = useMemo(() => { | ||
| // Only sessions that have a live worker (or are actively scheduled to | ||
| // wake up) should be connectable. Queued/paused/aborted/done/failed/ | ||
| // escalated sessions either never started or are terminal. | ||
| const liveStatuses = new Set([ | ||
| "running", | ||
| "preparing", | ||
| "verifying", | ||
| "waiting", | ||
| ]); | ||
| const claudeReadyForWorkspace = (workspaceId: string | null): McpStatus => { | ||
| if (!mcpStatus) return "unknown"; | ||
| if (mcpStatus.claudeHomeReady) return "ready"; | ||
| if (workspaceId && mcpStatus.claudeReadyByWorkspaceId[workspaceId]) | ||
| return "ready"; | ||
| return "missing"; | ||
| }; | ||
| const todo: AutomationSession[] = todoSessions | ||
| .filter((s) => liveStatuses.has(s.status)) | ||
| .map((s) => { | ||
| // Todo-agent rows always represent Claude Code workers (see | ||
| // todo-daemon/claude-code-runner.ts). | ||
| const provider = "Claude" as const; | ||
| const mcp: McpStatus = claudeReadyForWorkspace(s.workspaceId); | ||
| const displayName = s.title || `Session ${s.id.slice(0, 6)}`; | ||
| const branchOrContext = | ||
| s.workspaceBranch ?? | ||
| s.workspaceName ?? | ||
| (s.projectName ? s.projectName : "workspace"); | ||
| return { | ||
| id: s.id, | ||
| displayName, | ||
| provider, | ||
| kind: "TODO-Agent", | ||
| branchOrContextLabel: branchOrContext, | ||
| lastActiveAt: formatRelativeTime(s.updatedAt ?? s.createdAt), | ||
| mcpStatus: mcp, | ||
| }; | ||
| }); | ||
|
|
||
| const terminal: AutomationSession[] = terminalAgents.map((t) => { | ||
| const pane = panes[t.paneId]; | ||
| const mcp: McpStatus = | ||
| t.provider === "Codex" | ||
| ? mcpStatus | ||
| ? mcpStatus.codexReady | ||
| ? "ready" | ||
| : "missing" | ||
| : "unknown" | ||
| : claudeReadyForWorkspace(t.workspaceId); | ||
| return { | ||
| id: `terminal:${t.paneId}`, | ||
| displayName: pane?.userTitle || pane?.name || `Terminal ${t.command}`, | ||
| provider: t.provider, | ||
| kind: "Terminal", | ||
| branchOrContextLabel: t.command, | ||
| lastActiveAt: t.lastAttachedAt | ||
| ? formatRelativeTime(Date.parse(t.lastAttachedAt)) | ||
| : "active", | ||
| mcpStatus: mcp, | ||
| }; | ||
| }); | ||
|
|
||
| return [...todo, ...terminal]; | ||
| }, [todoSessions, terminalAgents, mcpStatus, panes]); | ||
|
|
||
| const bindingsByPane = useMemo(() => { | ||
| const map: Record<string, string> = {}; | ||
| for (const b of bindings) map[b.paneId] = b.sessionId; | ||
| return map; | ||
| }, [bindings]); | ||
|
|
||
| return { | ||
| sessions, | ||
| bindingsByPane, | ||
| mcpStatus, | ||
| refetchSessions, | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { electronTrpc } from "renderer/lib/electron-trpc"; | ||
|
|
||
| /** | ||
| * Centralized binding subscription — mount ONCE per window (from | ||
| * ContentView). Without this, every `useBrowserAutomationData` consumer | ||
| * (one per browser pane) would open its own subscription to the main | ||
| * process emitter and fan out an invalidation per binding mutation. | ||
| */ | ||
| export function useBrowserBindingsSync() { | ||
| const utils = electronTrpc.useUtils(); | ||
| electronTrpc.browserAutomation.onBindingsChanged.useSubscription(undefined, { | ||
| onData: () => { | ||
| utils.browserAutomation.listBindings.invalidate(); | ||
| utils.browserAutomation.listBindingLiveness.invalidate(); | ||
| }, | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
...absContent/TabView/BrowserPane/components/BrowserAutomationList/BrowserAutomationList.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| import { Button } from "@superset/ui/button"; | ||
| import { | ||
| Dialog, | ||
| DialogContent, | ||
| DialogDescription, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| } from "@superset/ui/dialog"; | ||
| import { cn } from "@superset/ui/utils"; | ||
| import { useMemo } from "react"; | ||
| import { useBrowserAutomationData } from "renderer/hooks/useBrowserAutomationData"; | ||
| import { useBrowserAutomationStore } from "renderer/stores/browser-automation"; | ||
| import { useTabsStore } from "renderer/stores/tabs/store"; | ||
|
|
||
| interface BrowserAutomationListProps { | ||
| workspaceId: string; | ||
| open: boolean; | ||
| onOpenChange: (open: boolean) => void; | ||
| } | ||
|
|
||
| export function BrowserAutomationList({ | ||
| workspaceId, | ||
| open, | ||
| onOpenChange, | ||
| }: BrowserAutomationListProps) { | ||
| const panes = useTabsStore((s) => s.panes); | ||
| const tabs = useTabsStore((s) => s.tabs); | ||
| const setFocusedPane = useTabsStore((s) => s.setFocusedPane); | ||
| const setActiveTab = useTabsStore((s) => s.setActiveTab); | ||
| const { sessions, bindingsByPane } = useBrowserAutomationData({ | ||
| enabled: open, | ||
| }); | ||
| const openConnectModal = useBrowserAutomationStore((s) => s.openConnectModal); | ||
|
|
||
| const browserPanes = useMemo(() => { | ||
| const tabById = new Map(tabs.map((t) => [t.id, t])); | ||
| return Object.values(panes).filter( | ||
| (p) => | ||
| p.type === "webview" && | ||
| tabById.get(p.tabId)?.workspaceId === workspaceId, | ||
| ); | ||
| }, [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. | ||
| 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 needsSetupCount = sessions.filter( | ||
| (s) => s.mcpStatus === "missing", | ||
| ).length; | ||
|
|
||
| return ( | ||
| <Dialog open={open} onOpenChange={onOpenChange}> | ||
| <DialogContent className="!max-w-[640px] sm:!max-w-[640px] p-0 gap-0 overflow-hidden"> | ||
| <DialogHeader className="px-5 py-4 border-b"> | ||
| <DialogTitle className="text-sm">Browser Automation</DialogTitle> | ||
| <DialogDescription className="text-xs"> | ||
| All browser panes in this workspace and their bound sessions. | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
|
|
||
| <div className="p-4 grid grid-cols-3 gap-2 border-b"> | ||
| <Metric label="Browser panes" value={browserPanes.length} /> | ||
| <Metric label="Connected" value={connectedCount} /> | ||
| <Metric label="Needs setup" value={needsSetupCount} /> | ||
| </div> | ||
|
|
||
| <div className="max-h-[420px] overflow-y-auto p-3 flex flex-col gap-2"> | ||
| {browserPanes.length === 0 && ( | ||
| <div className="text-xs text-muted-foreground text-center py-8"> | ||
| No browser panes in this workspace. | ||
| </div> | ||
| )} | ||
| {browserPanes.map((pane) => { | ||
| 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"; | ||
| return ( | ||
| <div | ||
| key={pane.id} | ||
| className={cn( | ||
| "rounded-xl border p-3 bg-card/60", | ||
| session && "border-brand/30 bg-brand/5", | ||
| )} | ||
| > | ||
| <div className="flex items-start justify-between gap-3"> | ||
| <div className="min-w-0"> | ||
| <div className="text-xs font-semibold truncate"> | ||
| {pane.userTitle || pane.name} | ||
| </div> | ||
| <div className="text-[11px] text-muted-foreground truncate"> | ||
| {url} | ||
| </div> | ||
| </div> | ||
| <span | ||
| className={cn( | ||
| "shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider", | ||
| session | ||
| ? "bg-emerald-500/15 text-emerald-300" | ||
| : "bg-muted text-muted-foreground", | ||
| )} | ||
| > | ||
| {session ? "Connected" : "Unassigned"} | ||
| </span> | ||
| </div> | ||
| <div className="mt-2 text-[11px] text-muted-foreground"> | ||
| {session | ||
| ? `${session.displayName} · ${session.provider} · ${session.mcpStatus === "ready" ? "MCP ready" : "MCP missing"}` | ||
| : "Pick any running LLM session"} | ||
| </div> | ||
| <div className="mt-3 flex gap-2"> | ||
| <Button | ||
| size="sm" | ||
| variant="outline" | ||
| onClick={() => { | ||
| setActiveTab(workspaceId, pane.tabId); | ||
| setFocusedPane(pane.tabId, pane.id); | ||
| onOpenChange(false); | ||
|
MocA-Love marked this conversation as resolved.
|
||
| }} | ||
| > | ||
| Focus | ||
| </Button> | ||
| <Button | ||
| size="sm" | ||
| onClick={() => { | ||
| // SessionConnectModal is rendered inside each | ||
| // BrowserPane (gated by isConnectOpenForThisPane), so | ||
| // it only mounts when the owning tab/pane is active. | ||
| // Activate the target before opening the modal, or the | ||
| // dialog never appears if the pane lives on another | ||
| // tab. | ||
| setActiveTab(workspaceId, pane.tabId); | ||
| setFocusedPane(pane.tabId, pane.id); | ||
| openConnectModal(pane.id, sessionId); | ||
| onOpenChange(false); | ||
| }} | ||
|
MocA-Love marked this conversation as resolved.
|
||
| > | ||
| {session ? "Change" : "Connect"} | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ); | ||
| } | ||
|
|
||
| function Metric({ label, value }: { label: string; value: number }) { | ||
| return ( | ||
| <div className="rounded-lg bg-muted/40 p-3"> | ||
| <div className="text-xl font-bold tabular-nums">{value}</div> | ||
| <div className="mt-1 text-[10px] uppercase tracking-wider text-muted-foreground"> | ||
| {label} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
1 change: 1 addition & 0 deletions
1
...iew/ContentView/TabsContent/TabView/BrowserPane/components/BrowserAutomationList/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { BrowserAutomationList } from "./BrowserAutomationList"; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.