mirror of
https://github.com/coder/coder.git
synced 2026-09-21 12:44:32 +08:00
feat: make Coder Agents right sidebar app and port tabs generally available (#26906)
The workspace-app and port preview tabs in the Coder Agents right panel were gated behind the `agent-app-tabs` deployment experiment. This removes the experiment entirely and renders the app and port tabs unconditionally, so the add-panel dropdown, workspace-app tabs, and port preview tabs are always available alongside terminals. ## Changes - Remove the `ExperimentAgentAppTabs` constant, its `DisplayName()` case, and its `ExperimentsKnown` registration in `codersdk/deployment.go`, then regenerate `site/src/api/typesGenerated.ts`, `coderd/apidoc/docs.go`, `coderd/apidoc/swagger.json`, and `docs/reference/api/schemas.md`. - Drop the frontend experiment gate in `AgentChatPageView.tsx` (including the now-unused `useDashboard`/`experiments` usage) so persisted app and port tabs are no longer filtered out. - Remove the `appExperimentEnabled` prop from `RightPanelAddTabControl` and render the add-panel dropdown unconditionally; update the stories accordingly. This reverses the gating introduced in #26395. note: the diff is tiny if you hide whitespace changes
This commit is contained in:
Generated
-4
@@ -19630,13 +19630,11 @@ const docTemplate = `{
|
||||
"nats_pubsub",
|
||||
"minimum-implicit-member",
|
||||
"ai-gateway-cost-control",
|
||||
"agent-app-tabs",
|
||||
"chat-advisor",
|
||||
"chat-virtual-desktop"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.",
|
||||
"ExperimentAgentAppTabs": "Enables workspace-app and port preview tabs in the Coder Agents right panel.",
|
||||
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
||||
"ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.",
|
||||
"ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.",
|
||||
@@ -19660,7 +19658,6 @@ const docTemplate = `{
|
||||
"Enables embedded NATS pubsub.",
|
||||
"Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
|
||||
"Enables AI Gateway cost control functionality.",
|
||||
"Enables workspace-app and port preview tabs in the Coder Agents right panel.",
|
||||
"Enables the advisor tool for root agent chats.",
|
||||
"Enables virtual desktop and computer use provider for agents."
|
||||
],
|
||||
@@ -19675,7 +19672,6 @@ const docTemplate = `{
|
||||
"ExperimentNATSPubsub",
|
||||
"ExperimentMinimumImplicitMember",
|
||||
"ExperimentAIGatewayCostControl",
|
||||
"ExperimentAgentAppTabs",
|
||||
"ExperimentChatAdvisor",
|
||||
"ExperimentChatVirtualDesktop"
|
||||
]
|
||||
|
||||
Generated
-4
@@ -17823,13 +17823,11 @@
|
||||
"nats_pubsub",
|
||||
"minimum-implicit-member",
|
||||
"ai-gateway-cost-control",
|
||||
"agent-app-tabs",
|
||||
"chat-advisor",
|
||||
"chat-virtual-desktop"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.",
|
||||
"ExperimentAgentAppTabs": "Enables workspace-app and port preview tabs in the Coder Agents right panel.",
|
||||
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
||||
"ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.",
|
||||
"ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.",
|
||||
@@ -17853,7 +17851,6 @@
|
||||
"Enables embedded NATS pubsub.",
|
||||
"Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
|
||||
"Enables AI Gateway cost control functionality.",
|
||||
"Enables workspace-app and port preview tabs in the Coder Agents right panel.",
|
||||
"Enables the advisor tool for root agent chats.",
|
||||
"Enables virtual desktop and computer use provider for agents."
|
||||
],
|
||||
@@ -17868,7 +17865,6 @@
|
||||
"ExperimentNATSPubsub",
|
||||
"ExperimentMinimumImplicitMember",
|
||||
"ExperimentAIGatewayCostControl",
|
||||
"ExperimentAgentAppTabs",
|
||||
"ExperimentChatAdvisor",
|
||||
"ExperimentChatVirtualDesktop"
|
||||
]
|
||||
|
||||
@@ -5197,7 +5197,6 @@ const (
|
||||
ExperimentNATSPubsub Experiment = "nats_pubsub" // Enables embedded NATS pubsub.
|
||||
ExperimentMinimumImplicitMember Experiment = "minimum-implicit-member" // Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.
|
||||
ExperimentAIGatewayCostControl Experiment = "ai-gateway-cost-control" // Enables AI Gateway cost control functionality.
|
||||
ExperimentAgentAppTabs Experiment = "agent-app-tabs" // Enables workspace-app and port preview tabs in the Coder Agents right panel.
|
||||
ExperimentChatAdvisor Experiment = "chat-advisor" // Enables the advisor tool for root agent chats.
|
||||
ExperimentChatVirtualDesktop Experiment = "chat-virtual-desktop" // Enables virtual desktop and computer use provider for agents.
|
||||
)
|
||||
@@ -5224,8 +5223,6 @@ func (e Experiment) DisplayName() string {
|
||||
return "Gateway Accounts (minimum implicit member)"
|
||||
case ExperimentAIGatewayCostControl:
|
||||
return "AI Gateway Cost Control"
|
||||
case ExperimentAgentAppTabs:
|
||||
return "Coder Agents App and Port Tabs"
|
||||
case ExperimentChatAdvisor:
|
||||
return "Chat Advisor"
|
||||
case ExperimentChatVirtualDesktop:
|
||||
@@ -5250,7 +5247,6 @@ var ExperimentsKnown = Experiments{
|
||||
ExperimentWorkspaceBuildUpdates,
|
||||
ExperimentMinimumImplicitMember,
|
||||
ExperimentAIGatewayCostControl,
|
||||
ExperimentAgentAppTabs,
|
||||
ExperimentChatAdvisor,
|
||||
ExperimentChatVirtualDesktop,
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -7087,9 +7087,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value(s) |
|
||||
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `agent-app-tabs`, `ai-gateway-cost-control`, `auto-fill-parameters`, `chat-advisor`, `chat-virtual-desktop`, `example`, `mcp-server-http`, `minimum-implicit-member`, `nats_pubsub`, `notifications`, `oauth2`, `workspace-build-updates`, `workspace-usage` |
|
||||
| Value(s) |
|
||||
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `ai-gateway-cost-control`, `auto-fill-parameters`, `chat-advisor`, `chat-virtual-desktop`, `example`, `mcp-server-http`, `minimum-implicit-member`, `nats_pubsub`, `notifications`, `oauth2`, `workspace-build-updates`, `workspace-usage` |
|
||||
|
||||
## codersdk.ExternalAPIKeyScopes
|
||||
|
||||
|
||||
Generated
-2
@@ -4586,7 +4586,6 @@ export const EntitlementsWarningHeader = "X-Coder-Entitlements-Warning";
|
||||
// From codersdk/deployment.go
|
||||
export type Experiment =
|
||||
| "ai-gateway-cost-control"
|
||||
| "agent-app-tabs"
|
||||
| "auto-fill-parameters"
|
||||
| "chat-advisor"
|
||||
| "chat-virtual-desktop"
|
||||
@@ -4601,7 +4600,6 @@ export type Experiment =
|
||||
|
||||
export const Experiments: Experiment[] = [
|
||||
"ai-gateway-cost-control",
|
||||
"agent-app-tabs",
|
||||
"auto-fill-parameters",
|
||||
"chat-advisor",
|
||||
"chat-virtual-desktop",
|
||||
|
||||
@@ -22,7 +22,6 @@ import { useProxy } from "#/contexts/ProxyContext";
|
||||
import { isWorkspaceAppEmbeddable } from "#/modules/apps/apps";
|
||||
import { WorkspaceAppFrame } from "#/modules/apps/WorkspaceAppFrame";
|
||||
import { findWorkspaceAppWithAgent } from "#/modules/apps/workspaceApps";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import { findWorkspaceAgent } from "#/utils/workspace";
|
||||
@@ -387,7 +386,6 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
|
||||
const queryClient = useQueryClient();
|
||||
const { proxy } = useProxy();
|
||||
const wildcardHostname = proxy.preferredWildcardHostname;
|
||||
const { experiments } = useDashboard();
|
||||
|
||||
const canOpenChatSharing = canShareChat && organizationId !== undefined;
|
||||
|
||||
@@ -494,19 +492,10 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
|
||||
const availableDesktopChatId =
|
||||
workspace && workspaceAgent ? desktopChatId : undefined;
|
||||
|
||||
// Workspace app and port preview tabs are gated behind the agent-app-tabs
|
||||
// experiment. Terminal tabs are generally available. Derived after all hook
|
||||
// calls so the React Compiler keeps the tab list and the tab-open handlers
|
||||
// below in a single memoization scope.
|
||||
const userAppTabsEnabled = experiments.includes("agent-app-tabs");
|
||||
|
||||
// When app and port tabs are gated off, persisted tabs of those kinds
|
||||
// are hidden rather than deleted; the save effect persists the raw tab
|
||||
// state, so they reappear if the gate lifts.
|
||||
const validatedUserRightPanelTabs = validateUserRightPanelTabs(
|
||||
userRightPanelTabs,
|
||||
{ workspace, workspaceAgent, wildcardHostname },
|
||||
).filter((tab) => userAppTabsEnabled || tab.kind === "terminal");
|
||||
);
|
||||
|
||||
const hasBuiltInTerminal = Boolean(
|
||||
workspace && workspaceAgent && !defaultTerminalHidden,
|
||||
@@ -1001,7 +990,6 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
|
||||
tabs={sidebarTabs}
|
||||
addTabControl={
|
||||
<RightPanelAddTabControl
|
||||
appExperimentEnabled={userAppTabsEnabled}
|
||||
workspace={workspace}
|
||||
agent={workspaceAgent}
|
||||
host={wildcardHostname}
|
||||
|
||||
@@ -48,7 +48,6 @@ const meta = {
|
||||
title: "pages/AgentsPage/components/RightPanel/RightPanelAddTabControl",
|
||||
component: RightPanelAddTabControl,
|
||||
args: {
|
||||
appExperimentEnabled: true,
|
||||
workspace: MockWorkspace,
|
||||
agent: {
|
||||
...MockWorkspaceAgent,
|
||||
@@ -176,14 +175,3 @@ export const DisconnectedWorkspace: Story = {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const AppExperimentDisabled: Story = {
|
||||
args: {
|
||||
appExperimentEnabled: false,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(canvas.getByLabelText("New terminal tab")).toBeEnabled();
|
||||
await expect(canvas.queryByLabelText("Add panel")).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,7 +59,6 @@ const AgentPortsSubMenu: FC<{
|
||||
};
|
||||
|
||||
export const RightPanelAddTabControl: FC<{
|
||||
appExperimentEnabled: boolean;
|
||||
workspace?: Workspace;
|
||||
agent?: WorkspaceAgent;
|
||||
host?: string;
|
||||
@@ -69,7 +68,6 @@ export const RightPanelAddTabControl: FC<{
|
||||
onOpenCommandApp?: (app: WorkspaceApp) => void;
|
||||
onOpenPort?: (selection: PortSelection) => void;
|
||||
}> = ({
|
||||
appExperimentEnabled,
|
||||
workspace,
|
||||
agent,
|
||||
host = "",
|
||||
@@ -93,119 +91,114 @@ export const RightPanelAddTabControl: FC<{
|
||||
disabled={!canCreateTerminal}
|
||||
aria-label="New terminal tab"
|
||||
title="New terminal tab"
|
||||
className={cn(
|
||||
"size-6 rounded-none border-0 bg-transparent p-0 text-content-secondary hover:bg-surface-secondary hover:text-content-primary",
|
||||
appExperimentEnabled && "border-r border-solid border-border-default",
|
||||
)}
|
||||
className="size-6 rounded-none border-0 bg-transparent p-0 text-content-secondary hover:bg-surface-secondary hover:text-content-primary border-r border-solid border-border-default"
|
||||
>
|
||||
<PlusIcon className="size-3.5" />
|
||||
</Button>
|
||||
{appExperimentEnabled && (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
aria-label="Add panel"
|
||||
className="size-6 rounded-none border-0 bg-transparent p-0 text-content-secondary hover:bg-surface-secondary hover:text-content-primary"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
"size-3 transition-transform",
|
||||
open && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
side="bottom"
|
||||
className="w-52 p-1 [&_[role=menuitem]]:py-1 [&_[role=menuitem]]:text-xs [&_img]:!size-3.5 [&_svg]:!size-3.5"
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
aria-label="Add panel"
|
||||
className="size-6 rounded-none border-0 bg-transparent p-0 text-content-secondary hover:bg-surface-secondary hover:text-content-primary"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={onNewTerminal}
|
||||
disabled={!canCreateTerminal}
|
||||
>
|
||||
<SquareTerminalIcon />
|
||||
New Terminal
|
||||
</DropdownMenuItem>
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
"size-3 transition-transform",
|
||||
open && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
side="bottom"
|
||||
className="w-52 p-1 [&_[role=menuitem]]:py-1 [&_[role=menuitem]]:text-xs [&_img]:!size-3.5 [&_svg]:!size-3.5"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={onNewTerminal}
|
||||
disabled={!canCreateTerminal}
|
||||
>
|
||||
<SquareTerminalIcon />
|
||||
New Terminal
|
||||
</DropdownMenuItem>
|
||||
|
||||
{workspace && agent && userApps.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
{userApps.map((app) => {
|
||||
if (app.command && onOpenCommandApp) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={app.id}
|
||||
onSelect={() => onOpenCommandApp(app)}
|
||||
disabled={!isRunning}
|
||||
>
|
||||
{app.icon ? (
|
||||
<ExternalImage
|
||||
src={app.icon}
|
||||
alt=""
|
||||
className="rounded-sm"
|
||||
/>
|
||||
) : (
|
||||
<SquareTerminalIcon />
|
||||
)}
|
||||
{app.display_name ?? app.slug}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
if (isWorkspaceAppEmbeddable(app) && onOpenWorkspaceApp) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={app.id}
|
||||
onSelect={() => onOpenWorkspaceApp(app)}
|
||||
disabled={!isRunning}
|
||||
>
|
||||
{app.icon ? (
|
||||
<ExternalImage
|
||||
src={app.icon}
|
||||
alt=""
|
||||
className="rounded-sm"
|
||||
/>
|
||||
) : (
|
||||
<LayoutGridIcon />
|
||||
)}
|
||||
{app.display_name ?? app.slug}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
{workspace && agent && userApps.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
{userApps.map((app) => {
|
||||
if (app.command && onOpenCommandApp) {
|
||||
return (
|
||||
<AppLink
|
||||
<DropdownMenuItem
|
||||
key={app.id}
|
||||
workspace={workspace}
|
||||
agent={agent}
|
||||
app={app}
|
||||
grouped
|
||||
/>
|
||||
onSelect={() => onOpenCommandApp(app)}
|
||||
disabled={!isRunning}
|
||||
>
|
||||
{app.icon ? (
|
||||
<ExternalImage
|
||||
src={app.icon}
|
||||
alt=""
|
||||
className="rounded-sm"
|
||||
/>
|
||||
) : (
|
||||
<SquareTerminalIcon />
|
||||
)}
|
||||
{app.display_name ?? app.slug}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{workspace &&
|
||||
agent &&
|
||||
onOpenPort &&
|
||||
canShowPortForwarding(agent, host) && (
|
||||
<>
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
<AgentPortsSubMenu
|
||||
}
|
||||
if (isWorkspaceAppEmbeddable(app) && onOpenWorkspaceApp) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={app.id}
|
||||
onSelect={() => onOpenWorkspaceApp(app)}
|
||||
disabled={!isRunning}
|
||||
>
|
||||
{app.icon ? (
|
||||
<ExternalImage
|
||||
src={app.icon}
|
||||
alt=""
|
||||
className="rounded-sm"
|
||||
/>
|
||||
) : (
|
||||
<LayoutGridIcon />
|
||||
)}
|
||||
{app.display_name ?? app.slug}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AppLink
|
||||
key={app.id}
|
||||
workspace={workspace}
|
||||
agent={agent}
|
||||
host={host}
|
||||
isOpen={open}
|
||||
isRunning={isRunning}
|
||||
onPortSelect={onOpenPort}
|
||||
app={app}
|
||||
grouped
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{workspace &&
|
||||
agent &&
|
||||
onOpenPort &&
|
||||
canShowPortForwarding(agent, host) && (
|
||||
<>
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
<AgentPortsSubMenu
|
||||
workspace={workspace}
|
||||
agent={agent}
|
||||
host={host}
|
||||
isOpen={open}
|
||||
isRunning={isRunning}
|
||||
onPortSelect={onOpenPort}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user