diff --git a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx
index 84011a3eaf..29fcd0a218 100644
--- a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx
+++ b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx
@@ -41,6 +41,11 @@ export type Stage = {
* The section is used to group stages together.
*/
section: string;
+ /**
+ * The agent ID for agent-related stages. Used to filter timings correctly
+ * when multiple agents exist.
+ */
+ agentId?: string;
/**
* The tooltip is used to provide additional information about the stage.
*/
@@ -268,12 +273,13 @@ export const provisioningStages: Stage[] = [
},
];
-export const agentStages = (section: string): Stage[] => {
+export const agentStages = (section: string, agentId: string): Stage[] => {
return [
{
name: "connect",
label: "connect",
section,
+ agentId,
tooltip: {
heading: "Connect",
description: "Establish an RPC connection with the control plane.",
@@ -283,6 +289,7 @@ export const agentStages = (section: string): Stage[] => {
name: "start",
label: "run startup scripts",
section,
+ agentId,
tooltip: {
heading: "Run startup scripts",
description: "Execute each agent startup script.",
diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx
index f82f1ccae5..5c7f91ddd6 100644
--- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx
+++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx
@@ -273,11 +273,140 @@ export const InvalidTimeRange: Story = {
display_name: "Startup Script 1",
started_at: "0001-01-01T00:00:00Z",
ended_at: "2025-01-01T00:10:00Z",
+ workspace_agent_id: "67e37a9d-ccac-497e-8f48-4093bcc4f3e7",
+ workspace_agent_name: "main",
},
],
},
};
+// Test case for multiple agents (e.g., main + devcontainer) where each agent
+// should only show its own timings, not duplicated across all agents.
+export const MultipleAgents: Story = {
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: {
+ provisionerTimings: [
+ {
+ job_id: "fb0a0941-5052-4f8b-8046-64da139220cd",
+ started_at: "2026-02-02T08:34:16.067798Z",
+ ended_at: "2026-02-02T08:34:16.626888Z",
+ stage: "init",
+ source: "coder",
+ action: "terraform",
+ resource: "coder_stage_init",
+ },
+ {
+ job_id: "fb0a0941-5052-4f8b-8046-64da139220cd",
+ started_at: "2026-02-02T08:34:16.649547Z",
+ ended_at: "2026-02-02T08:34:18.65871Z",
+ stage: "plan",
+ source: "coder",
+ action: "terraform",
+ resource: "coder_stage_plan",
+ },
+ {
+ job_id: "fb0a0941-5052-4f8b-8046-64da139220cd",
+ started_at: "2026-02-02T08:34:18.722631Z",
+ ended_at: "2026-02-02T08:34:21.332458Z",
+ stage: "apply",
+ source: "coder",
+ action: "terraform",
+ resource: "coder_stage_apply",
+ },
+ {
+ job_id: "fb0a0941-5052-4f8b-8046-64da139220cd",
+ started_at: "2026-02-02T08:34:21.698283Z",
+ ended_at: "2026-02-02T08:34:22.014735Z",
+ stage: "graph",
+ source: "coder",
+ action: "terraform",
+ resource: "coder_stage_graph",
+ },
+ ],
+ agentConnectionTimings: [
+ {
+ started_at: "2026-02-02T08:34:22.092544Z",
+ ended_at: "2026-02-02T08:34:23.090936Z",
+ stage: "connect",
+ workspace_agent_id: "a1d50955-a671-4e6c-8e0e-6bc938e931bf",
+ workspace_agent_name: "dev",
+ },
+ {
+ started_at: "2026-02-02T08:34:50.171921Z",
+ ended_at: "2026-02-02T08:34:51.36274Z",
+ stage: "connect",
+ workspace_agent_id: "afbdd368-b7b8-453e-af5a-02b13bc45553",
+ workspace_agent_name: "coder",
+ },
+ ],
+ agentScriptTimings: [
+ {
+ started_at: "2026-02-02T08:34:23.745887Z",
+ ended_at: "2026-02-02T08:34:25.23973Z",
+ exit_code: 0,
+ stage: "start",
+ status: "ok",
+ display_name: "Installing Dependencies",
+ workspace_agent_id: "a1d50955-a671-4e6c-8e0e-6bc938e931bf",
+ workspace_agent_name: "dev",
+ },
+ {
+ started_at: "2026-02-02T08:34:23.743853Z",
+ ended_at: "2026-02-02T08:34:23.809943Z",
+ exit_code: 0,
+ stage: "start",
+ status: "ok",
+ display_name: "Git Clone",
+ workspace_agent_id: "a1d50955-a671-4e6c-8e0e-6bc938e931bf",
+ workspace_agent_name: "dev",
+ },
+ {
+ started_at: "2026-02-02T08:34:23.74382Z",
+ ended_at: "2026-02-02T08:34:40.822488Z",
+ exit_code: 0,
+ stage: "start",
+ status: "ok",
+ display_name: "code-server",
+ workspace_agent_id: "a1d50955-a671-4e6c-8e0e-6bc938e931bf",
+ workspace_agent_name: "dev",
+ },
+ // Same display_name as dev agent to test dedup scoping by agent ID.
+ {
+ started_at: "2026-02-02T08:34:51.5Z",
+ ended_at: "2026-02-02T08:34:53.2Z",
+ exit_code: 0,
+ stage: "start",
+ status: "ok",
+ display_name: "Installing Dependencies",
+ workspace_agent_id: "afbdd368-b7b8-453e-af5a-02b13bc45553",
+ workspace_agent_name: "coder",
+ },
+ {
+ started_at: "2026-02-02T08:34:53.3Z",
+ ended_at: "2026-02-02T08:34:55.1Z",
+ exit_code: 0,
+ stage: "start",
+ status: "ok",
+ display_name: "Personalize",
+ workspace_agent_id: "afbdd368-b7b8-453e-af5a-02b13bc45553",
+ workspace_agent_name: "coder",
+ },
+ ],
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ // Verify both agents are shown
+ await canvas.findByText("agent (dev)");
+ await canvas.findByText("agent (coder)");
+ },
+};
+
// A template with no agent scripts.
export const NoAgentScripts: Story = {
args: {
diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx
index c4e0e63228..67365cf941 100644
--- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx
+++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx
@@ -52,21 +52,6 @@ export const WorkspaceTimings: FC = ({
defaultIsOpen = false,
}) => {
const [view, setView] = useState({ name: "default" });
- // This is a workaround to deal with the BE returning multiple timings for a
- // single agent script when it should return only one. Reference:
- // https://github.com/coder/coder/issues/15413#issuecomment-2493663571
- const uniqScriptTimings = uniqBy(
- sortBy(agentScriptTimings, (t) => new Date(t.started_at).getTime() * -1),
- (t) => t.display_name,
- );
- const timings = [
- ...provisionerTimings,
- ...uniqScriptTimings,
- ...agentConnectionTimings,
- ].sort((a, b) => {
- return new Date(a.started_at).getTime() - new Date(b.started_at).getTime();
- });
-
const [isOpen, setIsOpen] = useState(defaultIsOpen);
// If any of the timings are empty, we are still loading the data. They can be
@@ -79,21 +64,37 @@ export const WorkspaceTimings: FC = ({
// at least one entry.
].some((t) => t.length === 0);
+ // This is a workaround to deal with the BE returning multiple timings for a
+ // single agent script when it should return only one. Reference:
+ // https://github.com/coder/coder/issues/15413#issuecomment-2493663571
+ const uniqScriptTimings = sortBy(
+ uniqBy(
+ sortBy(agentScriptTimings, (t) => t.started_at).reverse(),
+ (t) => `${t.workspace_agent_id}:${t.display_name}`,
+ ),
+ (t) => t.started_at,
+ );
+
+ // Combine agent timings for filtering by agent ID.
+ const agentTimings = [...agentConnectionTimings, ...uniqScriptTimings];
+
// Each agent connection timing is a stage in the timeline to make it easier
// to users to see the timing for connection and the other scripts.
- const agentStageLabels = Array.from(
- new Set(
- agentConnectionTimings.map((t) => `agent (${t.workspace_agent_name})`),
- ),
- );
+ const agents = uniqBy(agentConnectionTimings, (t) => t.workspace_agent_id);
const stages = [
...provisioningStages,
- ...agentStageLabels.flatMap((a) => agentStages(a)),
+ ...agents.flatMap((agent) =>
+ agentStages(
+ `agent (${agent.workspace_agent_name})`,
+ agent.workspace_agent_id,
+ ),
+ ),
];
const displayProvisioningTime = () => {
- const totalRange = mergeTimeRanges(timings.map(toTimeRange));
+ const allTimings = [...provisionerTimings, ...agentTimings];
+ const totalRange = mergeTimeRanges(allTimings.map(toTimeRange));
const totalDuration = calcDuration(totalRange);
return formatTime(totalDuration);
};
@@ -131,9 +132,13 @@ export const WorkspaceTimings: FC = ({
{view.name === "default" && (
{
- const stageTimings = timings.filter(
- (t) => t.stage === s.name,
- );
+ const stageTimings = s.agentId
+ ? agentTimings.filter(
+ (t) =>
+ t.stage === s.name &&
+ t.workspace_agent_id === s.agentId,
+ )
+ : provisionerTimings.filter((t) => t.stage === s.name);
const stageRange =
stageTimings.length === 0
? undefined
@@ -196,8 +201,12 @@ export const WorkspaceTimings: FC = ({
{view.stage.name === "start" && (
t.stage === view.stage.name)
+ timings={uniqScriptTimings
+ .filter(
+ (t) =>
+ t.stage === view.stage.name &&
+ t.workspace_agent_id === view.stage.agentId,
+ )
.map((t) => {
return {
range: toTimeRange(t),
@@ -270,6 +279,6 @@ const styles = {
borderTop: `1px solid ${theme.palette.divider}`,
display: "flex",
flexDirection: "column",
- height: 420,
+ height: "var(--collapse-body-height, 420px)",
}),
} satisfies Record>;