Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/design/agent-navigation-web-shell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# First-class Agent navigation in WebShell

## Problem

WebShell already has a complete Agent definition manager for workspace and global Agents, including prompts, models, tools, MCP servers, and permissions. It is reachable through `/agents` and as a nested Plugins tab, but it has no primary-sidebar entry. Users therefore cannot discover Agent management or see that Agent definitions can participate in Agent Team collaboration.

## Design

- Add `Agents` to the existing configurable primary-sidebar navigation and open the existing Agent manager. Do not create a second manager or route.
- Keep the entry workspace-scoped, matching the manager's daemon API and the existing Plugins, Channels, Workflows, and Goals entries.
- Add one persistent sentence under the Agent manager title explaining that specialized Agents can be coordinated in an Agent Team.
- Preserve host customization: embedders can omit `agents` from `primaryNav.items`, and callers that use the sidebar component directly are not forced to provide an Agent callback.

## Scope

Included: discoverable sidebar navigation, accessible collapsed label, existing manager routing, and collaboration-oriented copy.

Excluded: a second Agent definition UI, durable Team Run history, remote Agent hosts, a Team creation form, or changes to Agent Team runtime behavior. Live Team execution remains owned by the parent PR.

## Affected areas

- WebShell sidebar navigation and App routing.
- Agent manager title copy and English/Chinese localization.
- Focused navigation and browser acceptance coverage.

## Open questions

