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",
"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"
]
-4
View File
@@ -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"
]
-4
View File
@@ -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,
}
+3 -3
View File
@@ -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
-2
View File
@@ -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>
);
};