diff --git a/src/web-ui/src/features/dispatch/DispatchTargetPicker.scss b/src/web-ui/src/features/dispatch/DispatchTargetPicker.scss index d37573d70a..061ffdce2f 100644 --- a/src/web-ui/src/features/dispatch/DispatchTargetPicker.scss +++ b/src/web-ui/src/features/dispatch/DispatchTargetPicker.scss @@ -74,6 +74,18 @@ &__option-row { min-height: var(--bf-control-height-md); + + // The two local execution modes keep matching title ink regardless of + // which row is selected. Every option detail shares the quieter treatment + // declared on `small` below. + &[data-bf-part='option'] { + .dispatch-target-picker__option-copy { + strong { + color: var(--bf-color-content-secondary); + opacity: var(--bf-opacity-focus); + } + } + } } &__option-copy { @@ -99,6 +111,7 @@ small { color: var(--bf-color-content-muted); + opacity: 0.5; font-family: var(--bf-type-meta-font-family); font-size: var(--bf-type-meta-font-size); font-weight: var(--bf-type-meta-font-weight); diff --git a/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx b/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx index 476313b195..ee38686bed 100644 --- a/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx +++ b/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx @@ -152,12 +152,6 @@ export const DispatchTargetPicker: React.FC = ({ data-testid="dispatch-target-menu" autoFocusFirstItem > - -
- {t('chatInput.dispatch.sessionScope')} -
-
- ({ 'reasoningSelector.auto': 'Auto', 'chatInput.permissionMode.ask.label': 'Ask', 'strip.newWorktree': 'New Worktree', + 'workspaceStrip.primaryAssistant': 'Primary assistant', + 'workspaceStrip.personalAssistant': 'Personal assistant', } as Record)[key] ?? options?.defaultValue ?? key, }), })); @@ -243,10 +245,39 @@ describe('ChatInputWorkspaceStrip git refresh behavior', () => { it('switches the active workspace from the strip menu when several are open', async () => { mocks.useOptionalWorkspaceContext.mockReturnValue({ openedWorkspacesList: [ - { id: 'ws-1', name: 'BitFun', path: 'D:/workspace/BitFun' }, - { id: 'ws-2', name: 'Other', path: 'D:/workspace/Other' }, + { + id: 'ws-1', + name: 'BitFun', + rootPath: 'D:/workspace/BitFun', + workspaceKind: 'normal', + }, + { + id: 'ws-2', + name: 'Other', + rootPath: 'D:/workspace/Other', + workspaceKind: 'normal', + }, + { + id: 'ws-3', + name: 'Primary', + rootPath: 'D:/internal/assistants/ws-3', + workspaceKind: 'assistant', + }, + { + id: 'ws-4', + name: 'Personal', + rootPath: 'D:/internal/assistants/ws-4', + workspaceKind: 'assistant', + assistantId: 'assistant-4', + }, ], - activeWorkspace: { id: 'ws-1', name: 'BitFun', path: 'D:/workspace/BitFun' }, + activeWorkspace: { + id: 'ws-1', + name: 'BitFun', + rootPath: 'D:/workspace/BitFun', + workspaceKind: 'normal', + }, + primaryAssistantWorkspaceId: 'ws-3', setActiveWorkspace: mocks.setActiveWorkspace, }); @@ -275,6 +306,19 @@ describe('ChatInputWorkspaceStrip git refresh behavior', () => { expect( menu?.querySelector('[data-testid="chat-input-workspace-option-ws-1"]')?.getAttribute('aria-checked'), ).toBe('true'); + expect( + menu?.querySelector('[data-testid="chat-input-workspace-option-ws-1"]')?.textContent, + ).toContain('D:/workspace/BitFun'); + expect( + menu?.querySelector('[data-testid="chat-input-workspace-option-ws-2"]')?.textContent, + ).toContain('D:/workspace/Other'); + expect( + menu?.querySelector('[data-testid="chat-input-workspace-option-ws-3"]')?.textContent, + ).toContain('Primary assistant'); + expect( + menu?.querySelector('[data-testid="chat-input-workspace-option-ws-4"]')?.textContent, + ).toContain('Personal assistant'); + expect(menu?.textContent).not.toContain('D:/internal/assistants/'); const other = menu?.querySelector( '[data-testid="chat-input-workspace-option-ws-2"]', diff --git a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx index 4909b5d5bf..6916835d59 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx @@ -25,6 +25,7 @@ import { useOptionalWorkspaceContext, } from '@/infrastructure/contexts/WorkspaceContext'; import { useI18n } from '@/infrastructure/i18n'; +import { WorkspaceKind } from '@/shared/types'; import { useAnchoredPopoverPosition } from '@/shared/utils/useAnchoredPopoverPosition'; import { DispatchResultDialog } from '@/features/dispatch/DispatchResultDialog'; import { DispatchTargetPicker } from '@/features/dispatch/DispatchTargetPicker'; @@ -597,6 +598,21 @@ export const ChatInputWorkspaceStrip: React.FC = ( > {switchableWorkspaces.map(workspace => { const isActive = workspace.id === workspaceContext.activeWorkspace?.id; + const workspaceName = getWorkspaceDisplayName(workspace); + const workspacePath = workspace.rootPath?.trim(); + const isAssistantWorkspace = workspace.workspaceKind === WorkspaceKind.Assistant; + const isPrimaryAssistantWorkspace = ( + isAssistantWorkspace + && ( + workspace.id === workspaceContext.primaryAssistantWorkspaceId + || (!workspaceContext.primaryAssistantWorkspaceId && !workspace.assistantId) + ) + ); + const workspaceDetail = isAssistantWorkspace + ? t(isPrimaryAssistantWorkspace + ? 'workspaceStrip.primaryAssistant' + : 'workspaceStrip.personalAssistant') + : workspacePath; return ( = ( data-bf-part="workspaceOption" data-bf-state={isActive ? 'active' : undefined} data-testid={`chat-input-workspace-option-${workspace.id}`} + aria-label={workspaceDetail + ? `${workspaceName}, ${workspaceDetail}` + : workspaceName} + title={workspaceDetail || workspaceName} metadata={isActive ? : null} onClick={event => { event.stopPropagation(); @@ -615,7 +635,16 @@ export const ChatInputWorkspaceStrip: React.FC = ( } }} > - {getWorkspaceDisplayName(workspace)} + + + {workspaceName} + + {workspaceDetail ? ( + + {workspaceDetail} + + ) : null} + ); })} diff --git a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts index cda7a7f37a..c5141d5c75 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts +++ b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts @@ -73,8 +73,8 @@ describe('composer context track layout', () => { expect(branchPicker).toMatch( /branch-quick-switch__list \[data-bf-part='list'\] \{\n gap: calc\(var\(--bf-space-1\) \/ 2\);/, ); - expect(targetPicker).toContain( - '&__option-row {\n min-height: var(--bf-control-height-md);\n }', + expect(targetPicker).toMatch( + /&__option-row \{[\s\S]*?min-height: var\(--bf-control-height-md\);/, ); expect(targetPicker).toMatch( /&__menu \{[\s\S]*?--bf-overlay-menu-section-gap: var\(--bf-space-1\);/, @@ -88,6 +88,12 @@ describe('composer context track layout', () => { expect(targetPicker).toMatch( /small \{[\s\S]*?font-size: var\(--bf-type-meta-font-size\);/, ); + expect(targetPicker).toMatch( + /\&\[data-bf-part='option'\] \{[\s\S]*?strong \{[\s\S]*?color: var\(--bf-color-content-secondary\);[\s\S]*?opacity: var\(--bf-opacity-focus\);/, + ); + expect(targetPicker).toMatch( + /small \{[\s\S]*?color: var\(--bf-color-content-muted\);[\s\S]*?opacity: 0\.5;/, + ); }); it('keeps passive context aligned and promotes consequential controls', () => { @@ -236,6 +242,23 @@ describe('composer context track layout', () => { expect(component).toContain('data-testid="chat-input-workspace-trigger"'); expect(component).toContain('data-testid="chat-input-workspace-menu"'); expect(component).toContain('data-bf-part="workspaceOption"'); + // Every entry carries one compact secondary detail line: repositories use + // their real path, while assistants expose their product role instead of + // leaking the internal assistant workspace directory. + expect(component).toContain('workspace.rootPath?.trim()'); + expect(component).toContain('workspaceContext.primaryAssistantWorkspaceId'); + expect(component).toContain("'workspaceStrip.primaryAssistant'"); + expect(component).toContain("'workspaceStrip.personalAssistant'"); + expect(component).toContain('__workspace-option-detail'); + expect(stylesheet).toMatch( + /&__workspace-option-copy \{[\s\S]*?padding-block: calc\(var\(--bf-space-1\) \/ 2\);/, + ); + expect(stylesheet).toMatch( + /&__workspace-option-detail \{[\s\S]*?color: var\(--bf-color-content-muted\);/, + ); + expect(stylesheet).toMatch( + /&__workspace-option-detail \{[\s\S]*?opacity: 0\.5;/, + ); // Segments part on a hairline rule, never on a slash: a slash claimed a // path that a host, a workspace and a branch do not form. expect(component).toContain('__divider'); diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index 42a90d4195..9f928db0e0 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -604,7 +604,9 @@ "workspaceStrip": { "branchSwitchLabel": "Switch branch. Current branch: {{branch}}", "branchTooltipUnavailable": "Not a git repository or no current branch", - "branchTooltipUntrusted": "Git will not read this repository because the folder is owned by another user. Trust the folder to see the branch." + "branchTooltipUntrusted": "Git will not read this repository because the folder is owned by another user. Trust the folder to see the branch.", + "primaryAssistant": "Primary assistant", + "personalAssistant": "Personal assistant" }, "context": { "title": "Context", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index a4286435d5..25aaa13939 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -604,7 +604,9 @@ "workspaceStrip": { "branchSwitchLabel": "切换分支。当前分支:{{branch}}", "branchTooltipUnavailable": "非 Git 仓库或当前无分支", - "branchTooltipUntrusted": "该目录属于其他用户,Git 拒绝读取此仓库。信任该目录后即可看到分支。" + "branchTooltipUntrusted": "该目录属于其他用户,Git 拒绝读取此仓库。信任该目录后即可看到分支。", + "primaryAssistant": "主助理", + "personalAssistant": "个人助理" }, "context": { "title": "上下文", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index f44ef3799f..2abd3e6c79 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -604,7 +604,9 @@ "workspaceStrip": { "branchSwitchLabel": "切換分支。目前分支:{{branch}}", "branchTooltipUnavailable": "非 Git 存放庫或目前無分支", - "branchTooltipUntrusted": "該目錄屬於其他使用者,Git 拒絕讀取此存放庫。信任該目錄後即可看到分支。" + "branchTooltipUntrusted": "該目錄屬於其他使用者,Git 拒絕讀取此存放庫。信任該目錄後即可看到分支。", + "primaryAssistant": "主助理", + "personalAssistant": "個人助理" }, "context": { "title": "上下文", diff --git a/src/web-ui/src/tools/git/components/BranchQuickSwitch.test.tsx b/src/web-ui/src/tools/git/components/BranchQuickSwitch.test.tsx index 34bd36ef17..9290ba4184 100644 --- a/src/web-ui/src/tools/git/components/BranchQuickSwitch.test.tsx +++ b/src/web-ui/src/tools/git/components/BranchQuickSwitch.test.tsx @@ -174,6 +174,22 @@ describe('BranchQuickSwitch', () => { expect(input?.classList.contains('branch-quick-switch__input')).toBe(false); }); + it('keeps the selected current branch readable instead of applying disabled colors', async () => { + await act(async () => { + root.render(); + }); + await vi.waitFor(() => expect( + document.querySelector('[data-testid="branch-quick-switch-option-main"]'), + ).not.toBeNull()); + + const currentBranch = document.querySelector( + '[data-testid="branch-quick-switch-option-main"]', + ); + expect(currentBranch?.getAttribute('aria-selected')).toBe('true'); + expect(currentBranch?.dataset.bfState).toBe('current'); + expect(currentBranch?.disabled).toBe(false); + }); + it('checks out a selected branch and publishes the shared branch-change event', async () => { const onSwitchSuccess = vi.fn(); mocks.checkoutBranch.mockResolvedValue({ success: true }); diff --git a/src/web-ui/src/tools/git/components/BranchQuickSwitch.tsx b/src/web-ui/src/tools/git/components/BranchQuickSwitch.tsx index 3ed9e939ef..35ee4125db 100644 --- a/src/web-ui/src/tools/git/components/BranchQuickSwitch.tsx +++ b/src/web-ui/src/tools/git/components/BranchQuickSwitch.tsx @@ -495,8 +495,9 @@ export const BranchQuickSwitch: React.FC = ({ active={index === selectedIndex} className="branch-quick-switch__item" data-index={index} + data-bf-state={branch.current ? 'current' : undefined} data-testid={`branch-quick-switch-option-${branch.name}`} - disabled={branch.current || isSwitching} + disabled={isSwitching} indicator={switchingBranch === branch.name ? : undefined}