From a0be6c223ceac46423e0686f9755f6e39a8684e7 Mon Sep 17 00:00:00 2001
From: Kacper Sawicki
Date: Mon, 11 Aug 2025 13:19:49 +0000
Subject: [PATCH 1/7] feat: add support for external agents in the UI and
extend CodeExample
---
site/src/api/api.ts | 10 +++
.../CodeExample/CodeExample.stories.tsx | 9 +++
.../components/CodeExample/CodeExample.tsx | 65 +++++++++++++++--
.../resources/AgentExternal.stories.tsx | 72 +++++++++++++++++++
site/src/modules/resources/AgentExternal.tsx | 51 +++++++++++++
site/src/modules/resources/AgentRow.tsx | 15 +++-
site/src/modules/workspaces/actions.ts | 8 +++
7 files changed, 221 insertions(+), 9 deletions(-)
create mode 100644 site/src/modules/resources/AgentExternal.stories.tsx
create mode 100644 site/src/modules/resources/AgentExternal.tsx
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index ea97a5b46a2ef..966c8902c3e73 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -2022,6 +2022,16 @@ class ApiMethods {
return response.data;
};
+ getWorkspaceAgentCredentials = async (
+ workspaceID: string,
+ agentName: string,
+ ): Promise => {
+ const response = await this.axios.get(
+ `/api/v2/workspaces/${workspaceID}/external-agent/${agentName}/credentials`,
+ );
+ return response.data;
+ };
+
upsertWorkspaceAgentSharedPort = async (
workspaceID: string,
req: TypesGen.UpsertWorkspaceAgentPortShareRequest,
diff --git a/site/src/components/CodeExample/CodeExample.stories.tsx b/site/src/components/CodeExample/CodeExample.stories.tsx
index 0213762fd31e2..61f129f448a73 100644
--- a/site/src/components/CodeExample/CodeExample.stories.tsx
+++ b/site/src/components/CodeExample/CodeExample.stories.tsx
@@ -31,3 +31,12 @@ export const LongCode: Story = {
code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L",
},
};
+
+export const Redact: Story = {
+ args: {
+ secret: false,
+ redactPattern: /CODER_AGENT_TOKEN="([^"]+)"/g,
+ redactReplacement: `CODER_AGENT_TOKEN="********"`,
+ showRevealButton: true,
+ },
+};
diff --git a/site/src/components/CodeExample/CodeExample.tsx b/site/src/components/CodeExample/CodeExample.tsx
index 474dcb1fac225..b69a220550958 100644
--- a/site/src/components/CodeExample/CodeExample.tsx
+++ b/site/src/components/CodeExample/CodeExample.tsx
@@ -1,11 +1,26 @@
import type { Interpolation, Theme } from "@emotion/react";
-import type { FC } from "react";
+import { Button } from "components/Button/Button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "components/Tooltip/Tooltip";
+import { EyeIcon, EyeOffIcon } from "lucide-react";
+import { type FC, useState } from "react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { CopyButton } from "../CopyButton/CopyButton";
interface CodeExampleProps {
code: string;
+ /** Defaulting to true to be on the safe side; you should have to opt out of the secure option, not remember to opt in */
secret?: boolean;
+ /** Redact parts of the code if the user doesn't want to obfuscate the whole code */
+ redactPattern?: RegExp;
+ /** Replacement text for redacted content */
+ redactReplacement?: string;
+ /** Show a button to reveal the redacted parts of the code */
+ showRevealButton?: boolean;
className?: string;
}
@@ -15,11 +30,28 @@ interface CodeExampleProps {
export const CodeExample: FC = ({
code,
className,
-
- // Defaulting to true to be on the safe side; you should have to opt out of
- // the secure option, not remember to opt in
secret = true,
+ redactPattern,
+ redactReplacement = "********",
+ showRevealButton,
}) => {
+ const [showFullValue, setShowFullValue] = useState(false);
+
+ const displayValue = secret
+ ? obfuscateText(code)
+ : redactPattern && !showFullValue
+ ? code.replace(redactPattern, redactReplacement)
+ : code;
+
+ const showButtonLabel = showFullValue
+ ? "Hide sensitive data"
+ : "Show sensitive data";
+ const icon = showFullValue ? (
+
+ ) : (
+
+ );
+
return (
@@ -33,17 +65,36 @@ export const CodeExample: FC = ({
* 2. Even with it turned on and supported, the plaintext is still
* readily available in the HTML itself
*/}
- {obfuscateText(code)}
+ {displayValue}
Encrypted text. Please access via the copy button.
>
) : (
- code
+ displayValue
)}
-
+
+ {showRevealButton && redactPattern && !secret && (
+
+
+
+ setShowFullValue(!showFullValue)}
+ >
+ {icon}
+ {showButtonLabel}
+
+
+ {showButtonLabel}
+
+
+ )}
+
+
);
};
diff --git a/site/src/modules/resources/AgentExternal.stories.tsx b/site/src/modules/resources/AgentExternal.stories.tsx
new file mode 100644
index 0000000000000..ed49a10efeee6
--- /dev/null
+++ b/site/src/modules/resources/AgentExternal.stories.tsx
@@ -0,0 +1,72 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { chromatic } from "testHelpers/chromatic";
+import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
+import { withDashboardProvider } from "testHelpers/storybook";
+import { AgentExternal } from "./AgentExternal";
+
+const meta: Meta = {
+ title: "modules/resources/AgentExternal",
+ component: AgentExternal,
+ args: {
+ isExternalAgent: true,
+ agent: {
+ ...MockWorkspaceAgent,
+ status: "connecting",
+ operating_system: "linux",
+ architecture: "amd64",
+ },
+ workspace: MockWorkspace,
+ },
+ decorators: [withDashboardProvider],
+ parameters: {
+ chromatic,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Connecting: Story = {
+ args: {
+ agent: {
+ ...MockWorkspaceAgent,
+ status: "connecting",
+ operating_system: "linux",
+ architecture: "amd64",
+ },
+ },
+};
+
+export const Timeout: Story = {
+ args: {
+ agent: {
+ ...MockWorkspaceAgent,
+ status: "timeout",
+ operating_system: "linux",
+ architecture: "amd64",
+ },
+ },
+};
+
+export const DifferentOS: Story = {
+ args: {
+ agent: {
+ ...MockWorkspaceAgent,
+ status: "connecting",
+ operating_system: "darwin",
+ architecture: "arm64",
+ },
+ },
+};
+
+export const NotExternalAgent: Story = {
+ args: {
+ isExternalAgent: false,
+ agent: {
+ ...MockWorkspaceAgent,
+ status: "connecting",
+ operating_system: "linux",
+ architecture: "amd64",
+ },
+ },
+};
diff --git a/site/src/modules/resources/AgentExternal.tsx b/site/src/modules/resources/AgentExternal.tsx
new file mode 100644
index 0000000000000..64fea9e23ed12
--- /dev/null
+++ b/site/src/modules/resources/AgentExternal.tsx
@@ -0,0 +1,51 @@
+import { API } from "api/api";
+import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
+import isChromatic from "chromatic/isChromatic";
+import { CodeExample } from "components/CodeExample/CodeExample";
+import { type FC, useEffect, useState } from "react";
+
+interface AgentExternalProps {
+ isExternalAgent: boolean;
+ agent: WorkspaceAgent;
+ workspace: Workspace;
+}
+
+export const AgentExternal: FC = ({
+ isExternalAgent,
+ agent,
+ workspace,
+}) => {
+ const [externalAgentToken, setExternalAgentToken] = useState(
+ null,
+ );
+ const [command, setCommand] = useState(null);
+
+ const origin = isChromatic() ? "https://example.com" : window.location.origin;
+ useEffect(() => {
+ if (
+ isExternalAgent &&
+ (agent.status === "timeout" || agent.status === "connecting")
+ ) {
+ API.getWorkspaceAgentCredentials(workspace.id, agent.name).then((res) => {
+ setExternalAgentToken(res.agent_token);
+ setCommand(res.command);
+ });
+ }
+ }, [isExternalAgent, agent.status, workspace.id, agent.name]);
+
+ return (
+
+
+ Please run the following command to attach an agent to the{" "}
+ {workspace.name} workspace:
+
+
+
+ );
+};
diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx
index 495dd01f123f2..2c6f55d5920cc 100644
--- a/site/src/modules/resources/AgentRow.tsx
+++ b/site/src/modules/resources/AgentRow.tsx
@@ -27,6 +27,7 @@ import AutoSizer from "react-virtualized-auto-sizer";
import type { FixedSizeList as List, ListOnScrollProps } from "react-window";
import { AgentApps, organizeAgentApps } from "./AgentApps/AgentApps";
import { AgentDevcontainerCard } from "./AgentDevcontainerCard";
+import { AgentExternal } from "./AgentExternal";
import { AgentLatency } from "./AgentLatency";
import { AGENT_LOG_LINE_HEIGHT } from "./AgentLogs/AgentLogLine";
import { AgentLogs } from "./AgentLogs/AgentLogs";
@@ -62,9 +63,10 @@ export const AgentRow: FC = ({
const appSections = organizeAgentApps(agent.apps);
const hasAppsToDisplay =
!browser_only || appSections.some((it) => it.apps.length > 0);
+ const isExternalAgent = workspace.latest_build.has_external_agent;
const shouldDisplayAgentApps =
(agent.status === "connected" && hasAppsToDisplay) ||
- agent.status === "connecting";
+ (agent.status === "connecting" && !isExternalAgent);
const hasVSCodeApp =
agent.display_apps.includes("vscode") ||
agent.display_apps.includes("vscode_insiders");
@@ -258,7 +260,7 @@ export const AgentRow: FC = ({
)}
- {agent.status === "connecting" && (
+ {agent.status === "connecting" && !isExternalAgent && (
)}
+ {isExternalAgent &&
+ (agent.status === "timeout" || agent.status === "connecting") && (
+
+ )}
+
diff --git a/site/src/modules/workspaces/actions.ts b/site/src/modules/workspaces/actions.ts
index 8b17d3e937c74..533cf981ed6d8 100644
--- a/site/src/modules/workspaces/actions.ts
+++ b/site/src/modules/workspaces/actions.ts
@@ -63,6 +63,14 @@ export const abilitiesByWorkspaceStatus = (
};
}
+ if (workspace.latest_build.has_external_agent) {
+ return {
+ actions: [],
+ canCancel: false,
+ canAcceptJobs: true,
+ };
+ }
+
const status = workspace.latest_build.status;
switch (status) {
From ceec966e60841666fc761f211b7f677fff371f78 Mon Sep 17 00:00:00 2001
From: Kacper Sawicki
Date: Mon, 11 Aug 2025 15:01:29 +0000
Subject: [PATCH 2/7] hide AgentExternal if feature is disabled
---
site/src/modules/resources/AgentRow.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx
index 2c6f55d5920cc..e806bdba90b66 100644
--- a/site/src/modules/resources/AgentRow.tsx
+++ b/site/src/modules/resources/AgentRow.tsx
@@ -59,7 +59,7 @@ export const AgentRow: FC = ({
onUpdateAgent,
initialMetadata,
}) => {
- const { browser_only } = useFeatureVisibility();
+ const { browser_only, workspace_external_agent } = useFeatureVisibility();
const appSections = organizeAgentApps(agent.apps);
const hasAppsToDisplay =
!browser_only || appSections.some((it) => it.apps.length > 0);
@@ -296,7 +296,8 @@ export const AgentRow: FC = ({
)}
{isExternalAgent &&
- (agent.status === "timeout" || agent.status === "connecting") && (
+ (agent.status === "timeout" || agent.status === "connecting") &&
+ workspace_external_agent && (
Date: Mon, 11 Aug 2025 15:18:13 +0000
Subject: [PATCH 3/7] Fix AgentExternal story
---
site/src/modules/resources/AgentExternal.stories.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/site/src/modules/resources/AgentExternal.stories.tsx b/site/src/modules/resources/AgentExternal.stories.tsx
index ed49a10efeee6..2ef486e0dc33b 100644
--- a/site/src/modules/resources/AgentExternal.stories.tsx
+++ b/site/src/modules/resources/AgentExternal.stories.tsx
@@ -1,4 +1,4 @@
-import type { Meta, StoryObj } from "@storybook/react";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import { chromatic } from "testHelpers/chromatic";
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
import { withDashboardProvider } from "testHelpers/storybook";
From f7ac4c04b72e83a0a8a81fbe04593956605d152d Mon Sep 17 00:00:00 2001
From: Kacper Sawicki
Date: Wed, 13 Aug 2025 12:13:01 +0000
Subject: [PATCH 4/7] Handle errors when fetching external agent credentials in
AgentExternal component
---
site/src/modules/resources/AgentExternal.tsx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/site/src/modules/resources/AgentExternal.tsx b/site/src/modules/resources/AgentExternal.tsx
index 64fea9e23ed12..1f5f89a1fa75e 100644
--- a/site/src/modules/resources/AgentExternal.tsx
+++ b/site/src/modules/resources/AgentExternal.tsx
@@ -1,7 +1,9 @@
import { API } from "api/api";
+import { getErrorMessage } from "api/errors";
import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
import isChromatic from "chromatic/isChromatic";
import { CodeExample } from "components/CodeExample/CodeExample";
+import { displayError } from "components/GlobalSnackbar/utils";
import { type FC, useEffect, useState } from "react";
interface AgentExternalProps {
@@ -29,6 +31,8 @@ export const AgentExternal: FC = ({
API.getWorkspaceAgentCredentials(workspace.id, agent.name).then((res) => {
setExternalAgentToken(res.agent_token);
setCommand(res.command);
+ }).catch((err) => {
+ displayError(getErrorMessage(err, "Failed to get external agent credentials"));
});
}
}, [isExternalAgent, agent.status, workspace.id, agent.name]);
From 73049f3e6ec19b83e68f50fac1e07a6e534e6565 Mon Sep 17 00:00:00 2001
From: Kacper Sawicki
Date: Wed, 13 Aug 2025 13:35:53 +0000
Subject: [PATCH 5/7] Fix lint
---
site/src/modules/resources/AgentExternal.tsx | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/site/src/modules/resources/AgentExternal.tsx b/site/src/modules/resources/AgentExternal.tsx
index 1f5f89a1fa75e..a1f7c4a768669 100644
--- a/site/src/modules/resources/AgentExternal.tsx
+++ b/site/src/modules/resources/AgentExternal.tsx
@@ -28,12 +28,16 @@ export const AgentExternal: FC = ({
isExternalAgent &&
(agent.status === "timeout" || agent.status === "connecting")
) {
- API.getWorkspaceAgentCredentials(workspace.id, agent.name).then((res) => {
- setExternalAgentToken(res.agent_token);
- setCommand(res.command);
- }).catch((err) => {
- displayError(getErrorMessage(err, "Failed to get external agent credentials"));
- });
+ API.getWorkspaceAgentCredentials(workspace.id, agent.name)
+ .then((res) => {
+ setExternalAgentToken(res.agent_token);
+ setCommand(res.command);
+ })
+ .catch((err) => {
+ displayError(
+ getErrorMessage(err, "Failed to get external agent credentials"),
+ );
+ });
}
}, [isExternalAgent, agent.status, workspace.id, agent.name]);
From 29e2625f47f34f9f50b4426876b9f10db5ce5aa3 Mon Sep 17 00:00:00 2001
From: Kacper Sawicki
Date: Mon, 18 Aug 2025 12:16:10 +0000
Subject: [PATCH 6/7] use useQuery to get external agent credentials
---
site/src/api/queries/workspaces.ts | 10 ++++
site/src/modules/resources/AgentExternal.tsx | 51 +++++++++-----------
2 files changed, 33 insertions(+), 28 deletions(-)
diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts
index 536925a97390f..bcfb07b75452b 100644
--- a/site/src/api/queries/workspaces.ts
+++ b/site/src/api/queries/workspaces.ts
@@ -430,3 +430,13 @@ export const updateWorkspaceACL = (workspaceId: string) => {
},
};
};
+
+export const workspaceAgentCredentials = (
+ workspaceId: string,
+ agentName: string,
+) => {
+ return {
+ queryKey: ["workspaces", workspaceId, "agents", agentName, "credentials"],
+ queryFn: () => API.getWorkspaceAgentCredentials(workspaceId, agentName),
+ };
+};
diff --git a/site/src/modules/resources/AgentExternal.tsx b/site/src/modules/resources/AgentExternal.tsx
index a1f7c4a768669..61d1cfa02c4cd 100644
--- a/site/src/modules/resources/AgentExternal.tsx
+++ b/site/src/modules/resources/AgentExternal.tsx
@@ -1,10 +1,10 @@
-import { API } from "api/api";
-import { getErrorMessage } from "api/errors";
+import { workspaceAgentCredentials } from "api/queries/workspaces";
import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
-import isChromatic from "chromatic/isChromatic";
+import { ErrorAlert } from "components/Alert/ErrorAlert";
import { CodeExample } from "components/CodeExample/CodeExample";
-import { displayError } from "components/GlobalSnackbar/utils";
-import { type FC, useEffect, useState } from "react";
+import { Loader } from "components/Loader/Loader";
+import type { FC } from "react";
+import { useQuery } from "react-query";
interface AgentExternalProps {
isExternalAgent: boolean;
@@ -17,29 +17,24 @@ export const AgentExternal: FC = ({
agent,
workspace,
}) => {
- const [externalAgentToken, setExternalAgentToken] = useState(
- null,
- );
- const [command, setCommand] = useState(null);
+ if (!isExternalAgent) {
+ return null;
+ }
+
+ const {
+ data: credentials,
+ error,
+ isLoading,
+ isError,
+ } = useQuery(workspaceAgentCredentials(workspace.id, agent.name));
+
+ if (isLoading) {
+ return ;
+ }
- const origin = isChromatic() ? "https://example.com" : window.location.origin;
- useEffect(() => {
- if (
- isExternalAgent &&
- (agent.status === "timeout" || agent.status === "connecting")
- ) {
- API.getWorkspaceAgentCredentials(workspace.id, agent.name)
- .then((res) => {
- setExternalAgentToken(res.agent_token);
- setCommand(res.command);
- })
- .catch((err) => {
- displayError(
- getErrorMessage(err, "Failed to get external agent credentials"),
- );
- });
- }
- }, [isExternalAgent, agent.status, workspace.id, agent.name]);
+ if (isError) {
+ return ;
+ }
return (
@@ -48,7 +43,7 @@ export const AgentExternal: FC = ({
{workspace.name} workspace:
Date: Mon, 18 Aug 2025 13:29:52 +0000
Subject: [PATCH 7/7] Fix lint
---
site/src/modules/resources/AgentExternal.stories.tsx | 4 +---
site/src/modules/resources/AgentExternal.tsx | 11 +----------
site/src/modules/resources/AgentRow.tsx | 6 +-----
3 files changed, 3 insertions(+), 18 deletions(-)
diff --git a/site/src/modules/resources/AgentExternal.stories.tsx b/site/src/modules/resources/AgentExternal.stories.tsx
index 2ef486e0dc33b..f83095dd1b570 100644
--- a/site/src/modules/resources/AgentExternal.stories.tsx
+++ b/site/src/modules/resources/AgentExternal.stories.tsx
@@ -1,14 +1,13 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
import { chromatic } from "testHelpers/chromatic";
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
import { withDashboardProvider } from "testHelpers/storybook";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import { AgentExternal } from "./AgentExternal";
const meta: Meta = {
title: "modules/resources/AgentExternal",
component: AgentExternal,
args: {
- isExternalAgent: true,
agent: {
...MockWorkspaceAgent,
status: "connecting",
@@ -61,7 +60,6 @@ export const DifferentOS: Story = {
export const NotExternalAgent: Story = {
args: {
- isExternalAgent: false,
agent: {
...MockWorkspaceAgent,
status: "connecting",
diff --git a/site/src/modules/resources/AgentExternal.tsx b/site/src/modules/resources/AgentExternal.tsx
index 61d1cfa02c4cd..aa100335fc54a 100644
--- a/site/src/modules/resources/AgentExternal.tsx
+++ b/site/src/modules/resources/AgentExternal.tsx
@@ -7,20 +7,11 @@ import type { FC } from "react";
import { useQuery } from "react-query";
interface AgentExternalProps {
- isExternalAgent: boolean;
agent: WorkspaceAgent;
workspace: Workspace;
}
-export const AgentExternal: FC = ({
- isExternalAgent,
- agent,
- workspace,
-}) => {
- if (!isExternalAgent) {
- return null;
- }
-
+export const AgentExternal: FC = ({ agent, workspace }) => {
const {
data: credentials,
error,
diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx
index e806bdba90b66..6dd52bb31b5bb 100644
--- a/site/src/modules/resources/AgentRow.tsx
+++ b/site/src/modules/resources/AgentRow.tsx
@@ -298,11 +298,7 @@ export const AgentRow: FC = ({
{isExternalAgent &&
(agent.status === "timeout" || agent.status === "connecting") &&
workspace_external_agent && (
-
+
)}
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