Skip to content
Closed
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
5 changes: 5 additions & 0 deletions wework/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
import { AppshotBridge } from '@/features/appshots/AppshotBridge'
import { SystemDragPanel } from '@/features/system-drag/SystemDragPanel'
import { SystemDragBridge } from '@/features/system-drag/SystemDragBridge'
import { installMacOSInputArrowKeyGuard } from '@/lib/macosInputArrowKeyGuard'

const WORKBENCH_STARTUP_REVEAL_TIMEOUT_MS = 6000

Expand Down Expand Up @@ -377,6 +378,10 @@ function AppShell() {
}
}, [isTauri])

useEffect(() => {
return installMacOSInputArrowKeyGuard()
}, [])

useEffect(() => {
if (
path === '/login' ||
Expand Down
2 changes: 1 addition & 1 deletion wework/src/components/chat/ChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@ describe('ChatInput', () => {
'data-enter-animation',
'main'
)
expect(selectorButton).toHaveStyle({ width: '240px' })
expect(selectorButton).toHaveStyle({ width: 'var(--model-selector-width, auto)' })
expect(screen.queryByTestId('model-selector-tooltip')).not.toBeInTheDocument()
expect(screen.getByTestId('model-selector-menu').parentElement).toHaveClass(
'fixed',
Expand Down
138 changes: 138 additions & 0 deletions wework/src/components/chat/composer/ComposerToolbar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import type { UnifiedModel } from '@/types/api'

const isMobileMock = vi.hoisted(() => vi.fn())
vi.mock('@/hooks/useIsMobile', () => ({
useIsMobile: isMobileMock,
}))

const configuredKeybindingMock = vi.hoisted(() => vi.fn())
vi.mock('@/hooks/useConfiguredKeybinding', () => ({
useConfiguredKeybinding: configuredKeybindingMock,
}))

const modelExecutionMock = vi.hoisted(() => ({
getModelExecutionOverride: vi.fn(),
}))
vi.mock('@/features/cloud-connection/modelExecution', () => modelExecutionMock)

vi.mock('@/hooks/useTranslation', () => ({
useTranslation: () => ({
t: (key: string, fallback?: string) => fallback ?? key,
}),
}))

vi.mock('@/hooks/useQuickPhrases', () => ({
useQuickPhrases: () => [],
}))

vi.mock('@/tauri/appPreferences', () => ({
getAppPreferences: vi.fn(),
updateAppPreferences: vi.fn(),
}))

import { ComposerToolbar } from './ComposerToolbar'

const SAMPLE_MODEL: UnifiedModel = {
name: 'gpt-5.5',
modelId: 'gpt-5.5',
displayName: 'GPT 5.5',
type: 'runtime',
provider: 'local',
config: {
weworkModelKind: 'codex-official',
ui: { family: 'codex-official', controls: ['speed'] },
},
}

function mockToolbarWidth(width: number) {
const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect
HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
if (this.getAttribute('data-testid') === 'composer-toolbar') {
return {
top: 0,
left: 0,
right: width,
bottom: 40,
width,
height: 40,
x: 0,
y: 0,
toJSON: () => {},
} as DOMRect
}
return originalGetBoundingClientRect.call(this)
}
return () => {
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect
}
}

describe('ComposerToolbar', () => {
const originalResizeObserver = globalThis.ResizeObserver

beforeEach(() => {
isMobileMock.mockReturnValue(false)
configuredKeybindingMock.mockReturnValue(null)
modelExecutionMock.getModelExecutionOverride.mockReturnValue(undefined)
})

afterEach(() => {
cleanup()
vi.restoreAllMocks()
globalThis.ResizeObserver = originalResizeObserver
})

test('shows the QuickPhraseMenu label when toolbar is wide', () => {
const restoreRect = mockToolbarWidth(600)
globalThis.ResizeObserver = class ResizeObserverMock {
observe() {}
disconnect() {}
} as unknown as typeof ResizeObserver

render(
<ComposerToolbar
canSend={false}
models={[SAMPLE_MODEL]}
selectedModel={SAMPLE_MODEL}
selectedModelOptions={{}}
isModelSelectionReady
onSelectModel={() => {}}
onFileSelect={() => {}}
onQuickPhraseSelect={() => {}}
onSubmit={() => {}}
/>
)

const button = screen.getByTestId('quick-phrase-button')
expect(button).toHaveTextContent('快捷短语')
restoreRect()
})

test('collapses the QuickPhraseMenu label when toolbar is narrow', () => {
const restoreRect = mockToolbarWidth(475)
globalThis.ResizeObserver = class ResizeObserverMock {
observe() {}
disconnect() {}
} as unknown as typeof ResizeObserver

render(
<ComposerToolbar
canSend={false}
models={[SAMPLE_MODEL]}
selectedModel={SAMPLE_MODEL}
selectedModelOptions={{}}
isModelSelectionReady
onSelectModel={() => {}}
onFileSelect={() => {}}
onQuickPhraseSelect={() => {}}
onSubmit={() => {}}
/>
)

const button = screen.getByTestId('quick-phrase-button')
expect(button).not.toHaveTextContent('快捷短语')
restoreRect()
})
})
29 changes: 27 additions & 2 deletions wework/src/components/chat/composer/ComposerToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ArrowUp, ChevronDown, ClipboardList, Clock3, CornerDownRight, Zap } from 'lucide-react'
import { useLayoutEffect, useRef, useState } from 'react'
import { ActionMenu } from '@/components/common/ActionMenu'
import type { ComposerSubmitOptions } from './ComposerTextarea'
import { useTranslation } from '@/hooks/useTranslation'
Expand All @@ -10,6 +11,9 @@ import { ModelSelector } from './ModelSelector'
import { QuickPhraseMenu } from './QuickPhraseMenu'
import type { QuickPhrase } from '@/tauri/appPreferences'

const COMPACT_QUICK_PHRASE_MAX_WIDTH = 475
const NARROW_MODEL_SELECTOR_MAX_WIDTH = 160

interface ComposerToolbarProps {
canSend: boolean
disabled?: boolean
Expand Down Expand Up @@ -64,17 +68,37 @@ export function ComposerToolbar({
onSubmit,
}: ComposerToolbarProps) {
const { t } = useTranslation('common')
const toolbarRef = useRef<HTMLDivElement>(null)
const [isNarrow, setIsNarrow] = useState(false)

useLayoutEffect(() => {
const toolbar = toolbarRef.current
if (!toolbar) return undefined

const updateWidth = () => {
setIsNarrow(toolbar.getBoundingClientRect().width <= COMPACT_QUICK_PHRASE_MAX_WIDTH)
}

updateWidth()
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(updateWidth)
observer?.observe(toolbar)
return () => observer?.disconnect()
}, [])

return (
<div className="mt-auto flex min-h-8 items-center justify-between gap-3 pt-1">
<div
ref={toolbarRef}
data-testid="composer-toolbar"
className="mt-auto flex min-h-8 items-center justify-between gap-3 pt-1"
>
<div className="flex min-w-0 items-center gap-2">
<AddContextMenu
disabled={disabled}
onFileSelect={onFileSelect}
onSetPlanMode={planModeActive ? undefined : onSetPlanMode}
onSetGoal={onSetGoal}
/>
<QuickPhraseMenu disabled={disabled} onSelect={onQuickPhraseSelect} />
<QuickPhraseMenu disabled={disabled} iconOnly={isNarrow} onSelect={onQuickPhraseSelect} />
{goalDraftActive ? (
<GoalDraftPill onCancel={onCancelGoalDraft} />
) : planModeActive ? (
Expand Down Expand Up @@ -108,6 +132,7 @@ export function ComposerToolbar({
onSelectModelOption={onSelectModelOption}
onBlockedModelSelect={onBlockedModelSelect}
buttonClassName="opacity-90 hover:opacity-100"
maxClosedWidth={isNarrow ? NARROW_MODEL_SELECTOR_MAX_WIDTH : undefined}
/>
) : (
<div className="h-11 w-32 shrink-0" data-testid="model-selector-loading" />
Expand Down
Loading
Loading