Skip to content

Commit 4fd0312

Browse files
authored
feat: use backend-supplied sidebar app id on the /task/$id page (#18458)
Related to #18454. It will close coder/internal#734.
1 parent 64a2214 commit 4fd0312

File tree

5 files changed

+231
-156
lines changed

5 files changed

+231
-156
lines changed

site/src/pages/TaskPage/TaskApps.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { type FC, useState } from "react";
1515
import { Link as RouterLink } from "react-router-dom";
1616
import { cn } from "utils/cn";
1717
import { TaskAppIFrame } from "./TaskAppIframe";
18-
import { AI_APP_CHAT_SLUG } from "./constants";
1918

2019
type TaskAppsProps = {
2120
task: Task;
@@ -30,7 +29,9 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
3029
// it here
3130
const apps = agents
3231
.flatMap((a) => a?.apps)
33-
.filter((a) => !!a && a.slug !== AI_APP_CHAT_SLUG);
32+
.filter(
33+
(a) => !!a && a.id !== task.workspace.latest_build.ai_task_sidebar_app_id,
34+
);
3435

3536
const embeddedApps = apps.filter((app) => !app.external);
3637
const externalApps = apps.filter((app) => app.external);

site/src/pages/TaskPage/TaskPage.stories.tsx

Lines changed: 133 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import { expect, spyOn, within } from "@storybook/test";
3-
import type { Workspace, WorkspaceApp } from "api/typesGenerated";
3+
import type {
4+
Workspace,
5+
WorkspaceApp,
6+
WorkspaceResource,
7+
} from "api/typesGenerated";
48
import {
59
MockFailedWorkspace,
610
MockStartingWorkspace,
@@ -13,11 +17,12 @@ import {
1317
mockApiError,
1418
} from "testHelpers/entities";
1519
import { withProxyProvider } from "testHelpers/storybook";
16-
import TaskPage, { data } from "./TaskPage";
20+
import TaskPage, { data, WorkspaceDoesNotHaveAITaskError } from "./TaskPage";
1721

1822
const meta: Meta<typeof TaskPage> = {
1923
title: "pages/TaskPage",
2024
component: TaskPage,
25+
decorators: [withProxyProvider()],
2126
parameters: {
2227
layout: "fullscreen",
2328
},
@@ -96,61 +101,142 @@ export const TerminatedBuildWithStatus: Story = {
96101
},
97102
};
98103

99-
function activeWorkspace(apps: WorkspaceApp[]): Workspace {
104+
export const SidebarAppDisabled: Story = {
105+
beforeEach: () => {
106+
spyOn(data, "fetchTask").mockResolvedValue({
107+
prompt: "Create competitors page",
108+
workspace: {
109+
...MockWorkspace,
110+
latest_build: {
111+
...MockWorkspace.latest_build,
112+
has_ai_task: true,
113+
ai_task_sidebar_app_id: "claude-code",
114+
resources: mockResources({
115+
claudeCodeAppOverrides: {
116+
health: "disabled",
117+
},
118+
}),
119+
},
120+
},
121+
});
122+
},
123+
};
124+
125+
export const SidebarAppLoading: Story = {
126+
beforeEach: () => {
127+
spyOn(data, "fetchTask").mockResolvedValue({
128+
prompt: "Create competitors page",
129+
workspace: {
130+
...MockWorkspace,
131+
latest_build: {
132+
...MockWorkspace.latest_build,
133+
has_ai_task: true,
134+
ai_task_sidebar_app_id: "claude-code",
135+
resources: mockResources({
136+
claudeCodeAppOverrides: {
137+
health: "initializing",
138+
},
139+
}),
140+
},
141+
},
142+
});
143+
},
144+
};
145+
146+
export const SidebarAppHealthy: Story = {
147+
beforeEach: () => {
148+
spyOn(data, "fetchTask").mockResolvedValue({
149+
prompt: "Create competitors page",
150+
workspace: {
151+
...MockWorkspace,
152+
latest_build: {
153+
...MockWorkspace.latest_build,
154+
has_ai_task: true,
155+
ai_task_sidebar_app_id: "claude-code",
156+
resources: mockResources({
157+
claudeCodeAppOverrides: {
158+
health: "healthy",
159+
},
160+
}),
161+
},
162+
},
163+
});
164+
},
165+
};
166+
167+
export const BuildNoAITask: Story = {
168+
beforeEach: () => {
169+
spyOn(data, "fetchTask").mockImplementation(() => {
170+
throw new WorkspaceDoesNotHaveAITaskError(MockWorkspace);
171+
});
172+
},
173+
};
174+
175+
interface MockResourcesProps {
176+
apps?: WorkspaceApp[];
177+
claudeCodeAppOverrides?: Partial<WorkspaceApp>;
178+
}
179+
180+
const mockResources = (
181+
props?: MockResourcesProps,
182+
): readonly WorkspaceResource[] => [
183+
{
184+
...MockWorkspaceResource,
185+
agents: [
186+
{
187+
...MockWorkspaceAgent,
188+
apps: [
189+
...(props?.apps ?? []),
190+
{
191+
...MockWorkspaceApp,
192+
id: "claude-code",
193+
display_name: "Claude Code",
194+
slug: "claude-code",
195+
icon: "/icon/claude.svg",
196+
statuses: [
197+
MockWorkspaceAppStatus,
198+
{
199+
...MockWorkspaceAppStatus,
200+
id: "2",
201+
message: "Planning changes",
202+
state: "working",
203+
},
204+
],
205+
...(props?.claudeCodeAppOverrides ?? {}),
206+
},
207+
{
208+
...MockWorkspaceApp,
209+
id: "vscode",
210+
slug: "vscode",
211+
display_name: "VS Code Web",
212+
icon: "/icon/code.svg",
213+
},
214+
{
215+
...MockWorkspaceApp,
216+
slug: "zed",
217+
id: "zed",
218+
display_name: "Zed",
219+
icon: "/icon/zed.svg",
220+
},
221+
],
222+
},
223+
],
224+
},
225+
];
226+
227+
const activeWorkspace = (apps: WorkspaceApp[]): Workspace => {
100228
return {
101229
...MockWorkspace,
102230
latest_build: {
103231
...MockWorkspace.latest_build,
104-
resources: [
105-
{
106-
...MockWorkspaceResource,
107-
agents: [
108-
{
109-
...MockWorkspaceAgent,
110-
apps: [
111-
...apps,
112-
{
113-
...MockWorkspaceApp,
114-
id: "claude-code",
115-
display_name: "Claude Code",
116-
slug: "claude-code",
117-
icon: "/icon/claude.svg",
118-
statuses: [
119-
MockWorkspaceAppStatus,
120-
{
121-
...MockWorkspaceAppStatus,
122-
id: "2",
123-
message: "Planning changes",
124-
state: "working",
125-
},
126-
],
127-
},
128-
{
129-
...MockWorkspaceApp,
130-
id: "vscode",
131-
slug: "vscode",
132-
display_name: "VS Code Web",
133-
icon: "/icon/code.svg",
134-
},
135-
{
136-
...MockWorkspaceApp,
137-
slug: "zed",
138-
id: "zed",
139-
display_name: "Zed",
140-
icon: "/icon/zed.svg",
141-
},
142-
],
143-
},
144-
],
145-
},
146-
],
232+
resources: mockResources({ apps }),
147233
},
148234
latest_app_status: {
149235
...MockWorkspaceAppStatus,
150236
app_id: "claude-code",
151237
},
152238
};
153-
}
239+
};
154240

155241
export const Active: Story = {
156242
decorators: [withProxyProvider()],

site/src/pages/TaskPage/TaskPage.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { API } from "api/api";
22
import { getErrorDetail, getErrorMessage } from "api/errors";
3-
import type { WorkspaceStatus } from "api/typesGenerated";
3+
import type { Workspace, WorkspaceStatus } from "api/typesGenerated";
44
import { Button } from "components/Button/Button";
55
import { Loader } from "components/Loader/Loader";
66
import { Margins } from "components/Margins/Margins";
@@ -164,7 +164,7 @@ const TaskPage = () => {
164164
return (
165165
<>
166166
<Helmet>
167-
<title>{pageTitle(ellipsizeText(task.prompt, 64)!)}</title>
167+
<title>{pageTitle(ellipsizeText(task.prompt, 64) ?? "Task")}</title>
168168
</Helmet>
169169

170170
<div className="h-full flex justify-stretch">
@@ -177,22 +177,34 @@ const TaskPage = () => {
177177

178178
export default TaskPage;
179179

180+
export class WorkspaceDoesNotHaveAITaskError extends Error {
181+
constructor(workspace: Workspace) {
182+
super(
183+
`Workspace ${workspace.owner_name}/${workspace.name} is not running an AI task`,
184+
);
185+
this.name = "WorkspaceDoesNotHaveAITaskError";
186+
}
187+
}
188+
180189
export const data = {
181190
fetchTask: async (workspaceOwnerUsername: string, workspaceName: string) => {
182191
const workspace = await API.getWorkspaceByOwnerAndName(
183192
workspaceOwnerUsername,
184193
workspaceName,
185194
);
195+
if (
196+
workspace.latest_build.job.completed_at &&
197+
!workspace.latest_build.has_ai_task
198+
) {
199+
throw new WorkspaceDoesNotHaveAITaskError(workspace);
200+
}
201+
186202
const parameters = await API.getWorkspaceBuildParameters(
187203
workspace.latest_build.id,
188204
);
189-
const prompt = parameters.find(
190-
(p) => p.name === AI_PROMPT_PARAMETER_NAME,
191-
)?.value;
192-
193-
if (!prompt) {
194-
return;
195-
}
205+
const prompt =
206+
parameters.find((p) => p.name === AI_PROMPT_PARAMETER_NAME)?.value ??
207+
"Unknown prompt";
196208

197209
return {
198210
workspace,

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy