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:
Ethan
2026-07-01 16:58:10 +10:00
committed by GitHub
parent cb1a87b9c0
commit ac8cda66f7
8 changed files with 101 additions and 146 deletions
-4
View File
@@ -19630,13 +19630,11 @@ const docTemplate = `{
"nats_pubsub", "nats_pubsub",
"minimum-implicit-member", "minimum-implicit-member",
"ai-gateway-cost-control", "ai-gateway-cost-control",
"agent-app-tabs",
"chat-advisor", "chat-advisor",
"chat-virtual-desktop" "chat-virtual-desktop"
], ],
"x-enum-comments": { "x-enum-comments": {
"ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.", "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.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
"ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.", "ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.",
"ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.", "ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.",
@@ -19660,7 +19658,6 @@ const docTemplate = `{
"Enables embedded NATS pubsub.", "Enables embedded NATS pubsub.",
"Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.", "Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
"Enables AI Gateway cost control functionality.", "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 the advisor tool for root agent chats.",
"Enables virtual desktop and computer use provider for agents." "Enables virtual desktop and computer use provider for agents."
], ],
@@ -19675,7 +19672,6 @@ const docTemplate = `{
"ExperimentNATSPubsub", "ExperimentNATSPubsub",
"ExperimentMinimumImplicitMember", "ExperimentMinimumImplicitMember",
"ExperimentAIGatewayCostControl", "ExperimentAIGatewayCostControl",
"ExperimentAgentAppTabs",
"ExperimentChatAdvisor", "ExperimentChatAdvisor",
"ExperimentChatVirtualDesktop" "ExperimentChatVirtualDesktop"
] ]
-4
View File
@@ -17823,13 +17823,11 @@
"nats_pubsub", "nats_pubsub",
"minimum-implicit-member", "minimum-implicit-member",
"ai-gateway-cost-control", "ai-gateway-cost-control",
"agent-app-tabs",
"chat-advisor", "chat-advisor",
"chat-virtual-desktop" "chat-virtual-desktop"
], ],
"x-enum-comments": { "x-enum-comments": {
"ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.", "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.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
"ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.", "ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.",
"ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.", "ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.",
@@ -17853,7 +17851,6 @@
"Enables embedded NATS pubsub.", "Enables embedded NATS pubsub.",
"Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.", "Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
"Enables AI Gateway cost control functionality.", "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 the advisor tool for root agent chats.",
"Enables virtual desktop and computer use provider for agents." "Enables virtual desktop and computer use provider for agents."
], ],
@@ -17868,7 +17865,6 @@
"ExperimentNATSPubsub", "ExperimentNATSPubsub",
"ExperimentMinimumImplicitMember", "ExperimentMinimumImplicitMember",
"ExperimentAIGatewayCostControl", "ExperimentAIGatewayCostControl",
"ExperimentAgentAppTabs",
"ExperimentChatAdvisor", "ExperimentChatAdvisor",
"ExperimentChatVirtualDesktop" "ExperimentChatVirtualDesktop"
] ]
-4
View File
@@ -5197,7 +5197,6 @@ const (
ExperimentNATSPubsub Experiment = "nats_pubsub" // Enables embedded NATS pubsub. 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. 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. 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. 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. 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)" return "Gateway Accounts (minimum implicit member)"
case ExperimentAIGatewayCostControl: case ExperimentAIGatewayCostControl:
return "AI Gateway Cost Control" return "AI Gateway Cost Control"
case ExperimentAgentAppTabs:
return "Coder Agents App and Port Tabs"
case ExperimentChatAdvisor: case ExperimentChatAdvisor:
return "Chat Advisor" return "Chat Advisor"
case ExperimentChatVirtualDesktop: case ExperimentChatVirtualDesktop:
@@ -5250,7 +5247,6 @@ var ExperimentsKnown = Experiments{
ExperimentWorkspaceBuildUpdates, ExperimentWorkspaceBuildUpdates,
ExperimentMinimumImplicitMember, ExperimentMinimumImplicitMember,
ExperimentAIGatewayCostControl, ExperimentAIGatewayCostControl,
ExperimentAgentAppTabs,
ExperimentChatAdvisor, ExperimentChatAdvisor,
ExperimentChatVirtualDesktop, ExperimentChatVirtualDesktop,
} }
+3 -3
View File
@@ -7087,9 +7087,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
#### Enumerated Values #### Enumerated Values
| Value(s) | | 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` | | `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 ## codersdk.ExternalAPIKeyScopes
-2
View File
@@ -4586,7 +4586,6 @@ export const EntitlementsWarningHeader = "X-Coder-Entitlements-Warning";
// From codersdk/deployment.go // From codersdk/deployment.go
export type Experiment = export type Experiment =
| "ai-gateway-cost-control" | "ai-gateway-cost-control"
| "agent-app-tabs"
| "auto-fill-parameters" | "auto-fill-parameters"
| "chat-advisor" | "chat-advisor"
| "chat-virtual-desktop" | "chat-virtual-desktop"
@@ -4601,7 +4600,6 @@ export type Experiment =
export const Experiments: Experiment[] = [ export const Experiments: Experiment[] = [
"ai-gateway-cost-control", "ai-gateway-cost-control",
"agent-app-tabs",
"auto-fill-parameters", "auto-fill-parameters",
"chat-advisor", "chat-advisor",
"chat-virtual-desktop", "chat-virtual-desktop",
@@ -22,7 +22,6 @@ import { useProxy } from "#/contexts/ProxyContext";
import { isWorkspaceAppEmbeddable } from "#/modules/apps/apps"; import { isWorkspaceAppEmbeddable } from "#/modules/apps/apps";
import { WorkspaceAppFrame } from "#/modules/apps/WorkspaceAppFrame"; import { WorkspaceAppFrame } from "#/modules/apps/WorkspaceAppFrame";
import { findWorkspaceAppWithAgent } from "#/modules/apps/workspaceApps"; import { findWorkspaceAppWithAgent } from "#/modules/apps/workspaceApps";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { cn } from "#/utils/cn"; import { cn } from "#/utils/cn";
import { pageTitle } from "#/utils/page"; import { pageTitle } from "#/utils/page";
import { findWorkspaceAgent } from "#/utils/workspace"; import { findWorkspaceAgent } from "#/utils/workspace";
@@ -387,7 +386,6 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { proxy } = useProxy(); const { proxy } = useProxy();
const wildcardHostname = proxy.preferredWildcardHostname; const wildcardHostname = proxy.preferredWildcardHostname;
const { experiments } = useDashboard();
const canOpenChatSharing = canShareChat && organizationId !== undefined; const canOpenChatSharing = canShareChat && organizationId !== undefined;
@@ -494,19 +492,10 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
const availableDesktopChatId = const availableDesktopChatId =
workspace && workspaceAgent ? desktopChatId : undefined; 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( const validatedUserRightPanelTabs = validateUserRightPanelTabs(
userRightPanelTabs, userRightPanelTabs,
{ workspace, workspaceAgent, wildcardHostname }, { workspace, workspaceAgent, wildcardHostname },
).filter((tab) => userAppTabsEnabled || tab.kind === "terminal"); );
const hasBuiltInTerminal = Boolean( const hasBuiltInTerminal = Boolean(
workspace && workspaceAgent && !defaultTerminalHidden, workspace && workspaceAgent && !defaultTerminalHidden,
@@ -1001,7 +990,6 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
tabs={sidebarTabs} tabs={sidebarTabs}
addTabControl={ addTabControl={
<RightPanelAddTabControl <RightPanelAddTabControl
appExperimentEnabled={userAppTabsEnabled}
workspace={workspace} workspace={workspace}
agent={workspaceAgent} agent={workspaceAgent}
host={wildcardHostname} host={wildcardHostname}
@@ -48,7 +48,6 @@ const meta = {
title: "pages/AgentsPage/components/RightPanel/RightPanelAddTabControl", title: "pages/AgentsPage/components/RightPanel/RightPanelAddTabControl",
component: RightPanelAddTabControl, component: RightPanelAddTabControl,
args: { args: {
appExperimentEnabled: true,
workspace: MockWorkspace, workspace: MockWorkspace,
agent: { agent: {
...MockWorkspaceAgent, ...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<{ export const RightPanelAddTabControl: FC<{
appExperimentEnabled: boolean;
workspace?: Workspace; workspace?: Workspace;
agent?: WorkspaceAgent; agent?: WorkspaceAgent;
host?: string; host?: string;
@@ -69,7 +68,6 @@ export const RightPanelAddTabControl: FC<{
onOpenCommandApp?: (app: WorkspaceApp) => void; onOpenCommandApp?: (app: WorkspaceApp) => void;
onOpenPort?: (selection: PortSelection) => void; onOpenPort?: (selection: PortSelection) => void;
}> = ({ }> = ({
appExperimentEnabled,
workspace, workspace,
agent, agent,
host = "", host = "",
@@ -93,119 +91,114 @@ export const RightPanelAddTabControl: FC<{
disabled={!canCreateTerminal} disabled={!canCreateTerminal}
aria-label="New terminal tab" aria-label="New terminal tab"
title="New terminal tab" title="New terminal tab"
className={cn( 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"
"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",
)}
> >
<PlusIcon className="size-3.5" /> <PlusIcon className="size-3.5" />
</Button> </Button>
{appExperimentEnabled && ( <DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenu open={open} onOpenChange={setOpen}> <DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild> <Button
<Button variant="subtle"
variant="subtle" size="icon"
size="icon" aria-label="Add panel"
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"
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"
> >
<DropdownMenuItem <ChevronDownIcon
onSelect={onNewTerminal} className={cn(
disabled={!canCreateTerminal} "size-3 transition-transform",
> open && "rotate-180",
<SquareTerminalIcon /> )}
New Terminal />
</DropdownMenuItem> </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 && ( {workspace && agent && userApps.length > 0 && (
<> <>
<DropdownMenuSeparator className="my-1" /> <DropdownMenuSeparator className="my-1" />
{userApps.map((app) => { {userApps.map((app) => {
if (app.command && onOpenCommandApp) { 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>
);
}
return ( return (
<AppLink <DropdownMenuItem
key={app.id} key={app.id}
workspace={workspace} onSelect={() => onOpenCommandApp(app)}
agent={agent} disabled={!isRunning}
app={app} >
grouped {app.icon ? (
/> <ExternalImage
src={app.icon}
alt=""
className="rounded-sm"
/>
) : (
<SquareTerminalIcon />
)}
{app.display_name ?? app.slug}
</DropdownMenuItem>
); );
})} }
</> if (isWorkspaceAppEmbeddable(app) && onOpenWorkspaceApp) {
)} return (
<DropdownMenuItem
{workspace && key={app.id}
agent && onSelect={() => onOpenWorkspaceApp(app)}
onOpenPort && disabled={!isRunning}
canShowPortForwarding(agent, host) && ( >
<> {app.icon ? (
<DropdownMenuSeparator className="my-1" /> <ExternalImage
<AgentPortsSubMenu src={app.icon}
alt=""
className="rounded-sm"
/>
) : (
<LayoutGridIcon />
)}
{app.display_name ?? app.slug}
</DropdownMenuItem>
);
}
return (
<AppLink
key={app.id}
workspace={workspace} workspace={workspace}
agent={agent} agent={agent}
host={host} app={app}
isOpen={open} grouped
isRunning={isRunning}
onPortSelect={onOpenPort}
/> />
</> );
)} })}
</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> </div>
); );
}; };