diff --git a/site/src/components/Icons/VSCodeIcon.stories.tsx b/site/src/components/Icons/VSCodeIcon.stories.tsx
new file mode 100644
index 0000000000000..3656005303789
--- /dev/null
+++ b/site/src/components/Icons/VSCodeIcon.stories.tsx
@@ -0,0 +1,12 @@
+import { Story } from "@storybook/react"
+import { VSCodeIcon } from "./VSCodeIcon"
+
+export default {
+ title: "icons/VSCodeIcon",
+ component: VSCodeIcon,
+}
+
+const Template: Story = (args) =>
+
+export const Example = Template.bind({})
+Example.args = {}
diff --git a/site/src/components/Icons/VSCodeIcon.tsx b/site/src/components/Icons/VSCodeIcon.tsx
new file mode 100644
index 0000000000000..7231dab0e2c22
--- /dev/null
+++ b/site/src/components/Icons/VSCodeIcon.tsx
@@ -0,0 +1,134 @@
+import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
+
+export const VSCodeIcon: typeof SvgIcon = (props: SvgIconProps) => (
+
+
+
+)
diff --git a/site/src/components/Resources/AgentRow.stories.tsx b/site/src/components/Resources/AgentRow.stories.tsx
index a289c3d1bf19f..7a74352db92fc 100644
--- a/site/src/components/Resources/AgentRow.stories.tsx
+++ b/site/src/components/Resources/AgentRow.stories.tsx
@@ -32,6 +32,15 @@ HideSSHButton.args = {
hideSSHButton: true,
}
+export const HideVSCodeDesktopButton = Template.bind({})
+HideVSCodeDesktopButton.args = {
+ agent: MockWorkspaceAgent,
+ workspace: MockWorkspace,
+ applicationsHost: "",
+ showApps: true,
+ hideVSCodeDesktopButton: true,
+}
+
export const NotShowingApps = Template.bind({})
NotShowingApps.args = {
agent: MockWorkspaceAgent,
diff --git a/site/src/components/Resources/AgentRow.tsx b/site/src/components/Resources/AgentRow.tsx
index 553761233b39c..c6174f7f56807 100644
--- a/site/src/components/Resources/AgentRow.tsx
+++ b/site/src/components/Resources/AgentRow.tsx
@@ -13,6 +13,7 @@ import { Maybe } from "components/Conditionals/Maybe"
import { AgentStatus } from "./AgentStatus"
import { AppLinkSkeleton } from "components/AppLink/AppLinkSkeleton"
import { useTranslation } from "react-i18next"
+import { VSCodeDesktopButton } from "components/VSCodeDesktopButton/VSCodeDesktopButton"
export interface AgentRowProps {
agent: WorkspaceAgent
@@ -20,6 +21,7 @@ export interface AgentRowProps {
applicationsHost: string | undefined
showApps: boolean
hideSSHButton?: boolean
+ hideVSCodeDesktopButton?: boolean
serverVersion: string
}
@@ -29,6 +31,7 @@ export const AgentRow: FC = ({
applicationsHost,
showApps,
hideSSHButton,
+ hideVSCodeDesktopButton,
serverVersion,
}) => {
const styles = useStyles()
@@ -105,6 +108,13 @@ export const AgentRow: FC = ({
agentName={agent.name}
/>
)}
+ {!hideVSCodeDesktopButton && (
+
+ )}
{applicationsHost !== undefined && applicationsHost !== "" && (
= (args) => (
+
+)
+
+export const Default = Template.bind({})
+Default.args = {
+ userName: MockWorkspace.owner_name,
+ workspaceName: MockWorkspace.name,
+ agentName: MockWorkspaceAgent.name,
+}
diff --git a/site/src/components/VSCodeDesktopButton/VSCodeDesktopButton.tsx b/site/src/components/VSCodeDesktopButton/VSCodeDesktopButton.tsx
new file mode 100644
index 0000000000000..b7d9e9f70b618
--- /dev/null
+++ b/site/src/components/VSCodeDesktopButton/VSCodeDesktopButton.tsx
@@ -0,0 +1,52 @@
+import Button from "@material-ui/core/Button"
+import { getApiKey } from "api/api"
+import { VSCodeIcon } from "components/Icons/VSCodeIcon"
+import { FC, PropsWithChildren, useState } from "react"
+
+export interface VSCodeDesktopButtonProps {
+ userName: string
+ workspaceName: string
+ agentName?: string
+}
+
+export const VSCodeDesktopButton: FC<
+ PropsWithChildren
+> = ({ userName, workspaceName, agentName }) => {
+ const [loading, setLoading] = useState(false)
+
+ return (
+ }
+ size="small"
+ disabled={loading}
+ onClick={() => {
+ setLoading(true)
+ getApiKey()
+ .then(({ key }) => {
+ const query = new URLSearchParams({
+ owner: userName,
+ workspace: workspaceName,
+ url: location.origin,
+ token: key,
+ })
+ if (agentName) {
+ query.set("agent", agentName)
+ }
+
+ window.open(
+ `vscode://coder.coder-remote/open?${query.toString()}`,
+ "_blank",
+ )
+ })
+ .catch((ex) => {
+ console.error(ex)
+ })
+ .finally(() => {
+ setLoading(false)
+ })
+ }}
+ >
+ VS Code Desktop
+
+ )
+}
diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx
index d9f3728976ab4..c1405e4bd079a 100644
--- a/site/src/components/Workspace/Workspace.tsx
+++ b/site/src/components/Workspace/Workspace.tsx
@@ -50,6 +50,7 @@ export interface WorkspaceProps {
builds?: TypesGen.WorkspaceBuild[]
canUpdateWorkspace: boolean
hideSSHButton?: boolean
+ hideVSCodeDesktopButton?: boolean
workspaceErrors: Partial>
buildInfo?: TypesGen.BuildInfoResponse
applicationsHost?: string
@@ -75,6 +76,7 @@ export const Workspace: FC> = ({
canUpdateWorkspace,
workspaceErrors,
hideSSHButton,
+ hideVSCodeDesktopButton,
buildInfo,
applicationsHost,
template,
@@ -215,6 +217,7 @@ export const Workspace: FC> = ({
applicationsHost={applicationsHost}
showApps={canUpdateWorkspace}
hideSSHButton={hideSSHButton}
+ hideVSCodeDesktopButton={hideVSCodeDesktopButton}
serverVersion={serverVersion}
/>
)}
diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
index 968b74cd14ebc..7f801b17e8df8 100644
--- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
@@ -42,6 +42,10 @@ export const WorkspaceReadyPage = ({
workspaceState.children["scheduleBannerMachine"],
)
const xServices = useContext(XServiceContext)
+ const experimental = useSelector(
+ xServices.entitlementsXService,
+ (state) => state.context.entitlements.experimental,
+ )
const featureVisibility = useSelector(
xServices.entitlementsXService,
selectFeatureVisibility,
@@ -120,6 +124,9 @@ export const WorkspaceReadyPage = ({
builds={builds}
canUpdateWorkspace={canUpdateWorkspace}
hideSSHButton={featureVisibility[FeatureNames.BrowserOnly]}
+ hideVSCodeDesktopButton={
+ !experimental || featureVisibility[FeatureNames.BrowserOnly]
+ }
workspaceErrors={{
[WorkspaceErrors.GET_RESOURCES_ERROR]: refreshWorkspaceWarning,
[WorkspaceErrors.GET_BUILDS_ERROR]: getBuildsError,