fix(site): default agent logs tab to failed script, else All Logs (#25442) (#26067)

Co-authored-by: Jake Howell <jacob@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: Jeremy Ruppel <jeremy.ruppel@gmail.com>
This commit is contained in:
github-actions[bot]
2026-06-04 19:19:05 +05:00
committed by GitHub
parent e1d7ab0f68
commit 328c649c0f
2 changed files with 204 additions and 13 deletions
@@ -189,6 +189,42 @@ export const Connecting: Story = {
},
};
export const ConnectingWithStartupLogs: Story = {
args: {
agent: {
...M.MockWorkspaceAgentConnecting,
logs_length: 1,
},
initialMetadata: [],
},
parameters: {
webSocket: [
{
event: "message",
data: JSON.stringify([
{
id: 1,
level: "info",
output: "starting up",
source_id: M.MockWorkspaceAgentLogSource.id,
created_at: fixedLogTimestamp,
},
]),
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Agent is connecting (hasAgentIssues=true) but no script has failed.
// Old code snapped to the Startup Script tab; the fix keeps us on All Logs.
const allLogsTab = await canvas.findByRole("tab", { name: "All Logs" });
await waitFor(() =>
expect(allLogsTab).toHaveAttribute("data-state", "active"),
);
},
};
export const Timeout: Story = {
args: {
agent: M.MockWorkspaceAgentTimeout,
@@ -257,6 +293,135 @@ export const StartError: Story = {
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// MockWorkspaceAgentStartError ships with a Startup Script whose script
// has exit_code: 1, so the auto-select should land us there.
const startupScriptTab = await canvas.findByRole("tab", {
name: "Startup Script",
});
await waitFor(() =>
expect(startupScriptTab).toHaveAttribute("data-state", "active"),
);
},
};
export const StartErrorWithoutFailedSourceLogs: Story = {
args: {
agent: M.MockWorkspaceAgentStartError,
},
parameters: {
// Send log entries only for the OK script, mirroring the case where a
// failed script never emitted any output. The selected tab must not be
// initialized to a source that has no rendered tab.
webSocket: [
{
event: "message",
data: JSON.stringify(
M.MockWorkspaceAgentStartError.log_sources
.filter((source) => {
const script = M.MockWorkspaceAgentStartError.scripts.find(
(s) => s.log_source_id === source.id,
);
return !script?.exit_code && script?.status === "ok";
})
.flatMap((source, i) => [
{
id: i,
level: "info",
output: `output from '${source.display_name}'`,
source_id: source.id,
created_at: fixedLogTimestamp,
},
]),
),
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for a non-failed source tab to render, confirming logs streamed in.
await canvas.findByRole("tab", { name: "coder" });
// All Logs must stay active because no failed source has rendered logs.
const allLogsTab = canvas.getByRole("tab", { name: "All Logs" });
await waitFor(() =>
expect(allLogsTab).toHaveAttribute("data-state", "active"),
);
},
};
const NON_STARTUP_SCRIPT_SOURCE_ID = "install-script-source-id";
export const NonStartupScriptError: Story = {
args: {
agent: {
...M.MockWorkspaceAgent,
logs_length: 2,
scripts: [
// Startup Script succeeded.
{
...M.MockWorkspaceAgent.scripts[0],
exit_code: 0,
status: "ok",
},
// A non-startup script failed; that's the tab we should auto-select.
{
...M.MockWorkspaceAgent.scripts[0],
id: "install-script-id",
log_source_id: NON_STARTUP_SCRIPT_SOURCE_ID,
exit_code: 1,
status: "exit_failure",
display_name: "Install Script",
},
],
log_sources: [
...M.MockWorkspaceAgent.log_sources,
{
...M.MockWorkspaceAgent.log_sources[0],
id: NON_STARTUP_SCRIPT_SOURCE_ID,
display_name: "Install Script",
},
],
},
},
parameters: {
webSocket: [
{
event: "message",
data: JSON.stringify([
{
id: 1,
level: "info",
output: "startup ok",
source_id: M.MockWorkspaceAgentLogSource.id,
created_at: fixedLogTimestamp,
},
{
id: 2,
level: "error",
output: "install failed",
source_id: NON_STARTUP_SCRIPT_SOURCE_ID,
created_at: fixedLogTimestamp,
},
]),
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Startup Script is OK; only Install Script failed. The auto-select must
// follow the failure, not the position or display name.
const installScriptTab = await canvas.findByRole("tab", {
name: "Install Script",
});
await waitFor(() =>
expect(installScriptTab).toHaveAttribute("data-state", "active"),
);
},
};
export const ShuttingDown: Story = {
+39 -13
View File
@@ -25,6 +25,7 @@ import type {
Workspace,
WorkspaceAgent,
WorkspaceAgentMetadata,
WorkspaceAgentScript,
} from "#/api/typesGenerated";
import { CheckIcon } from "#/components/AnimatedIcons/Check";
import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown";
@@ -128,6 +129,13 @@ const getAgentBorderClass = (
const STARTUP_SCRIPT_DISPLAY_NAME = "Startup Script";
// A script is considered failed if it exited with a non-zero code, or if its
// status reports a known failure mode (anything other than "ok"). Kept aligned
// with the per-tab error indicator so the auto-selected tab matches the visual
// warning badge.
const isScriptFailed = (script: WorkspaceAgentScript | undefined): boolean =>
Boolean(script?.exit_code || (script?.status && script.status !== "ok"));
export const AgentRow: FC<AgentRowProps> = ({
agent,
subAgents,
@@ -235,14 +243,34 @@ export const AgentRow: FC<AgentRowProps> = ({
agent,
Boolean(hasDevcontainerErrors || shouldShowWildcardWarning),
);
const failedStartupScriptSource = hasAgentIssues
? agent.log_sources.find(
(s) => s.display_name === STARTUP_SCRIPT_DISPLAY_NAME,
)
: undefined;
const [selectedLogTab, setSelectedLogTab] = useState(
failedStartupScriptSource?.id ?? "all",
);
const [selectedLogTab, setSelectedLogTab] = useState("all");
const hasAutoSelectedLogTabRef = useRef(false);
// Auto-select the first log tab whose script failed and has rendered output.
useEffect(() => {
if (hasAutoSelectedLogTabRef.current) {
return;
}
const failedSourceWithLogs = agent.log_sources.find((logSource) => {
const script = agent.scripts.find(
(s) => s.log_source_id === logSource.id,
);
if (!isScriptFailed(script)) {
return false;
}
return agentLogs.some(
(log) =>
log.source_id === logSource.id && (log.output?.length ?? 0) > 0,
);
});
if (failedSourceWithLogs) {
hasAutoSelectedLogTabRef.current = true;
setSelectedLogTab(failedSourceWithLogs.id);
}
}, [agent.log_sources, agent.scripts, agentLogs]);
const handleSelectedLogTabChange = (value: string) => {
hasAutoSelectedLogTabRef.current = true;
setSelectedLogTab(value);
};
const sortedSourceLogTabs = agent.log_sources
.filter((logSource) => {
// Remove the logSources that have no entries.
@@ -269,9 +297,7 @@ export const AgentRow: FC<AgentRowProps> = ({
) : null,
title: logSource.display_name,
value: logSource.id,
error: Boolean(
script?.exit_code || (script?.status && script.status !== "ok"),
),
error: isScriptFailed(script),
};
})
.sort((a, b) => {
@@ -563,7 +589,7 @@ export const AgentRow: FC<AgentRowProps> = ({
<Tabs
className="-mx-px -mt-px"
value={selectedLogTab}
onValueChange={setSelectedLogTab}
onValueChange={handleSelectedLogTabChange}
>
<div className="flex items-stretch">
<div className="min-w-0 flex-1 overflow-hidden">
@@ -621,7 +647,7 @@ export const AgentRow: FC<AgentRowProps> = ({
<DropdownMenuContent align="end">
<DropdownMenuRadioGroup
value={selectedLogTab}
onValueChange={setSelectedLogTab}
onValueChange={handleSelectedLogTabChange}
>
{overflowLogTabs.map((tab) => (
<DropdownMenuRadioItem