diff --git a/site/src/pages/AgentsPage/AgentDetail.tsx b/site/src/pages/AgentsPage/AgentDetail.tsx index bcfd992a7b..f1ffc58ab0 100644 --- a/site/src/pages/AgentsPage/AgentDetail.tsx +++ b/site/src/pages/AgentsPage/AgentDetail.tsx @@ -11,12 +11,18 @@ import { interruptChat, promoteChatQueuedMessage, } from "api/queries/chats"; +import { deploymentSSHConfig } from "api/queries/deployment"; import { workspaceById } from "api/queries/workspaces"; import type * as TypesGen from "api/typesGenerated"; import type { ModelSelectorOption } from "components/ai-elements"; import { Skeleton } from "components/Skeleton/Skeleton"; import { ArchiveIcon } from "lucide-react"; -import { getVSCodeHref, SESSION_TOKEN_PLACEHOLDER } from "modules/apps/apps"; +import { + getTerminalHref, + getVSCodeHref, + openAppInNewWindow, + SESSION_TOKEN_PLACEHOLDER, +} from "modules/apps/apps"; import { type FC, useCallback, @@ -515,6 +521,7 @@ const AgentDetail: FC = () => { }); const chatModelsQuery = useQuery(chatModels()); const chatModelConfigsQuery = useQuery(chatModelConfigs()); + const sshConfigQuery = useQuery(deploymentSSHConfig()); const hasDiffStatus = Boolean(diffStatusQuery.data?.url); const workspace = workspaceQuery.data; const workspaceAgent = getWorkspaceAgent(workspace, undefined); @@ -818,6 +825,18 @@ const AgentDetail: FC = () => { : null; const canOpenWorkspace = Boolean(workspaceRoute); const canOpenEditors = Boolean(workspace && workspaceAgent); + const terminalHref = + workspace && workspaceAgent + ? getTerminalHref({ + username: workspace.owner_name, + workspace: workspace.name, + agent: workspaceAgent.name, + }) + : null; + const sshCommand = + workspace && workspaceAgent && sshConfigQuery.data?.hostname_suffix + ? `ssh ${workspaceAgent.name}.${workspace.name}.${workspace.owner_name}.${sshConfigQuery.data.hostname_suffix}` + : undefined; const shouldShowDiffPanel = hasDiffStatus && showDiffPanel; const handleOpenInEditor = async (editor: "cursor" | "vscode") => { @@ -868,6 +887,13 @@ const AgentDetail: FC = () => { navigate(workspaceRoute); }; + const handleOpenTerminal = () => { + if (!terminalHref) { + return; + } + openAppInNewWindow(terminalHref); + }; + const handleArchiveAgentAction = () => { if (!agentId || isArchived) { return; @@ -897,6 +923,8 @@ const AgentDetail: FC = () => { canOpenWorkspace: false, onOpenInEditor: () => {}, onViewWorkspace: () => {}, + onOpenTerminal: () => {}, + sshCommand: undefined, }} onOpenParentChat={() => {}} onArchiveAgent={() => {}} @@ -968,6 +996,8 @@ const AgentDetail: FC = () => { canOpenWorkspace: false, onOpenInEditor: () => {}, onViewWorkspace: () => {}, + onOpenTerminal: () => {}, + sshCommand: undefined, }} onOpenParentChat={() => {}} onArchiveAgent={() => {}} @@ -1009,6 +1039,8 @@ const AgentDetail: FC = () => { void handleOpenInEditor(editor); }, onViewWorkspace: handleViewWorkspace, + onOpenTerminal: handleOpenTerminal, + sshCommand, }} onArchiveAgent={handleArchiveAgentAction} onArchiveAndDeleteWorkspace={handleArchiveAndDeleteWorkspaceAction} diff --git a/site/src/pages/AgentsPage/AgentDetail/TopBar.stories.tsx b/site/src/pages/AgentsPage/AgentDetail/TopBar.stories.tsx index 7848d6e590..fd787b49d1 100644 --- a/site/src/pages/AgentsPage/AgentDetail/TopBar.stories.tsx +++ b/site/src/pages/AgentsPage/AgentDetail/TopBar.stories.tsx @@ -26,6 +26,8 @@ const defaultProps = { canOpenWorkspace: true, onOpenInEditor: () => {}, onViewWorkspace: () => {}, + onOpenTerminal: () => {}, + sshCommand: "ssh main.my-workspace.admin.coder", }, onArchiveAgent: () => {}, onArchiveAndDeleteWorkspace: () => {}, diff --git a/site/src/pages/AgentsPage/AgentDetail/TopBar.tsx b/site/src/pages/AgentsPage/AgentDetail/TopBar.tsx index 37ba256687..f460291083 100644 --- a/site/src/pages/AgentsPage/AgentDetail/TopBar.tsx +++ b/site/src/pages/AgentsPage/AgentDetail/TopBar.tsx @@ -5,6 +5,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "components/DropdownMenu/DropdownMenu"; import { useAuthenticated } from "hooks"; @@ -12,18 +13,21 @@ import { ArchiveIcon, ArrowLeftIcon, ChevronRightIcon, + CopyIcon, EllipsisIcon, ExternalLinkIcon, MonitorIcon, PanelLeftIcon, PanelRightCloseIcon, PanelRightOpenIcon, + TerminalIcon, Trash2Icon, } from "lucide-react"; import { UserDropdown } from "modules/dashboard/Navbar/UserDropdown/UserDropdown"; import { useDashboard } from "modules/dashboard/useDashboard"; import type { FC } from "react"; import { useNavigate } from "react-router"; +import { toast } from "sonner"; import { WebPushButton } from "../WebPushButton"; interface DiffStatsBadgeProps { @@ -73,6 +77,8 @@ interface WorkspaceActions { canOpenWorkspace: boolean; onOpenInEditor: (editor: "cursor" | "vscode") => void; onViewWorkspace: () => void; + onOpenTerminal: () => void; + sshCommand: string | undefined; } type AgentDetailTopBarProps = { @@ -197,6 +203,30 @@ export const AgentDetailTopBar: FC = ({ Open in VS Code + + + Open Terminal + + { + if (!workspace.sshCommand) return; + try { + await navigator.clipboard.writeText(workspace.sshCommand); + toast.success("SSH command copied to clipboard"); + } catch { + toast.error("Failed to copy SSH command"); + } + }} + > + + Copy SSH Command + + = ({ View Workspace + {!isArchived && (