Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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 @@ -370,6 +371,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