None for this slice. A future Team Runs page should be added only after the daemon has persistent Run identity rather than presenting current-session state as durable management.
28 changes: 28 additions & 0 deletions packages/web-shell/client/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,7 @@ vi.mock('./components/sidebar/WebShellSidebar', async () => {
WebShellSidebar: (props: {
collapsed?: boolean;
onOpenPlugins?: () => void;
onOpenAgents?: () => void;
onOpenChannels?: () => void;
onOpenDaemonStatus?: () => void;
onOpenSessions?: () => void;
Expand Down Expand Up @@ -1385,6 +1386,15 @@ vi.mock('./components/sidebar/WebShellSidebar', async () => {
},
'plugins',
),
React.createElement(
'button',
{
'data-testid': 'open-agents',
type: 'button',
onClick: props.onOpenAgents,
},
'agents',
),
React.createElement(
'button',
{
Expand Down Expand Up @@ -23876,6 +23886,24 @@ describe('App session callbacks', () => {
).toBe('true');
});

it('opens Agent management from the sidebar', async () => {
const { container } = renderApp();
await flush();

await act(async () => {
container
.querySelector<HTMLButtonElement>('[data-testid="open-agents"]')
?.click();
await Promise.resolve();
});

expect(
container
.querySelector('[data-testid="inline-panel"]')
?.getAttribute('aria-label'),
).toBe('Agents');
});

it('restores composer interaction after closing Plugins on the MCP tab', async () => {
mockMcp.reload.mockResolvedValue({
v: 1,
Expand Down
5 changes: 5 additions & 0 deletions packages/web-shell/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16272,6 +16272,11 @@ export function App({
closeMobileDrawer();
openPanel('settings');
}}
onOpenAgents={() => {
closeMobileDrawer();
setAgentsCreateScope(null);
openPanel('agents');
}}
onOpenPlugins={() => {
closeMobileDrawer();
openPanel('plugins');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,9 @@ export function AgentsManagerPage({
{t('agents.title')}
</h1>
<p className="mt-1 text-sm text-muted-foreground tabular-nums">
{t('agents.description')}
</p>
<p className="mt-1 text-xs text-muted-foreground tabular-nums">
{t('agent.count', { count: agents.length })}
</p>
</div>
Expand Down
22 changes: 22 additions & 0 deletions packages/web-shell/client/components/sidebar/WebShellSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
import {
FolderKanbanIcon,
ActivityIcon,
BotIcon,
BlocksIcon,
CalendarClockIcon,
ChevronDownIcon,
Expand Down Expand Up @@ -236,6 +237,7 @@ export interface WebShellSidebarLockedWorkspace {

export type WebShellSidebarPrimaryNavItem =
| 'newTask'
| 'agents'
| 'plugins'
| 'channels'
| 'scheduledTasks'
Expand Down Expand Up @@ -268,6 +270,7 @@ const DEFAULT_FOOTER_ITEMS: readonly WebShellSidebarFooterItem[] = [

const DEFAULT_PRIMARY_NAV_ITEMS: readonly WebShellSidebarPrimaryNavItem[] = [
'newTask',
'agents',
'plugins',
'channels',
'scheduledTasks',
Expand Down Expand Up @@ -374,6 +377,7 @@ interface WebShellSidebarProps {
collapsed: boolean;
onCollapsedChange: (collapsed: boolean) => void;
onOpenSettings: () => void;
onOpenAgents?: () => void;
onOpenPlugins: () => void;
onOpenChannels: () => void;
onOpenDaemonStatus: () => void;
Expand Down Expand Up @@ -877,6 +881,7 @@ export function WebShellSidebar({
collapsed,
onCollapsedChange,
onOpenSettings,
onOpenAgents,
onOpenPlugins,
onOpenChannels,
onOpenDaemonStatus,
Expand Down Expand Up @@ -939,6 +944,7 @@ export function WebShellSidebar({
const hasScrollingPrimaryNav =
(projectFeaturesEnabled &&
(primaryNavItems.has('plugins') ||
(primaryNavItems.has('agents') && Boolean(onOpenAgents)) ||
primaryNavItems.has('channels') ||
primaryNavItems.has('scheduledTasks') ||
primaryNavItems.has('workflows') ||
Expand Down Expand Up @@ -5289,6 +5295,22 @@ export function WebShellSidebar({
>
{hasScrollingPrimaryNav && (
<div className={styles.primaryNav}>
{projectFeaturesEnabled &&
onOpenAgents &&
primaryNavItems.has('agents') && (
<button
className={styles.pluginButton}
type="button"
title={t('agents.title')}
aria-label={t('agents.title')}
onClick={onOpenAgents}
>
<span className={styles.navIcon}>
<BotIcon size={16} strokeWidth={1.2} />
</span>
{!collapsed && <span>{t('agents.title')}</span>}
</button>
)}
{projectFeaturesEnabled && primaryNavItems.has('plugins') && (
<button
className={styles.pluginButton}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ function renderSidebar(
onSelectWorkspace?: (cwd: string | undefined) => void;
onError?: (error: unknown, message: string) => void;
onOpenGoals?: () => void;
onOpenAgents?: () => void;
onOpenWorkflows?: () => void;
onOpenWorkspaceManagement?: (
target: WorkspaceManagementTarget,
Expand Down Expand Up @@ -430,6 +431,7 @@ function renderSidebar(
render?: (workspace: DaemonWorkspaceCapability) => ReactNode;
};
showSessionSourceSwitch?: boolean;
primaryNav?: Parameters<typeof WebShellSidebar>[0]['primaryNav'];
projectFeaturesEnabled?: boolean;
sessionActions?: {
items?: readonly (
Expand All @@ -456,6 +458,7 @@ function renderSidebar(
onOpenScheduledTasks={() => {}}
onOpenWorkflows={overrides.onOpenWorkflows ?? (() => {})}
onOpenGoals={overrides.onOpenGoals ?? (() => {})}
onOpenAgents={overrides.onOpenAgents}
onOpenSessions={() => {}}
onOpenSplitView={() => {}}
onNewSession={overrides.onNewSession ?? (() => false)}
Expand All @@ -474,6 +477,7 @@ function renderSidebar(
lockedWorkspaceCwd={overrides.lockedWorkspaceCwd}
lockedWorkspace={overrides.lockedWorkspace}
showSessionSourceSwitch={overrides.showSessionSourceSwitch}
primaryNav={overrides.primaryNav}
projectFeaturesEnabled={overrides.projectFeaturesEnabled}
sessionActions={overrides.sessionActions}
/>
Expand Down Expand Up @@ -1652,6 +1656,7 @@ describe('WebShellSidebar workspace removal', () => {
}));
renderSidebar({
projectFeaturesEnabled: false,
onOpenAgents: vi.fn(),
onOpenAddWorkspace: vi.fn(),
onOpenWorkspacesOverview: vi.fn(),
onOpenGitDiff: vi.fn(),
Expand All @@ -1662,6 +1667,7 @@ describe('WebShellSidebar workspace removal', () => {
});

expect(container.querySelector('button[aria-label="Plugins"]')).toBeNull();
expect(container.querySelector('button[aria-label="Agents"]')).toBeNull();
expect(container.querySelector('button[aria-label="Channels"]')).toBeNull();
expect(container.querySelector('button[aria-label="Settings"]')).toBeNull();
// These open a project panel or dialog that only renders inside a
Expand Down Expand Up @@ -4453,6 +4459,30 @@ describe('WebShellSidebar goals entry', () => {
});
});

describe('WebShellSidebar Agents entry', () => {
it('opens Agent management from primary navigation by default', () => {
const onOpenAgents = vi.fn();
renderSidebar({ onOpenAgents });
const button = container.querySelector<HTMLButtonElement>(
'button[aria-label="Agents"]',
);
expect(button).not.toBeNull();

click(button!);

expect(onOpenAgents).toHaveBeenCalledOnce();
});

it('can be hidden through primary navigation customization', () => {
renderSidebar({
onOpenAgents: vi.fn(),
primaryNav: { items: ['plugins'] },
});

expect(container.querySelector('button[aria-label="Agents"]')).toBeNull();
});
});

describe('WebShellSidebar workflows entry', () => {
it('opens the workflow runs page from primary navigation', () => {
const onOpenWorkflows = vi.fn();
Expand Down
4 changes: 4 additions & 0 deletions packages/web-shell/client/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,8 @@ const EN: Messages = {
'agent.view': 'View',
'agents.closed': 'Agents panel closed.',
'agents.title': 'Agents',
'agents.description':
'Create specialized agents and let Qwen coordinate them in an Agent Team.',
'subagent.result': 'Result',
'subagent.tools': (v) => `Tools (${v?.count ?? 0})`,
'subagent.toolsCount': (v) => `${v?.count ?? 0} tools`,
Expand Down Expand Up @@ -3911,6 +3913,8 @@ const ZH: Messages = {
'agent.view': '查看',
'agents.closed': '智能体面板已关闭。',
'agents.title': '智能体',
'agents.description':
'创建专业智能体,并让 Qwen 通过 Agent Team 协调它们共同完成任务。',
'subagent.result': '结果',
'subagent.tools': (v) => `工具 (${v?.count ?? 0})`,
'subagent.toolsCount': (v) => `${v?.count ?? 0} 个工具`,
Expand Down