Skip to content
Merged
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
11 changes: 9 additions & 2 deletions wework/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import { AppUpdateTitlebarButton } from '@/components/topnav/AppUpdateTitlebarBu
import { LocalRuntimeInitializer } from '@/features/local-runtime/LocalRuntimeInitializer'
import { CloudConnectionProvider } from '@/features/cloud-connection/CloudConnectionProvider'
import { LocalExecutorCloudBridge } from '@/features/cloud-connection/LocalExecutorCloudBridge'
import { useDesktopSidebarCollapsed } from '@/components/layout/useDesktopSidebarCollapsed'
import {
requestDesktopSidebarToggle,
useDesktopSidebarCollapsed,
} from '@/components/layout/useDesktopSidebarCollapsed'
import { DESKTOP_TOP_BAR_BUTTON_CLASS } from '@/components/layout/DesktopTopBar'
import { useTranslation } from '@/hooks/useTranslation'
import { cn } from '@/lib/utils'
Expand Down Expand Up @@ -208,7 +211,11 @@ function TitlebarSidebarToggle() {
<button
type="button"
data-testid={sidebarCollapsed ? 'expand-sidebar-button' : 'collapse-sidebar-button'}
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
onClick={() => {
if (!requestDesktopSidebarToggle()) {
setSidebarCollapsed(!sidebarCollapsed)
}
}}
className={DESKTOP_TOP_BAR_BUTTON_CLASS}
title={label}
aria-label={label}
Expand Down
109 changes: 98 additions & 11 deletions wework/src/components/chat/ChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1259,12 +1259,16 @@ describe('ChatInput', () => {
await userEvent.click(screen.getByTestId('model-selector-button'))

expect(screen.getByTestId('model-selector-menu')).toBeInTheDocument()
expect(screen.getByTestId('model-selector-menu').parentElement).toHaveClass('right-0', 'w-64')
expect(screen.getByTestId('model-selector-submenu')).toBeInTheDocument()
expect(screen.getByTestId('model-family-gpt')).toBeInTheDocument()
expect(screen.getByTestId('model-family-gpt')).toHaveTextContent('海外:gpt-5.5')
expect(screen.getByTestId('model-selector-submenu')).toHaveStyle({ left: '256px' })
expect(screen.getByTestId('model-control-reasoning-high')).toBeInTheDocument()
expect(screen.getByTestId('model-control-collaborationMode-default')).toBeInTheDocument()
expect(screen.getByTestId('model-control-collaborationMode-plan')).toBeInTheDocument()
expect(screen.getByTestId('model-control-speed-fast')).toBeInTheDocument()
expect(screen.queryByTestId('model-control-collaborationMode-default')).not.toBeInTheDocument()
expect(screen.queryByTestId('model-control-collaborationMode-plan')).not.toBeInTheDocument()
expect(screen.getByTestId('model-control-menu-speed')).toBeInTheDocument()
expect(screen.queryByTestId('model-control-speed-fast')).not.toBeInTheDocument()
expect(screen.queryByTestId('model-option-default')).not.toBeInTheDocument()
expect(screen.getByTestId('model-selector-button')).toHaveTextContent('海外:gpt-5.5 High')
const modelOption = screen.getByTestId('model-option-overseas-gpt-5.5')
Expand All @@ -1283,6 +1287,91 @@ describe('ChatInput', () => {
expect(setSelectedModel).toHaveBeenCalledWith(model)
})

test('opens desktop speed options from a collapsed model control submenu', async () => {
const model: UnifiedModel = {
name: 'overseas-gpt-5.5',
type: 'user',
displayName: '海外:gpt-5.5',
config: {
ui: {
family: 'gpt',
region: 'overseas',
modelLabel: 'gpt-5.5',
sortOrder: 10,
controls: ['speed'],
},
},
}
const setSelectedModelOption = vi.fn()
render(
<ChatInput
value=""
onChange={vi.fn()}
onSubmit={vi.fn()}
disabled={false}
variant="desktop"
projectChat={projectChatControls({
models: [model],
selectedModel: model,
selectedModelOptions: { reasoning: 'high', speed: 'standard' },
setSelectedModelOption,
})}
/>
)

await userEvent.click(screen.getByTestId('model-selector-button'))

expect(screen.queryByTestId('model-control-speed-fast')).not.toBeInTheDocument()

await userEvent.hover(screen.getByTestId('model-control-menu-speed'))

expect(screen.getByTestId('model-control-speed-standard')).toBeInTheDocument()
expect(screen.getByTestId('model-control-speed-fast')).toBeInTheDocument()
expect(screen.getByTestId('model-selector-submenu')).toHaveStyle({ left: '256px' })

await userEvent.click(screen.getByTestId('model-control-speed-fast'))

expect(setSelectedModelOption).toHaveBeenCalledWith('speed', 'fast')
})

test('hides the desktop model submenu after the pointer leaves the menu', async () => {
const model: UnifiedModel = {
name: 'overseas-gpt-5.5',
type: 'user',
displayName: '海外:gpt-5.5',
config: {
ui: {
family: 'gpt',
region: 'overseas',
modelLabel: 'gpt-5.5',
sortOrder: 10,
},
},
}
render(
<ChatInput
value=""
onChange={vi.fn()}
onSubmit={vi.fn()}
disabled={false}
variant="desktop"
projectChat={projectChatControls({
models: [model],
selectedModel: model,
selectedModelOptions: { reasoning: 'high' },
})}
/>
)

await userEvent.click(screen.getByTestId('model-selector-button'))

expect(screen.getByTestId('model-selector-submenu')).toBeInTheDocument()

fireEvent.mouseLeave(screen.getByTestId('model-selector-menu').parentElement as HTMLElement)

expect(screen.queryByTestId('model-selector-submenu')).not.toBeInTheDocument()
})

test('keeps the desktop model menu in narrow Tauri windows', async () => {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
Expand Down Expand Up @@ -1587,7 +1676,7 @@ describe('ChatInput', () => {
})
})

test('selects Codex plan mode from the desktop model menu', async () => {
test('omits Codex plan mode from the desktop model menu', async () => {
const model: UnifiedModel = {
name: 'codex-gpt-5.5',
type: 'user',
Expand All @@ -1601,7 +1690,6 @@ describe('ChatInput', () => {
},
},
}
const setSelectedModelOption = vi.fn()
render(
<ChatInput
value=""
Expand All @@ -1613,18 +1701,17 @@ describe('ChatInput', () => {
models: [model],
selectedModel: model,
selectedModelOptions: { reasoning: 'high' },
setSelectedModelOption,
})}
/>
)

await userEvent.click(screen.getByTestId('model-selector-button'))
await userEvent.click(screen.getByTestId('model-control-collaborationMode-plan'))

expect(setSelectedModelOption).toHaveBeenCalledWith('collaborationMode', 'plan')
await waitFor(() => {
expect(screen.queryByTestId('model-selector-menu')).not.toBeInTheDocument()
})
const menu = within(screen.getByTestId('model-selector-menu'))
expect(screen.queryByTestId('model-control-collaborationMode-default')).not.toBeInTheDocument()
expect(screen.queryByTestId('model-control-collaborationMode-plan')).not.toBeInTheDocument()
expect(menu.queryByText('运行模式')).not.toBeInTheDocument()
expect(menu.queryByText('计划模式')).not.toBeInTheDocument()
})

test('keeps reasoning controls for the selected GPT model while hovering another family', async () => {
Expand Down
59 changes: 54 additions & 5 deletions wework/src/components/chat/FileChangesCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,13 @@ describe('FileChangesCard', () => {
act(() => vi.advanceTimersByTime(1))
const preview = screen.getByTestId('file-change-diff-preview')
expect(preview).toHaveAttribute('data-placement', 'below')
expect(preview).toHaveStyle({ width: '640px' })
expect(preview).toHaveClass('fixed', 'z-[9999]', 'pointer-events-auto', 'select-text')
expect(preview).not.toHaveClass('pointer-events-none')
expect(preview).toHaveTextContent('github-pr-flow.md')
expect(preview.firstElementChild).not.toHaveTextContent('references/')
expect(preview.lastElementChild).toHaveClass('max-h-[min(24rem,calc(100vh-9rem))]')
expect(preview.firstElementChild).toHaveTextContent(
'/workspace/project/references/github-pr-flow.md'
)
expect(preview).toHaveTextContent('+3')
expect(preview).toHaveTextContent('-0')
expect(preview).toHaveTextContent('After the PR is created')
Expand All @@ -153,7 +156,7 @@ describe('FileChangesCard', () => {
}
})

test('shows only the file name in the diff preview header for absolute paths', () => {
test('shows the absolute file path in the diff preview header', () => {
vi.useFakeTimers()
try {
renderCard({
Expand Down Expand Up @@ -184,8 +187,54 @@ describe('FileChangesCard', () => {
act(() => vi.advanceTimersByTime(500))

const previewHeader = screen.getByTestId('file-change-diff-preview').firstElementChild
expect(previewHeader).toHaveTextContent('SKILL.md')
expect(previewHeader).not.toHaveTextContent('/Users/crystal/dev/git')
expect(previewHeader).toHaveTextContent(
'/Users/crystal/dev/git/if/skills/wegent-dev/SKILL.md'
)
} finally {
vi.useRealTimers()
}
})

test('keeps more than the first fourteen diff lines available in the scroll preview', () => {
vi.useFakeTimers()
try {
renderCard({
summary: {
...summary,
file_count: 1,
additions: 20,
deletions: 0,
files: [
{
path: 'executor/src/runtime_work/mod.rs',
change_type: 'modified',
additions: 20,
deletions: 0,
binary: false,
},
],
diff: [
'@@ -1,2 +1,22 @@',
' fn demo() {',
...Array.from(
{ length: 20 },
(_, index) => `+ let value_${index + 1} = ${index + 1};`
),
' }',
].join('\n'),
},
})

fireEvent.pointerEnter(screen.getByTestId('file-change-trigger'))
act(() => vi.advanceTimersByTime(500))

const preview = screen.getByTestId('file-change-diff-preview')
expect(preview.firstElementChild).toHaveTextContent(
'/workspace/project/executor/src/runtime_work/mod.rs'
)
expect(preview).toHaveTextContent('let value_1 = 1;')
expect(preview).toHaveTextContent('let value_20 = 20;')
expect(preview).not.toHaveTextContent('...')
} finally {
vi.useRealTimers()
}
Expand Down
25 changes: 20 additions & 5 deletions wework/src/components/chat/FileChangesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { parseUnifiedDiff } from './parseUnifiedDiff'
const DEFAULT_VISIBLE_FILE_COUNT = 3
const DIFF_PREVIEW_CLOSE_DELAY_MS = 140
const DIFF_PREVIEW_DELAY_MS = 500
const DIFF_PREVIEW_ESTIMATED_HEIGHT = 340
const DIFF_PREVIEW_MAX_LINES = 14
const DIFF_PREVIEW_MAX_WIDTH = 704
const DIFF_PREVIEW_ESTIMATED_HEIGHT = 424
const DIFF_PREVIEW_MAX_LINES = 240
const DIFF_PREVIEW_MAX_WIDTH = 640
const DIFF_PREVIEW_VIEWPORT_GUTTER = 32
const DIFF_PREVIEW_VERTICAL_GAP = 8

Expand Down Expand Up @@ -236,6 +236,7 @@ function basename(path: string): string {

interface DiffPreview {
path: string
displayPath: string
additions: number
deletions: number
lines: DiffPreviewLine[]
Expand Down Expand Up @@ -382,11 +383,13 @@ function FileChangeDiffPreview({
onPointerLeave={onPointerLeave}
>
<div className="flex h-10 items-center gap-3 border-b border-border px-4 text-[13px] font-semibold">
<span className="min-w-0 flex-1 truncate">{basename(preview.path)}</span>
<span className="min-w-0 flex-1 truncate" title={preview.displayPath}>
{preview.displayPath}
</span>
<span className="shrink-0 font-medium text-green-400">+{preview.additions}</span>
<span className="shrink-0 font-medium text-red-400">-{preview.deletions}</span>
</div>
<div className="max-h-[18rem] overflow-auto py-1 font-mono text-[12px] leading-5">
<div className="max-h-[min(24rem,calc(100vh-9rem))] overflow-auto py-1 font-mono text-[12px] leading-5">
{preview.lines.map(line =>
line.type === 'separator' ? (
<div key={line.key} className="my-1 h-px bg-border" />
Expand Down Expand Up @@ -440,13 +443,25 @@ function buildFileDiffPreview(

return {
path: file.path,
displayPath: absoluteFilePath(summary.workspace_path, file.path),
additions: file.additions,
deletions: file.deletions,
lines: lines.slice(0, DIFF_PREVIEW_MAX_LINES),
truncated: lines.length > DIFF_PREVIEW_MAX_LINES,
}
}

function absoluteFilePath(workspacePath: string, filePath: string): string {
if (isAbsolutePath(filePath)) return filePath
const normalizedWorkspacePath = workspacePath.replace(/[\\/]+$/, '')
if (!normalizedWorkspacePath) return filePath
return `${normalizedWorkspacePath}/${filePath.replace(/^[\\/]+/, '')}`
}

function isAbsolutePath(path: string): boolean {
return /^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(path)
}

function fileDiffLines(file: TurnFileChangeItem, summary: TurnFileChangesSummary): string[] {
const diff = summary.diff?.trimEnd()
if (!diff) return []
Expand Down
41 changes: 39 additions & 2 deletions wework/src/components/chat/MessageList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,33 @@ describe('MessageList', () => {
expect(screen.queryByText(/proposed_plan/)).not.toBeInTheDocument()
})

test('renders Codex plan implementation user messages as the chosen option label', () => {
render(
<MessageList
messages={[
{
id: 'user-plan-implementation',
role: 'user',
content: [
'PLEASE IMPLEMENT THIS PLAN:',
'# Wework 输入框 Codex 化视觉优化计划',
'',
'## Summary',
'- 保持输入框视觉与 Codex App 一致。',
].join('\n'),
status: 'done',
createdAt: '2026-06-11T10:00:00Z',
},
]}
/>
)

expect(screen.getByTestId('user-message-content')).toHaveTextContent('是的,执行此计划')
expect(screen.queryByText(/PLEASE IMPLEMENT THIS PLAN/)).not.toBeInTheDocument()
expect(screen.queryByText(/Wework 输入框 Codex 化视觉优化计划/)).not.toBeInTheDocument()
expect(screen.queryByTestId('toggle-user-message-button')).not.toBeInTheDocument()
})

test('renders answered request user input as an assistant question summary', () => {
render(
<MessageList
Expand Down Expand Up @@ -256,7 +283,11 @@ describe('MessageList', () => {
/>
)

expect(screen.getByTestId('assistant-plan-card')).toHaveTextContent('计划')
const planCard = screen.getByTestId('assistant-plan-card')
expect(planCard).toHaveTextContent('计划')
expect(planCard).toHaveAttribute('role', 'button')
expect(screen.getByTestId('assistant-plan-card-preview').className).toContain('max-h-[168px]')
expect(screen.getByTestId('assistant-plan-card-content').className).toContain('text-sm')
expect(screen.queryByText('套餐')).not.toBeInTheDocument()
expect(screen.queryByTestId('assistant-plan-like-button')).not.toBeInTheDocument()
expect(screen.queryByTestId('assistant-plan-dislike-button')).not.toBeInTheDocument()
Expand All @@ -265,10 +296,16 @@ describe('MessageList', () => {
expect.stringContaining('Wegent 代码质量与前端一致性巡检计划')
)
expect(await screen.findByTestId('assistant-plan-copy-success')).toHaveTextContent('已复制')
await user.click(screen.getByTestId('assistant-plan-expand-button'))
expect(onOpenAssistantPlan).not.toHaveBeenCalled()
await user.click(planCard)
expect(onOpenAssistantPlan).toHaveBeenCalledWith(
expect.stringContaining('Wegent 代码质量与前端一致性巡检计划')
)
await user.click(screen.getByTestId('assistant-plan-expand-button'))
expect(onOpenAssistantPlan).toHaveBeenLastCalledWith(
expect.stringContaining('Wegent 代码质量与前端一致性巡检计划')
)
expect(onOpenAssistantPlan).toHaveBeenCalledTimes(2)
expect(screen.queryByTestId('assistant-plan-reading-panel')).not.toBeInTheDocument()
})

Expand Down
Loading
Loading