diff --git a/docs/ai-coder/create-template.md b/docs/ai-coder/create-template.md index febd626406c82..53e61b7379fbe 100644 --- a/docs/ai-coder/create-template.md +++ b/docs/ai-coder/create-template.md @@ -42,9 +42,19 @@ Follow the instructions in the Coder Registry to install the module. Be sure to enable the `experiment_use_screen` and `experiment_report_tasks` variables to report status back to the Coder control plane. +> [!TIP] +> > Alternatively, you can [use a custom agent](./custom-agents.md) that is > not in our registry via MCP. +The module uses `experiment_report_tasks` to stream changes to the Coder dashboard: + +```hcl +# Enable experimental features +experiment_use_screen = true # Or use experiment_use_tmux = true to use tmux instead +experiment_report_tasks = true +``` + ## 3. Confirm tasks are streaming in the Coder UI The Coder dashboard should now show tasks being reported by the agent. diff --git a/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx b/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx index 5de1f7e4b6bda..816a5ae34e24e 100644 --- a/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx +++ b/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx @@ -1,10 +1,10 @@ import type { Interpolation, Theme } from "@emotion/react"; -import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import Snackbar, { type SnackbarProps as MuiSnackbarProps, } from "@mui/material/Snackbar"; import { type ClassName, useClassName } from "hooks/useClassName"; +import { X as XIcon } from "lucide-react"; import type { FC } from "react"; type EnterpriseSnackbarVariant = "error" | "info" | "success"; @@ -47,7 +47,11 @@ export const EnterpriseSnackbar: FC = ({
{action} - +
} @@ -96,8 +100,6 @@ const styles = { alignItems: "center", }, closeIcon: (theme) => ({ - width: 25, - height: 25, color: theme.palette.primary.contrastText, }), } satisfies Record>; diff --git a/site/src/modules/provisioners/ProvisionerTag.tsx b/site/src/modules/provisioners/ProvisionerTag.tsx index 2436fafad85b9..94467497cfa1b 100644 --- a/site/src/modules/provisioners/ProvisionerTag.tsx +++ b/site/src/modules/provisioners/ProvisionerTag.tsx @@ -1,10 +1,10 @@ import type { Interpolation, Theme } from "@emotion/react"; -import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"; import CloseIcon from "@mui/icons-material/Close"; -import DoNotDisturbOnOutlined from "@mui/icons-material/DoNotDisturbOnOutlined"; -import Sell from "@mui/icons-material/Sell"; import IconButton from "@mui/material/IconButton"; import { Pill } from "components/Pill/Pill"; +import { CircleCheck as CircleCheckIcon } from "lucide-react"; +import { CircleMinus as CircleMinusIcon } from "lucide-react"; +import { Tag as TagIcon } from "lucide-react"; import type { ComponentProps, FC } from "react"; const parseBool = (s: string): { valid: boolean; value: boolean } => { @@ -62,7 +62,11 @@ export const ProvisionerTag: FC = ({ return {content}; } return ( - } data-testid={`tag-${tagName}`}> + } + data-testid={`tag-${tagName}`} + > {content} ); @@ -83,9 +87,9 @@ const BooleanPill: FC = ({ size="lg" icon={ value ? ( - + ) : ( - + ) } {...divProps} diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 3921d5266ef2d..e2670eed65b02 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -1,5 +1,4 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import CloseIcon from "@mui/icons-material/Close"; import LockIcon from "@mui/icons-material/Lock"; import LockOpenIcon from "@mui/icons-material/LockOpen"; import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; @@ -41,6 +40,7 @@ import { } from "components/deprecated/Popover/Popover"; import { type FormikContextType, useFormik } from "formik"; import { type ClassName, useClassName } from "hooks/useClassName"; +import { X as XIcon } from "lucide-react"; import { ChevronDownIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, useState } from "react"; @@ -497,7 +497,7 @@ export const PortForwardPopoverView: FC = ({ await sharedPortsQuery.refetch(); }} > - = ({

Creating template...

