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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type TerminalAgentBinding = TerminalAgentBindings[number];
*/
export function useTerminalAgentBindings(
workspaceId: string,
options?: { enabled?: boolean },
): Map<string, TerminalAgentBinding> {
const hostUrl = useWorkspaceHostUrl(workspaceId);
const queryClient = useQueryClient();
Expand All @@ -26,7 +27,8 @@ export function useTerminalAgentBindings(
[hostUrl, workspaceId],
);

const enabled = Boolean(workspaceId) && Boolean(hostUrl);
const enabled =
(options?.enabled ?? true) && Boolean(workspaceId) && Boolean(hostUrl);

const { data } = useQuery({
queryKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useDiffStats } from "renderer/hooks/host-service/useDiffStats";
import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions";
import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider";
import { RenameBranchDialog } from "renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components";
import { useInlineWorkspacePortsEnabled } from "renderer/stores/inline-workspace-ports";
import { useV2WorkspaceNotificationStatus } from "renderer/stores/v2-notifications";
import { useDashboardSidebarHover } from "../../providers/DashboardSidebarHoverProvider";
import type { DashboardSidebarWorkspace } from "../../types";
Expand Down Expand Up @@ -43,7 +42,6 @@ export function DashboardSidebarWorkspaceItem({
const isMainWorkspace = workspace.type === "main";
const diffStats = useDiffStats(id);
const workspaceStatus = useV2WorkspaceNotificationStatus(id);
const inlineWorkspacePortsEnabled = useInlineWorkspacePortsEnabled();
const {
cancelRename,
handleClearStatus,
Expand Down Expand Up @@ -229,7 +227,7 @@ export function DashboardSidebarWorkspaceItem({
onSubmitRename={submitRename}
onCancelRename={cancelRename}
>
{!isPending && inlineWorkspacePortsEnabled && (
{!isPending && (
<DashboardSidebarWorkspaceDetails
workspaceId={id}
isInSection={isInSection}
Expand Down
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} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The StatusIndicator renders at its default size-2 (8px) inside the fixed size-3.5 (14px) container. In contrast, the idle state renders icons at the full size-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 a className to 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
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/components/DashboardSidebarWorkspaceAgentBadge/DashboardSidebarWorkspaceAgentBadge.tsx, line 58:

<comment>The `StatusIndicator` renders at its default `size-2` (8px) inside the fixed `size-3.5` (14px) container. In contrast, the idle state renders icons at the full `size-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 a `className` to scale the indicator to match the 14px slot (e.g., `className="size-3.5"`) or wrapping it to fill the container.</comment>

<file context>
@@ -0,0 +1,75 @@
+								<LuBot className="size-3.5" strokeWidth={STROKE_WIDTH} />
+							)
+						) : (
+							<StatusIndicator status={agent.status} />
+						)}
+					</span>
</file context>

)}
</span>
Comment on lines +50 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 StatusIndicator to the 14px badge slot. It currently renders at size-2 (8px), so the non-idle state reads smaller than the icon state inside the fixed size-3.5 container. Pass a matching className here or bump the default indicator size.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/components/DashboardSidebarWorkspaceAgentBadge/DashboardSidebarWorkspaceAgentBadge.tsx`
around lines 50 - 60, The non-idle badge state is rendering smaller than the
idle icon inside the fixed DashboardSidebarWorkspaceAgentBadge slot. Update the
DashboardSidebarWorkspaceAgentBadge rendering so the StatusIndicator used for
agent.status !== "idle" matches the 14px container, either by passing a matching
className at the call site or by increasing StatusIndicator’s default size where
it is defined.

<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";
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import { Fragment, type ReactNode } from "react";
import { LuX } from "react-icons/lu";
import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants";
import { useWorkspaceDetailsStore } from "renderer/stores";
import { useInlineWorkspacePortsEnabled } from "renderer/stores/inline-workspace-ports";
import { useWorkspaceAgentsRowEnabled } from "renderer/stores/workspace-agents-row";
import { useDashboardSidebarWorkspacePorts } from "../../../../providers/DashboardSidebarPortsProvider";
import { useDashboardSidebarPortKill } from "../../../DashboardSidebarPortsList/hooks/useDashboardSidebarPortKill";
import { DashboardSidebarWorkspaceAgentsRow } from "../DashboardSidebarWorkspaceAgentsRow";
import { useDashboardSidebarWorkspaceRunningAgents } from "../DashboardSidebarWorkspaceAgentsRow/hooks/useDashboardSidebarWorkspaceRunningAgents";
import { DashboardSidebarWorkspacePortsRow } from "../DashboardSidebarWorkspacePortsRow";
import { DashboardSidebarWorkspaceDetailsAction } from "./components/DashboardSidebarWorkspaceDetailsAction";
import { DashboardSidebarWorkspaceDetailsToggle } from "./components/DashboardSidebarWorkspaceDetailsToggle";
Expand Down Expand Up @@ -45,13 +49,15 @@ export function DashboardSidebarWorkspaceDetails({
);
const { isPending: isKillingPorts, killPorts } =
useDashboardSidebarPortKill();
const inlineWorkspacePortsEnabled = useInlineWorkspacePortsEnabled();
const workspaceAgentsRowEnabled = useWorkspaceAgentsRowEnabled();

// --- Section registry -----------------------------------------------------
const sections: WorkspaceDetailSection[] = [];

const portGroup = useDashboardSidebarWorkspacePorts(workspaceId);
const portCount = portGroup?.ports.length ?? 0;
if (portGroup && portCount > 0) {
if (inlineWorkspacePortsEnabled && portGroup && portCount > 0) {
sections.push({
key: "ports",
summary: `${portCount} ${portCount === 1 ? "port" : "ports"}`,
Expand All @@ -72,7 +78,23 @@ export function DashboardSidebarWorkspaceDetails({
});
}

// Add more detail rows here (e.g. running agents).
const runningAgents = useDashboardSidebarWorkspaceRunningAgents(
workspaceId,
workspaceAgentsRowEnabled,
);
const agentCount = runningAgents.length;
if (workspaceAgentsRowEnabled && agentCount > 0) {
sections.push({
key: "agents",
summary: `${agentCount} ${agentCount === 1 ? "agent" : "agents"}`,
content: (
<DashboardSidebarWorkspaceAgentsRow
workspaceId={workspaceId}
isInSection={isInSection}
/>
),
});
}
// --------------------------------------------------------------------------

if (sections.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { OverflowFadeContainer } from "@superset/ui/overflow-fade-container";
import { cn } from "@superset/ui/utils";
import { useDashboardSidebarWorkspacePorts } from "../../../../providers/DashboardSidebarPortsProvider";
import { DashboardSidebarPortBadge } from "../../../DashboardSidebarPortsList/components/DashboardSidebarPortBadge";
Expand All @@ -19,10 +18,9 @@ export function DashboardSidebarWorkspacePortsRow({
}

return (
<OverflowFadeContainer
observeChildren
<div
className={cn(
"grid auto-cols-max grid-flow-col grid-rows-2 gap-1 overflow-x-auto pr-2 pb-1 hide-scrollbar",
"flex flex-wrap gap-1 pr-2 pb-1",
isInSection ? "pl-14" : "pl-12",
)}
>
Expand All @@ -32,6 +30,6 @@ export function DashboardSidebarWorkspacePortsRow({
port={port}
/>
))}
</OverflowFadeContainer>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
} from "renderer/stores/inline-workspace-ports";
import { useOpenV1ImportModal } from "renderer/stores/v1-import-modal";
import { useV2LocalOverrideStore } from "renderer/stores/v2-local-override";
import {
useWorkspaceAgentsRowEnabled,
useWorkspaceAgentsRowStore,
} from "renderer/stores/workspace-agents-row";
import {
isItemVisible,
SETTING_ITEM_ID,
Expand All @@ -37,6 +41,10 @@ export function ExperimentalSettings({
SETTING_ITEM_ID.EXPERIMENTAL_INLINE_WORKSPACE_PORTS,
visibleItems,
);
const showWorkspaceAgents = isItemVisible(
SETTING_ITEM_ID.EXPERIMENTAL_WORKSPACE_AGENTS,
visibleItems,
);
const isV2CloudEnabled = useIsV2CloudEnabled();
const isV2OnlyUser = useIsV2OnlyUser();
const setOptInV2 = useV2LocalOverrideStore((state) => state.setOptInV2);
Expand All @@ -45,6 +53,10 @@ export function ExperimentalSettings({
const setInlineWorkspacePortsEnabled = useInlineWorkspacePortsStore(
(state) => state.setEnabled,
);
const workspaceAgentsEnabled = useWorkspaceAgentsRowEnabled();
const setWorkspaceAgentsEnabled = useWorkspaceAgentsRowStore(
(state) => state.setEnabled,
);

return (
<div className="p-6 max-w-4xl w-full mx-auto">
Expand Down Expand Up @@ -126,6 +138,24 @@ export function ExperimentalSettings({
/>
</div>
)}
{showWorkspaceAgents && (
<div className="flex items-center justify-between gap-6">
<div className="min-w-0 flex-1 space-y-0.5">
<Label htmlFor="workspace-agents" className="text-sm font-medium">
Workspace agents
</Label>
<p className="text-xs text-muted-foreground">
Show running agents under each workspace in the sidebar, with
their live status.
</p>
</div>
<Switch
id="workspace-agents"
checked={workspaceAgentsEnabled}
onCheckedChange={setWorkspaceAgentsEnabled}
/>
</div>
)}
</div>
</div>
);
Expand Down
Loading
Loading