Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(site): add support for external agents in the UI and extend CodeExample#19288

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
kacpersaw merged 7 commits intomainfromkacpersaw/feat-coder-attach-ui
Aug 19, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletionssite/src/api/api.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2022,6 +2022,16 @@ class ApiMethods {
return response.data;
};

getWorkspaceAgentCredentials = async (
workspaceID: string,
agentName: string,
): Promise<TypesGen.ExternalAgentCredentials> => {
const response = await this.axios.get(
`/api/v2/workspaces/${workspaceID}/external-agent/${agentName}/credentials`,
);
return response.data;
};

upsertWorkspaceAgentSharedPort = async (
workspaceID: string,
req: TypesGen.UpsertWorkspaceAgentPortShareRequest,
Expand Down
10 changes: 10 additions & 0 deletionssite/src/api/queries/workspaces.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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),
};
};
9 changes: 9 additions & 0 deletionssite/src/components/CodeExample/CodeExample.stories.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
},
};
65 changes: 58 additions & 7 deletionssite/src/components/CodeExample/CodeExample.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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;
}

Expand All@@ -15,11 +30,28 @@ interface CodeExampleProps {
export const CodeExample: FC<CodeExampleProps> = ({
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 ? (
<EyeOffIcon className="h-4 w-4" />
) : (
<EyeIcon className="h-4 w-4" />
);

return (
<div css={styles.container} className={className}>
<code css={[styles.code, secret && styles.secret]}>
Expand All@@ -33,17 +65,36 @@ export const CodeExample: FC<CodeExampleProps> = ({
* 2. Even with it turned on and supported, the plaintext is still
* readily available in the HTML itself
*/}
<span aria-hidden>{obfuscateText(code)}</span>
<span aria-hidden>{displayValue}</span>
<span className="sr-only">
Encrypted text. Please access via the copy button.
</span>
</>
) : (
code
displayValue
)}
</code>

<CopyButton text={code} label="Copy code" />
<div className="flex items-center gap-1">
{showRevealButton && redactPattern && !secret && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="subtle"
onClick={() => setShowFullValue(!showFullValue)}
>
{icon}
<span className="sr-only">{showButtonLabel}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{showButtonLabel}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<CopyButton text={code} label="Copy code" />
</div>
</div>
);
};
Expand Down
70 changes: 70 additions & 0 deletionssite/src/modules/resources/AgentExternal.stories.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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<typeof AgentExternal> = {
title: "modules/resources/AgentExternal",
component: AgentExternal,
args: {
agent: {
...MockWorkspaceAgent,
status: "connecting",
operating_system: "linux",
architecture: "amd64",
},
workspace: MockWorkspace,
},
decorators: [withDashboardProvider],
parameters: {
chromatic,
},
};

export default meta;
type Story = StoryObj<typeof AgentExternal>;

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: {
agent: {
...MockWorkspaceAgent,
status: "connecting",
operating_system: "linux",
architecture: "amd64",
},
},
};
45 changes: 45 additions & 0 deletionssite/src/modules/resources/AgentExternal.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
import { workspaceAgentCredentials } from "api/queries/workspaces";
import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { CodeExample } from "components/CodeExample/CodeExample";
import { Loader } from "components/Loader/Loader";
import type { FC } from "react";
import { useQuery } from "react-query";

interface AgentExternalProps {
agent: WorkspaceAgent;
workspace: Workspace;
}

export const AgentExternal: FC<AgentExternalProps> = ({ agent, workspace }) => {
const {
data: credentials,
error,
isLoading,
isError,
} = useQuery(workspaceAgentCredentials(workspace.id, agent.name));

if (isLoading) {
return <Loader />;
}

if (isError) {
return <ErrorAlert error={error} />;
}

return (
<section className="text-base text-muted-foreground pb-2 leading-relaxed">
<p>
Please run the following command to attach an agent to the{" "}
{workspace.name} workspace:
</p>
<CodeExample
code={credentials?.command ?? ""}
secret={false}
redactPattern={/CODER_AGENT_TOKEN="([^"]+)"/g}
redactReplacement={`CODER_AGENT_TOKEN="********"`}
showRevealButton={true}
/>
</section>
);
};
14 changes: 11 additions & 3 deletionssite/src/modules/resources/AgentRow.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -58,13 +59,14 @@ export const AgentRow: FC<AgentRowProps> = ({
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);
const isExternalAgent = workspace.latest_build.has_external_agent;
const shouldDisplayAgentApps =
(agent.status === "connected" && hasAppsToDisplay) ||
agent.status === "connecting";
(agent.status === "connecting" && !isExternalAgent);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

why do we only hide them while connecting?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I hid the apps when connecting to have a clear view with external agent credentials. The apps would only be installed when the agent is connected, so having them visible doesn’t make sense imo.

imageimage

aslilac reacted with thumbs up emoji
const hasVSCodeApp =
agent.display_apps.includes("vscode") ||
agent.display_apps.includes("vscode_insiders");
Expand DownExpand Up@@ -258,7 +260,7 @@ export const AgentRow: FC<AgentRowProps> = ({
</section>
)}

{agent.status === "connecting" && (
{agent.status === "connecting" &&!isExternalAgent &&(
<section css={styles.apps}>
<Skeleton
width={80}
Expand DownExpand Up@@ -293,6 +295,12 @@ export const AgentRow: FC<AgentRowProps> = ({
</section>
)}

{isExternalAgent &&
(agent.status === "timeout" || agent.status === "connecting") &&
workspace_external_agent && (
<AgentExternal agent={agent} workspace={workspace} />
)}

<AgentMetadata initialMetadata={initialMetadata} agent={agent} />
</div>

Expand Down
8 changes: 8 additions & 0 deletionssite/src/modules/workspaces/actions.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,14 @@ export const abilitiesByWorkspaceStatus = (
};
}

if(workspace.latest_build.has_external_agent){
return{
actions:[],
canCancel:false,
canAcceptJobs:true,
};
}

conststatus=workspace.latest_build.status;

switch(status){
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp