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
116 changes: 115 additions & 1 deletion src/browser/components/AgentListItem/AgentListItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import type * as RuntimeStatusStoreModuleType from "@/browser/stores/RuntimeStat
import type * as WorkspaceStoreModule from "@/browser/stores/WorkspaceStore";
import * as TooltipModule from "../Tooltip/Tooltip";
import * as WorkspaceStatusIndicatorModule from "../WorkspaceStatusIndicator/WorkspaceStatusIndicator";
import type { AgentRowRenderMeta } from "@/browser/utils/ui/workspaceFiltering";
import type {
AgentRowRenderMeta,
WorkspaceDelegatedActivity,
} from "@/browser/utils/ui/workspaceFiltering";
import type { StreamAbortReasonSnapshot } from "@/common/types/stream";
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
import type { WorkspaceSelection } from "./AgentListItem";
Expand Down Expand Up @@ -241,6 +244,7 @@ function renderWorkspaceItem(
depth?: number;
rowRenderMeta?: AgentRowRenderMeta;
subAgentConnectorLayout?: "default" | "task-group-member";
delegatedActivity?: WorkspaceDelegatedActivity;
completedChildrenExpanded?: boolean;
onToggleCompletedChildren?: (workspaceId: string) => void;
onSelectWorkspace?: (selection: WorkspaceSelection) => void;
Expand All @@ -257,6 +261,7 @@ function renderWorkspaceItem(
depth={options.depth ?? options.rowRenderMeta?.depth}
rowRenderMeta={options.rowRenderMeta}
subAgentConnectorLayout={options.subAgentConnectorLayout}
delegatedActivity={options.delegatedActivity}
completedChildrenExpanded={options.completedChildrenExpanded}
onToggleCompletedChildren={options.onToggleCompletedChildren}
onSelectWorkspace={options.onSelectWorkspace ?? (() => undefined)}
Expand Down Expand Up @@ -298,6 +303,115 @@ describe("AgentListItem", () => {
mock.restore();
});

test("shows active delegated workflow work on idle workspace rows", () => {
const { row } = renderWorkspaceItem({
delegatedActivity: {
activeCount: 2,
queuedCount: 1,
workflowActiveCount: 2,
workflowQueuedCount: 1,
},
});
const rowView = within(row);

expect(row.querySelector(".workspace-status-dot-active")).toBeTruthy();
expect(rowView.getByText("Workflow running Β· 2 sub-agents active Β· 1 queued")).toBeTruthy();
const descriptionId = row.getAttribute("aria-describedby");
expect(descriptionId).toBe(`workspace-status-description-${TEST_WORKSPACE_ID}`);
expect(document.getElementById(descriptionId ?? "")?.textContent).toContain(
"Workflow running Β· 2 sub-agents active Β· 1 queued"
);
expect(rowView.queryByTestId(`workspace-status-indicator-${TEST_WORKSPACE_ID}`)).toBeNull();
});

test("keeps coordinator streaming copy ahead of delegated workflow work", () => {
mockWorkspaceSidebarState = createWorkspaceSidebarState({ canInterrupt: true });

const { row } = renderWorkspaceItem({
delegatedActivity: {
activeCount: 1,
queuedCount: 0,
workflowActiveCount: 1,
workflowQueuedCount: 0,
},
});
const rowView = within(row);

expect(row.querySelector(".workspace-status-dot-active")).toBeTruthy();
expect(rowView.getByTestId(`workspace-status-indicator-${TEST_WORKSPACE_ID}`)).toBeTruthy();
expect(rowView.queryByText("Workflow running Β· 1 sub-agent active")).toBeNull();
});

test("keeps coordinator streaming copy ahead of active delegated work", () => {
mockWorkspaceSidebarState = createWorkspaceSidebarState({ canInterrupt: true });

const { row } = renderWorkspaceItem({
delegatedActivity: {
activeCount: 1,
queuedCount: 0,
workflowActiveCount: 0,
workflowQueuedCount: 0,
},
});
const rowView = within(row);

expect(row.querySelector(".workspace-status-dot-active")).toBeTruthy();
expect(rowView.getByTestId(`workspace-status-indicator-${TEST_WORKSPACE_ID}`)).toBeTruthy();
expect(rowView.queryByText("1 sub-agent active")).toBeNull();
});

test("keeps own question status ahead of delegated workflow work", () => {
mockWorkspaceSidebarState = createWorkspaceSidebarState({ awaitingUserQuestion: true });

const { row } = renderWorkspaceItem({
delegatedActivity: {
activeCount: 1,
queuedCount: 0,
workflowActiveCount: 1,
workflowQueuedCount: 0,
},
});
const rowView = within(row);

expect(row.querySelector(".bg-border-pending.border-surface-sky")).toBeTruthy();
expect(rowView.getByText("Mux has a few questions")).toBeTruthy();
expect(rowView.queryByText("Workflow running Β· 1 sub-agent active")).toBeNull();
});

test("shows queued delegated workflow work without marking the row active", () => {
const { row } = renderWorkspaceItem({
delegatedActivity: {
activeCount: 0,
queuedCount: 1,
workflowActiveCount: 0,
workflowQueuedCount: 1,
},
});
const rowView = within(row);

expect(row.querySelector(".workspace-status-dot-active")).toBeNull();
expect(rowView.getByText("Workflow queued Β· 1 sub-agent queued")).toBeTruthy();
});

test("keeps own system errors ahead of delegated workflow work", () => {
mockWorkspaceSidebarState = createWorkspaceSidebarState({
lastAbortReason: createSystemAbortReason(),
});

const { row } = renderWorkspaceItem({
delegatedActivity: {
activeCount: 1,
queuedCount: 0,
workflowActiveCount: 1,
workflowQueuedCount: 0,
},
});
const rowView = within(row);

expect(row.querySelector(".bg-content-destructive.border-surface-destructive")).toBeTruthy();
expect(rowView.queryByText("Workflow running Β· 1 sub-agent active")).toBeNull();
});

test("keeps archiving feedback inline instead of rendering a secondary status row", () => {
const { row } = renderWorkspaceItem({ isArchiving: true });
const rowView = within(row);
Expand Down
79 changes: 76 additions & 3 deletions src/browser/components/AgentListItem/AgentListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { useWorkspaceUnread } from "@/browser/hooks/useWorkspaceUnread";
import { useRuntimeStatus } from "@/browser/stores/RuntimeStatusStore";
import { useWorkspaceSidebarState } from "@/browser/stores/WorkspaceStore";
import { stopKeyboardPropagation } from "@/browser/utils/events";
import type { AgentRowRenderMeta } from "@/browser/utils/ui/workspaceFiltering";
import type {
AgentRowRenderMeta,
WorkspaceDelegatedActivity,
} from "@/browser/utils/ui/workspaceFiltering";
import { cn } from "@/common/lib/utils";
import {
TASK_GROUP_KIND,
Expand Down Expand Up @@ -104,6 +107,7 @@ export interface AgentListItemProps extends AgentListItemBaseProps {
/** Section ID this workspace belongs to (for drag-drop targeting) */
sectionId?: string;
rowRenderMeta?: AgentRowRenderMeta;
delegatedActivity?: WorkspaceDelegatedActivity;
completedChildrenExpanded?: boolean;
onToggleCompletedChildren?: (workspaceId: string) => void;
onSelectWorkspace: (selection: WorkspaceSelection) => void;
Expand Down Expand Up @@ -150,6 +154,7 @@ function getVisualState(opts: {
isArchiving: boolean;
isWorking: boolean;
isStarting: boolean;
hasActiveDelegatedWork: boolean;
isUnread: boolean;
isSelected: boolean;
hasError: boolean;
Expand All @@ -163,7 +168,7 @@ function getVisualState(opts: {
if (opts.awaitingUserQuestion) {
return "question";
}
if (opts.isWorking || opts.isStarting || opts.isInitializing) {
if (opts.isWorking || opts.isStarting || opts.isInitializing || opts.hasActiveDelegatedWork) {
return "active";
}
// Avoid unread flicker for the currently selected workspace while last-read
Expand Down Expand Up @@ -248,6 +253,50 @@ function StatusDot(props: {
);
}

function formatSubAgentCount(count: number, label: "active" | "queued"): string {
return `${count} sub-agent${count === 1 ? "" : "s"} ${label}`;
}

function formatDelegatedActivityText(activity: WorkspaceDelegatedActivity): string | null {
const parts: string[] = [];
if (activity.activeCount > 0) {
if (activity.workflowActiveCount > 0) {
parts.push("Workflow running");
}
parts.push(formatSubAgentCount(activity.activeCount, "active"));
} else if (activity.queuedCount > 0) {
if (activity.workflowQueuedCount > 0) {
parts.push("Workflow queued");
}
parts.push(formatSubAgentCount(activity.queuedCount, "queued"));
}

if (activity.activeCount > 0 && activity.queuedCount > 0) {
parts.push(`${activity.queuedCount} queued`);
}

return parts.length > 0 ? parts.join(" Β· ") : null;
}

function DelegatedActivityIndicator(props: {
workspaceId: string;
activity: WorkspaceDelegatedActivity;
}) {
const statusText = formatDelegatedActivityText(props.activity);
if (!statusText) {
return null;
}

return (
<div
className="text-muted flex min-w-0 items-center gap-1.5 text-xs leading-4"
data-testid={`workspace-delegated-activity-${props.workspaceId}`}
>
<span className="min-w-0 truncate">{statusText}</span>
</div>
);
}

function QuickArchiveButton(props: {
displayTitle: string;
onArchiveWorkspace: (button: HTMLElement) => void;
Expand Down Expand Up @@ -432,6 +481,7 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
depth,
sectionId,
rowRenderMeta,
delegatedActivity,
completedChildrenExpanded,
onToggleCompletedChildren,
onSelectWorkspace,
Expand Down Expand Up @@ -603,20 +653,30 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
useWorkspaceStreamingStatusPhase(streamingStatusPhase);
const isWorking = displayStreamingStatusPhase !== null && !awaitingUserQuestion;
const hasError = lastAbortReason?.reason === "system";
const hasActiveDelegatedWork = (delegatedActivity?.activeCount ?? 0) > 0;
const delegatedStatusText = delegatedActivity
? formatDelegatedActivityText(delegatedActivity)
: null;
const hasDelegatedStatusText = delegatedStatusText != null;
const hasOwnLiveStatusText =
awaitingUserQuestion || displayStreamingStatusPhase !== null || isRemoving;
const shouldShowDelegatedStatus = hasDelegatedStatusText && !hasOwnLiveStatusText && !hasError;
const visualState = getVisualState({
awaitingUserQuestion,
isInitializing,
isRemoving,
isArchiving: isArchiving === true,
isWorking,
isStarting: displayStreamingStatusPhase === "starting",
hasActiveDelegatedWork,
isUnread,
isSelected,
hasError,
});
const isSubAgentRow = rowRenderMeta?.rowKind === "subagent";
const showsVisibleStatusDot = isStatusDotVisible(visualState, false, isSubAgentRow);
const hasStatusText =
shouldShowDelegatedStatus ||
Boolean(agentStatus) ||
awaitingUserQuestion ||
displayStreamingStatusPhase !== null ||
Expand All @@ -627,6 +687,9 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
// Note: we intentionally render the secondary row even while the workspace is still
// initializing so users can see early streaming/status information immediately.
const hasSecondaryRow = !shouldShowInlineArchivingStatus && hasStatusText;
const secondaryStatusDescriptionId = hasSecondaryRow
? `workspace-status-description-${workspaceId}`
: undefined;
const hasCompletedChildren =
(rowRenderMeta?.hasHiddenCompletedChildren ?? false) ||
(rowRenderMeta?.visibleCompletedChildrenCount ?? 0) > 0;
Expand Down Expand Up @@ -784,6 +847,7 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
? `Archiving workspace ${displayTitle}`
: `Select workspace ${displayTitle}`
}
aria-describedby={secondaryStatusDescriptionId}
aria-disabled={isDisabled}
data-workspace-path={namedWorkspacePath}
data-workspace-id={workspaceId}
Expand Down Expand Up @@ -1073,7 +1137,11 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
)}
</div>
{hasSecondaryRow && (
<div className="min-w-0" data-testid={`workspace-secondary-row-${workspaceId}`}>
<div
id={secondaryStatusDescriptionId}
className="min-w-0"
data-testid={`workspace-secondary-row-${workspaceId}`}
>
{isRemoving ? (
<div className="text-muted flex min-w-0 items-center gap-1.5 text-xs">
<Loader2 className="h-3 w-3 shrink-0 animate-spin" />
Expand All @@ -1084,6 +1152,11 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
<MessageCircleQuestionMark className="h-3 w-3 shrink-0" strokeWidth={1.8} />
<span className="min-w-0 truncate">Mux has a few questions</span>
</div>
) : shouldShowDelegatedStatus && delegatedActivity ? (
<DelegatedActivityIndicator
workspaceId={workspaceId}
activity={delegatedActivity}
/>
) : (
<WorkspaceStatusIndicator
workspaceId={workspaceId}
Expand Down
Loading
Loading