- + Close build logs
diff --git a/site/src/pages/HealthPage/Content.tsx b/site/src/pages/HealthPage/Content.tsx index dcc645e1c08e8..2bd5e96f2450e 100644 --- a/site/src/pages/HealthPage/Content.tsx +++ b/site/src/pages/HealthPage/Content.tsx @@ -1,10 +1,10 @@ import { css } from "@emotion/css"; import { useTheme } from "@emotion/react"; -import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"; -import DoNotDisturbOnOutlined from "@mui/icons-material/DoNotDisturbOnOutlined"; import ErrorOutline from "@mui/icons-material/ErrorOutline"; import Link from "@mui/material/Link"; import type { HealthCode, HealthSeverity } from "api/typesGenerated"; +import { CircleCheck as CircleCheckIcon } from "lucide-react"; +import { CircleMinus as CircleMinusIcon } from "lucide-react"; import { type ComponentProps, type FC, @@ -57,7 +57,7 @@ interface HealthIconProps { export const HealthIcon: FC = ({ size, severity }) => { const theme = useTheme(); const color = healthyColor(theme, severity); - const Icon = severity === "error" ? ErrorOutline : CheckCircleOutlined; + const Icon = severity === "error" ? ErrorOutline : CircleCheckIcon; return ; }; @@ -201,9 +201,9 @@ export const BooleanPill: FC = ({ + ) : ( - + ) } {...divProps} diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 325b86a70cf37..8aa1ac57a5403 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -1,6 +1,5 @@ import { useTheme } from "@emotion/react"; import CancelOutlined from "@mui/icons-material/CancelOutlined"; -import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"; import LinkOutlined from "@mui/icons-material/LinkOutlined"; import LinearProgress from "@mui/material/LinearProgress"; import Link from "@mui/material/Link"; @@ -45,6 +44,7 @@ import { subDays, } from "date-fns"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; +import { CircleCheck as CircleCheckIcon } from "lucide-react"; import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout"; import { type FC, @@ -759,12 +759,11 @@ const ParameterUsageLabel: FC = ({ ) : ( <> - True diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx index a7278b3bfc9ce..307e5386092d6 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx @@ -1,5 +1,4 @@ import { useTheme } from "@emotion/react"; -import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"; import GitHubIcon from "@mui/icons-material/GitHub"; import KeyIcon from "@mui/icons-material/VpnKey"; import Button from "@mui/material/Button"; @@ -16,6 +15,7 @@ import type { import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Stack } from "components/Stack/Stack"; +import { CircleCheck as CircleCheckIcon } from "lucide-react"; import { type FC, useState } from "react"; import { useMutation } from "react-query"; import { docs } from "utils/docs"; @@ -191,11 +191,11 @@ export const SingleSignOnSection: FC = ({ fontSize: 14, }} > - Authenticated with{" "} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index f9dc50d0cbff3..dc1b9c2f4fb82 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -23,6 +23,7 @@ import { MockTemplate, MockUserOwner, MockWorkspace, + MockWorkspaceAgent, MockWorkspaceAppStatus, mockApiError, } from "testHelpers/entities"; @@ -299,6 +300,42 @@ export const InvalidPageNumber: Story = { }, }; +export const MultipleApps: Story = { + args: { + workspaces: [ + { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [ + { + ...MockWorkspaceAgent, + apps: [ + { + ...MockWorkspaceAgent.apps[0], + display_name: "App 1", + id: "app-1", + }, + { + ...MockWorkspaceAgent.apps[0], + display_name: "App 2", + id: "app-2", + }, + ], + }, + ], + }, + ], + }, + }, + ], + count: allWorkspaces.length, + }, +}; + export const ShowOrganizations: Story = { args: { workspaces: [{ ...MockWorkspace, organization_name: "limbus-co" }], diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index f749316369b26..4ec1156b7fcd5 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -19,6 +19,7 @@ import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton"; import { Button } from "components/Button/Button"; +import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { VSCodeIcon } from "components/Icons/VSCodeIcon"; import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon"; import { InfoTooltip } from "components/InfoTooltip/InfoTooltip"; @@ -63,6 +64,7 @@ import { getVSCodeHref, openAppInNewWindow, } from "modules/apps/apps"; +import { useAppLink } from "modules/apps/useAppLink"; import { useDashboard } from "modules/dashboard/useDashboard"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge"; @@ -622,6 +624,9 @@ const PrimaryAction: FC = ({ ); }; +// The total number of apps that can be displayed in the workspace row +const WORKSPACE_APPS_SLOTS = 4; + type WorkspaceAppsProps = { workspace: Workspace; }; @@ -647,11 +652,18 @@ const WorkspaceApps: FC = ({ workspace }) => { return null; } + const builtinApps = new Set(agent.display_apps); + builtinApps.delete("port_forwarding_helper"); + builtinApps.delete("ssh_helper"); + + const remainingSlots = WORKSPACE_APPS_SLOTS - builtinApps.size; + const userApps = agent.apps.slice(0, remainingSlots); + const buttons: ReactNode[] = []; - if (agent.display_apps.includes("vscode")) { + if (builtinApps.has("vscode")) { buttons.push( - = ({ workspace }) => { })} > - , + , ); } - if (agent.display_apps.includes("vscode_insiders")) { + if (builtinApps.has("vscode_insiders")) { buttons.push( - = ({ workspace }) => { })} > - , + , ); } - if (agent.display_apps.includes("web_terminal")) { + for (const app of userApps) { + buttons.push( + , + ); + } + + if (builtinApps.has("web_terminal")) { const href = getTerminalHref({ username: workspace.owner_name, workspace: workspace.name, agent: agent.name, }); buttons.push( - { @@ -704,21 +727,45 @@ const WorkspaceApps: FC = ({ workspace }) => { label="Open Terminal" > - , + , ); } return buttons; }; -type AppLinkProps = PropsWithChildren<{ +type IconAppLinkProps = { + app: WorkspaceApp; + workspace: Workspace; + agent: WorkspaceAgent; +}; + +const IconAppLink: FC = ({ app, workspace, agent }) => { + const link = useAppLink(app, { + workspace, + agent, + }); + + return ( + + + + ); +}; + +type BaseIconLinkProps = PropsWithChildren<{ label: string; href: string; isLoading?: boolean; onClick?: (e: React.MouseEvent) => void; }>; -const AppLink: FC = ({ +const BaseIconLink: FC = ({ href, isLoading, label, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 084bb78345d8f..a8db5f55879c4 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -896,17 +896,10 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = { id: "test-app", slug: "test-app", display_name: "Test App", - icon: "", subdomain: false, health: "disabled", external: false, - url: "", sharing_level: "owner", - healthcheck: { - url: "", - interval: 0, - threshold: 0, - }, hidden: false, open_in: "slim-window", statuses: [],