mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): add Open Terminal and Copy SSH Command to agent chat TopBar (#22529)
Adds two new items to the agent chat TopBar dropdown menu: - **Open Terminal**: opens the workspace web terminal in a new browser window, reusing the existing `getTerminalHref`/`openAppInNewWindow` infra. - **Copy SSH Command**: copies the SSH command (e.g. `ssh agent.workspace.owner.suffix`) to the clipboard with a toast confirmation. Only shown when the deployment SSH hostname suffix is configured. Both items appear after a separator below the existing editor/workspace actions. ## Changes | File | What | |---|---| | `TopBar.tsx` | Added `Open Terminal` and `Copy SSH Command` dropdown items with separator, `TerminalIcon`/`CopyIcon`, toast on copy | | `AgentDetail.tsx` | Wired up `getTerminalHref`, `openAppInNewWindow`, `deploymentSSHConfig` query, and passed new props to TopBar in all 3 render paths | | `TopBar.stories.tsx` | Added new fields to default story props |
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -26,6 +26,8 @@ const defaultProps = {
|
||||
canOpenWorkspace: true,
|
||||
onOpenInEditor: () => {},
|
||||
onViewWorkspace: () => {},
|
||||
onOpenTerminal: () => {},
|
||||
sshCommand: "ssh main.my-workspace.admin.coder",
|
||||
},
|
||||
onArchiveAgent: () => {},
|
||||
onArchiveAndDeleteWorkspace: () => {},
|
||||
|
||||
@@ -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<AgentDetailTopBarProps> = ({
|
||||
<ExternalLinkIcon className="h-3.5 w-3.5" />
|
||||
Open in VS Code
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
// You can think of the web terminal as an editor if you squint.
|
||||
disabled={!workspace.canOpenEditors}
|
||||
onSelect={workspace.onOpenTerminal}
|
||||
>
|
||||
<TerminalIcon className="h-3.5 w-3.5" />
|
||||
Open Terminal
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={!workspace.sshCommand}
|
||||
onSelect={async () => {
|
||||
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");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-3.5 w-3.5" />
|
||||
Copy SSH Command
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
disabled={!workspace.canOpenWorkspace}
|
||||
onSelect={workspace.onViewWorkspace}
|
||||
@@ -204,6 +234,7 @@ export const AgentDetailTopBar: FC<AgentDetailTopBarProps> = ({
|
||||
<MonitorIcon className="h-3.5 w-3.5" />
|
||||
View Workspace
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{!isArchived && (
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
|
||||
Reference in New Issue
Block a user