fix(desktop): streamline panel and effort controls

This commit is contained in:
程序员阿江(Relakkes) 2026-07-13 18:33:48 +08:00
parent c5ad527034
commit 96fa164cb1
6 changed files with 44 additions and 22 deletions

View File

@ -44,16 +44,16 @@ describe('ReasoningEffortPopover', () => {
renderPopover()
const popover = screen.getByTestId('reasoning-effort-popover')
expect(popover).toHaveStyle({ width: '280px' })
expect(popover).toHaveClass('px-4', 'pb-4', 'pt-3.5')
expect(popover).toHaveStyle({ width: '240px' })
expect(popover).toHaveClass('px-3.5', 'pb-3.5', 'pt-3')
expect(popover.querySelectorAll('svg')).toHaveLength(0)
expect(screen.getByTestId('reasoning-effort-header')).toHaveClass('mb-3', 'justify-between')
expect(screen.getByTestId('reasoning-effort-label')).toHaveClass('text-[15px]')
expect(screen.getByTestId('reasoning-effort-context-label')).toHaveClass('text-[11px]')
expect(screen.getByTestId('reasoning-effort-header')).toHaveClass('mb-2.5', 'justify-between')
expect(screen.getByTestId('reasoning-effort-label')).toHaveClass('text-sm')
expect(screen.getByTestId('reasoning-effort-context-label')).toHaveClass('text-[10px]')
expect(screen.getByTestId('reasoning-effort-context-label')).toHaveTextContent('推理强度')
expect(screen.getByRole('slider', { name: '推理强度' })).toHaveClass('h-11')
expect(screen.getByTestId('reasoning-effort-track')).toHaveClass('h-[30px]')
expect(screen.getByTestId('reasoning-effort-thumb')).toHaveClass('h-10', 'w-10')
expect(screen.getByRole('slider', { name: '推理强度' })).toHaveClass('h-9')
expect(screen.getByTestId('reasoning-effort-track')).toHaveClass('h-6')
expect(screen.getByTestId('reasoning-effort-thumb')).toHaveClass('h-8', 'w-8')
})
it('renders every model-supported stop and exposes the selected localized value', () => {

View File

@ -20,7 +20,7 @@ type PopoverPosition = {
width: number
}
const POPOVER_WIDTH = 280
const POPOVER_WIDTH = 240
const VIEWPORT_MARGIN = 16
const POPOVER_GAP = 10
@ -107,22 +107,22 @@ export function ReasoningEffortPopover({
<div
ref={popoverRef}
data-testid="reasoning-effort-popover"
className="fixed z-[90] rounded-[18px] border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-4 pb-4 pt-3.5 shadow-[0_14px_36px_rgba(15,23,42,0.14)]"
className="fixed z-[90] rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-3.5 pb-3.5 pt-3 shadow-[0_12px_30px_rgba(15,23,42,0.13)]"
style={{ bottom: position.bottom, left: position.left, width: position.width }}
>
<div
data-testid="reasoning-effort-header"
className="mb-3 flex items-baseline justify-between gap-3"
className="mb-2.5 flex items-baseline justify-between gap-3"
>
<div
data-testid="reasoning-effort-label"
className="text-[15px] font-semibold text-[var(--color-text-secondary)]"
className="text-sm font-semibold text-[var(--color-text-secondary)]"
>
{labels[value]}
</div>
<div
data-testid="reasoning-effort-context-label"
className="text-[11px] font-medium tracking-wide text-[var(--color-text-tertiary)]"
className="text-[10px] font-medium tracking-wide text-[var(--color-text-tertiary)]"
>
{ariaLabel}
</div>
@ -137,7 +137,7 @@ export function ReasoningEffortPopover({
aria-valuemax={maxIndex}
aria-valuenow={selectedIndex}
aria-valuetext={labels[value]}
className="group relative flex h-11 touch-none cursor-pointer items-center outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)] focus-visible:ring-offset-4 focus-visible:ring-offset-[var(--color-surface-container-lowest)]"
className="group relative flex h-9 touch-none cursor-pointer items-center outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)] focus-visible:ring-offset-3 focus-visible:ring-offset-[var(--color-surface-container-lowest)]"
onClick={(event) => selectFromClientX(event.clientX)}
onPointerDown={(event) => {
draggingRef.current = true
@ -182,7 +182,7 @@ export function ReasoningEffortPopover({
>
<div
data-testid="reasoning-effort-track"
className="absolute inset-x-0 h-[30px] overflow-hidden rounded-full bg-[var(--color-surface-container-high)] shadow-[inset_0_0_0_1px_var(--color-border)]"
className="absolute inset-x-0 h-6 overflow-hidden rounded-full bg-[var(--color-surface-container-high)] shadow-[inset_0_0_0_1px_var(--color-border)]"
>
<div
data-testid="reasoning-effort-fill"
@ -191,12 +191,12 @@ export function ReasoningEffortPopover({
/>
</div>
<div className="absolute inset-x-0 flex items-center justify-between px-[22px]">
<div className="absolute inset-x-0 flex items-center justify-between px-[18px]">
{options.map((option, index) => (
<span
key={option}
data-testid="reasoning-effort-stop"
className={`h-2 w-2 rounded-full ${index <= selectedIndex ? 'bg-white/45' : 'bg-[var(--color-outline)]/55'}`}
className={`h-1.5 w-1.5 rounded-full ${index <= selectedIndex ? 'bg-white/45' : 'bg-[var(--color-outline)]/55'}`}
/>
))}
</div>
@ -204,7 +204,7 @@ export function ReasoningEffortPopover({
<div
aria-hidden="true"
data-testid="reasoning-effort-thumb"
className="absolute top-1/2 h-10 w-10 -translate-x-1/2 -translate-y-1/2 rounded-full border border-[var(--color-border)] bg-white shadow-[0_3px_9px_rgba(15,23,42,0.14)] transition-[left] duration-200 motion-reduce:transition-none"
className="absolute top-1/2 h-8 w-8 -translate-x-1/2 -translate-y-1/2 rounded-full border border-[var(--color-border)] bg-white shadow-[0_2px_7px_rgba(15,23,42,0.13)] transition-[left] duration-200 motion-reduce:transition-none"
style={{ left: `${fillPercent}%` }}
/>
</div>

View File

@ -295,11 +295,12 @@ describe('TabBar', () => {
expect(screen.queryByTestId('session-activity-badge')).not.toBeInTheDocument()
})
it('shows the activity button for completed TodoWrite history without a badge', async () => {
it('shows the activity button for completed TodoWrite history and hides it while the workspace is open', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useSessionStore } = await import('../../stores/sessionStore')
const { useWorkspacePanelStore } = await import('../../stores/workspacePanelStore')
const sessionId = 'session-1'
const chatSession = makeChatSession('idle')
chatSession.messages = [completedTodoWriteMessage()]
@ -324,6 +325,12 @@ describe('TabBar', () => {
expect(screen.getByRole('button', { name: /activity/i })).toBeInTheDocument()
expect(screen.queryByTestId('session-activity-badge')).not.toBeInTheDocument()
act(() => {
useWorkspacePanelStore.getState().openPanel(sessionId)
})
expect(screen.queryByRole('button', { name: /activity/i })).not.toBeInTheDocument()
})
it('shows the activity button without a numeric badge for running or failed activity', async () => {

View File

@ -141,7 +141,7 @@ export function TabBar() {
hasVisibleActivity: hasVisibleSessionActivity(model),
}
}))
const showActivityButton = activeTabId && activityState.hasVisibleActivity
const showActivityButton = activeTabId && activityState.hasVisibleActivity && !isWorkbenchOpen
const moveTab = useTabStore((s) => s.moveTab)
const scrollRef = useRef<HTMLDivElement>(null)

View File

@ -468,7 +468,7 @@ describe('ActiveSession task polling', () => {
expect(screen.queryByTestId('background-tasks-button')).not.toBeInTheDocument()
})
it('renders the activity panel inside the chat column with session activity rows', () => {
it('renders the activity panel as a rail and hides it when the workspace opens', async () => {
const sessionId = 'activity-panel-open-session'
useCLITaskStore.setState({
@ -577,6 +577,16 @@ describe('ActiveSession task polling', () => {
expect(chatColumn).toContainElement(screen.getByTestId('chat-input'))
expect(screen.queryByTestId('session-task-bar')).not.toBeInTheDocument()
expect(screen.queryByTestId('background-tasks-button')).not.toBeInTheDocument()
act(() => {
useWorkspacePanelStore.getState().openPanel(sessionId)
})
expect(screen.getByTestId('workbench-panel')).toBeInTheDocument()
expect(screen.queryByTestId('session-activity-panel')).not.toBeInTheDocument()
await waitFor(() => {
expect(useActivityPanelStore.getState().isOpen(sessionId)).toBe(false)
})
})
it('does not render the activity panel when the store is open without visible activity', async () => {

View File

@ -477,6 +477,11 @@ export function ActiveSession() {
closeActivityPanel(activeTabId)
}, [activeTabId, closeActivityPanel, hasVisibleActivity, isActivityPanelOpen])
useEffect(() => {
if (!activeTabId || !showWorkbench || !isActivityPanelOpen) return
closeActivityPanel(activeTabId)
}, [activeTabId, closeActivityPanel, isActivityPanelOpen, showWorkbench])
const handleOpenSubagentRun = useCallback((payload: { sessionId: string; toolUseId: string; title: string }) => {
useTabStore.getState().openSubagentTab(payload.sessionId, payload.toolUseId, payload.title)
}, [])
@ -725,7 +730,7 @@ export function ActiveSession() {
) : null}
</div>
{activityModel && hasVisibleActivity && !isMobileLayout && !isMemberSession && isSessionTabState(activeTabId, activeTabType) ? (
{activityModel && hasVisibleActivity && !showWorkbench && !isMobileLayout && !isMemberSession && isSessionTabState(activeTabId, activeTabType) ? (
<SessionActivityPanel
model={activityModel}
open={isActivityPanelOpen}