Repository navigation
feat(desktop): running agents section in workspace sidebar #5414
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
Changes from all commits
86769ee
f932581
44470d7
6cb32c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { cn } from "@superset/ui/utils"; | ||
| import { DashboardSidebarWorkspaceAgentBadge } from "./components/DashboardSidebarWorkspaceAgentBadge"; | ||
| import { useDashboardSidebarWorkspaceRunningAgents } from "./hooks/useDashboardSidebarWorkspaceRunningAgents"; | ||
|
|
||
| interface DashboardSidebarWorkspaceAgentsRowProps { | ||
| workspaceId: string; | ||
| isInSection?: boolean; | ||
| } | ||
|
|
||
| export function DashboardSidebarWorkspaceAgentsRow({ | ||
| workspaceId, | ||
| isInSection = false, | ||
| }: DashboardSidebarWorkspaceAgentsRowProps) { | ||
| const agents = useDashboardSidebarWorkspaceRunningAgents(workspaceId); | ||
|
|
||
| if (agents.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn( | ||
| "flex flex-wrap gap-1 pr-2 pb-1", | ||
| isInSection ? "pl-14" : "pl-12", | ||
| )} | ||
| > | ||
| {agents.map((agent) => ( | ||
| <DashboardSidebarWorkspaceAgentBadge | ||
| key={agent.sourceKey} | ||
| workspaceId={workspaceId} | ||
| agent={agent} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; | ||
| import { cn } from "@superset/ui/utils"; | ||
| import { useNavigate } from "@tanstack/react-router"; | ||
| import { LuBot } from "react-icons/lu"; | ||
| import { usePresetIcon } from "renderer/assets/app-icons/preset-icons"; | ||
| import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; | ||
| import { | ||
| getStatusTooltip, | ||
| StatusIndicator, | ||
| } from "renderer/screens/main/components/StatusIndicator"; | ||
| import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; | ||
| import type { DashboardSidebarRunningAgent } from "../../hooks/useDashboardSidebarWorkspaceRunningAgents"; | ||
|
|
||
| interface DashboardSidebarWorkspaceAgentBadgeProps { | ||
| workspaceId: string; | ||
| agent: DashboardSidebarRunningAgent; | ||
| } | ||
|
|
||
| export function DashboardSidebarWorkspaceAgentBadge({ | ||
| workspaceId, | ||
| agent, | ||
| }: DashboardSidebarWorkspaceAgentBadgeProps) { | ||
| const navigate = useNavigate(); | ||
| const iconUrl = usePresetIcon(agent.agentId); | ||
|
|
||
| const handleClick = () => { | ||
| void navigateToV2Workspace(workspaceId, navigate, { | ||
| search: { | ||
| terminalId: agent.terminalId, | ||
| focusRequestId: crypto.randomUUID(), | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| const statusLabel = | ||
| agent.status === "idle" ? "Idle" : getStatusTooltip(agent.status); | ||
|
|
||
| return ( | ||
| <Tooltip delayDuration={700}> | ||
| <TooltipTrigger asChild> | ||
| <button | ||
| type="button" | ||
| onClick={handleClick} | ||
| className={cn( | ||
| "inline-flex max-w-40 items-center gap-1 rounded-md px-1.5 py-0.5", | ||
| "bg-primary/10 text-xs font-medium text-primary transition-colors hover:bg-primary/20", | ||
| "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", | ||
| )} | ||
| > | ||
| <span className="flex size-3.5 shrink-0 items-center justify-center"> | ||
| {agent.status === "idle" ? ( | ||
| iconUrl ? ( | ||
| <img src={iconUrl} alt="" className="size-3.5 object-contain" /> | ||
| ) : ( | ||
| <LuBot className="size-3.5" strokeWidth={STROKE_WIDTH} /> | ||
| ) | ||
| ) : ( | ||
| <StatusIndicator status={agent.status} /> | ||
| )} | ||
| </span> | ||
|
Comment on lines
+50
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
fd -t f "StatusIndicator" apps/desktop/src/renderer/screens/main/components/StatusIndicator --exec cat -n {}Repository: superset-sh/superset Length of output: 2336 Match 🤖 Prompt for AI Agents |
||
| <span className="min-w-0 truncate">{agent.label}</span> | ||
| </button> | ||
| </TooltipTrigger> | ||
| <TooltipContent side="top" sideOffset={6} showArrow={false}> | ||
| <div className="space-y-1 text-xs"> | ||
| <div className="font-medium">{agent.label}</div> | ||
| <div className="text-background/70">{statusLabel}</div> | ||
| <div className="text-[10px] text-background/60"> | ||
| Click to open agent | ||
| </div> | ||
| </div> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { DashboardSidebarWorkspaceAgentBadge } from "./DashboardSidebarWorkspaceAgentBadge"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export { | ||
| type DashboardSidebarRunningAgent, | ||
| type RunningAgentStatus, | ||
| useDashboardSidebarWorkspaceRunningAgents, | ||
| } from "./useDashboardSidebarWorkspaceRunningAgents"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { | ||
| BUILTIN_AGENT_LABELS, | ||
| type BuiltinAgentId, | ||
| } from "@superset/shared/agent-catalog"; | ||
| import { useMemo } from "react"; | ||
| import { useTerminalAgentBindings } from "renderer/hooks/host-service/useTerminalAgentBindings"; | ||
| import { | ||
| useV2NotificationStore, | ||
| type V2NotificationSource, | ||
| } from "renderer/stores/v2-notifications"; | ||
| import type { PaneStatus } from "shared/tabs-types"; | ||
| import { useShallow } from "zustand/react/shallow"; | ||
|
|
||
| /** | ||
| * State of a bound agent. `idle` means the agent process is alive but not | ||
| * currently `working` / awaiting `permission` / ready for `review`. | ||
| */ | ||
| export type RunningAgentStatus = PaneStatus; | ||
|
|
||
| export interface DashboardSidebarRunningAgent { | ||
| /** Stable key for React lists, derived from the notification source. */ | ||
| sourceKey: string; | ||
| source: V2NotificationSource; | ||
| /** Host terminal the agent is bound to. */ | ||
| terminalId: string; | ||
| /** Built-in agent id (`claude`, `codex`, …) — drives label + icon. */ | ||
| agentId: BuiltinAgentId; | ||
| /** `idle` | `working` | `permission` | `review`. */ | ||
| status: RunningAgentStatus; | ||
| /** When the agent process was bound (ms since epoch), used for stable order. */ | ||
| startedAt: number; | ||
| /** Agent display name (e.g. "Claude"). */ | ||
| label: string; | ||
| } | ||
|
|
||
| /** | ||
| * Live list of agents bound to a workspace's terminals, newest binding last. | ||
| * Every live agent process is included regardless of state; its `status` comes | ||
| * from the notification store (or `idle` when it has no active status). | ||
| * | ||
| * Mirrors {@link useDashboardSidebarWorkspacePorts} so a workspace detail row | ||
| * can render agents the same way it renders ports. | ||
| */ | ||
| export function useDashboardSidebarWorkspaceRunningAgents( | ||
| workspaceId: string, | ||
| enabled = true, | ||
| ): DashboardSidebarRunningAgent[] { | ||
| const bindings = useTerminalAgentBindings(workspaceId, { enabled }); | ||
|
|
||
| const statusByTerminal = useV2NotificationStore( | ||
| useShallow((state) => { | ||
| const map: Record<string, RunningAgentStatus> = {}; | ||
| for (const entry of Object.values(state.sources)) { | ||
| if ( | ||
| entry.workspaceId === workspaceId && | ||
| entry.source.type === "terminal" | ||
| ) { | ||
| map[entry.source.id] = entry.status; | ||
| } | ||
| } | ||
| return map; | ||
| }), | ||
| ); | ||
|
|
||
| return useMemo(() => { | ||
| const agents: DashboardSidebarRunningAgent[] = []; | ||
| for (const binding of bindings.values()) { | ||
| agents.push({ | ||
| sourceKey: `terminal:${binding.terminalId}`, | ||
| source: { type: "terminal", id: binding.terminalId }, | ||
| terminalId: binding.terminalId, | ||
| agentId: binding.agentId, | ||
| status: statusByTerminal[binding.terminalId] ?? "idle", | ||
| startedAt: binding.startedAt, | ||
| label: BUILTIN_AGENT_LABELS[binding.agentId] ?? binding.agentId, | ||
| }); | ||
| } | ||
| agents.sort((a, b) => a.startedAt - b.startedAt); | ||
| return agents; | ||
| }, [bindings, statusByTerminal]); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export { DashboardSidebarWorkspaceAgentsRow } from "./DashboardSidebarWorkspaceAgentsRow"; | ||
| export { | ||
| type DashboardSidebarRunningAgent, | ||
| useDashboardSidebarWorkspaceRunningAgents, | ||
| } from "./hooks/useDashboardSidebarWorkspaceRunningAgents"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The
StatusIndicatorrenders at its defaultsize-2(8px) inside the fixedsize-3.5(14px) container. In contrast, the idle state renders icons at the fullsize-3.5. This size mismatch means the non-idle state glyph appears noticeably smaller than the idle icon, which undermines the goal of a fixed-size slot with no layout shifts. Consider passing aclassNameto scale the indicator to match the 14px slot (e.g.,className="size-3.5") or wrapping it to fill the container.Prompt for AI agents