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
13 changes: 3 additions & 10 deletions wework/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@
"enable": true,
"scope": {
"requireLiteralLeadingDot": false,
"allow": [
"**/*"
]
"allow": ["**/*"]
}
}
}
Expand All @@ -55,13 +53,8 @@
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": [
"binaries/wegent-executor"
],
"resources": [
"binaries/codex/**/*",
"bundled-hooks/**/*"
],
"externalBin": ["binaries/wegent-executor"],
"resources": ["binaries/codex/**/*", "bundled-hooks/**/*"],
"android": {
"debugApplicationIdSuffix": ".debug"
}
Expand Down
9 changes: 2 additions & 7 deletions wework/src/components/chat/ChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2274,7 +2274,7 @@ describe('ChatInput', () => {
expect(menu.queryByText('计划模式')).not.toBeInTheDocument()
})

test('nests models by family while keeping controls from the selected GPT model', async () => {
test('lists models by family in the second-level menu while keeping selected controls', async () => {
const gptModel: UnifiedModel = {
name: 'overseas-gpt-5.5',
type: 'user',
Expand Down Expand Up @@ -2319,13 +2319,8 @@ describe('ChatInput', () => {
await userEvent.click(screen.getByTestId('model-selector-button'))
await userEvent.hover(screen.getByTestId('model-control-menu-model'))

expect(screen.getByTestId('model-family-claude')).toBeInTheDocument()
expect(screen.getByTestId('model-family-gpt')).toBeInTheDocument()
expect(screen.queryByTestId('model-option-claude-opus')).not.toBeInTheDocument()

await userEvent.hover(screen.getByTestId('model-family-gpt'))
expect(screen.getByTestId('model-option-claude-opus')).toBeInTheDocument()
expect(screen.getByTestId('model-option-overseas-gpt-5.5')).toBeInTheDocument()
expect(screen.queryByTestId('model-option-claude-opus')).not.toBeInTheDocument()
await userEvent.hover(screen.getByTestId('model-control-menu-reasoning'))
expect(screen.getByTestId('model-control-reasoning-high')).toBeInTheDocument()
expect(screen.queryByTestId('model-control-reasoning-auto')).not.toBeInTheDocument()
Expand Down
91 changes: 5 additions & 86 deletions wework/src/components/chat/composer/ModelSelector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ describe('ModelSelector desktop layout', () => {
expect(screen.queryByTestId('model-family-codex-official')).not.toBeInTheDocument()
})

test('shows family entries before model options when there are multiple families', async () => {
test('shows models from every family in the second-level menu', async () => {
createShellElement({ hidden: true })

render(
Expand All @@ -189,92 +189,11 @@ describe('ModelSelector desktop layout', () => {
fireEvent.click(screen.getByTestId('model-selector-button'))
fireEvent.mouseEnter(await screen.findByTestId('model-control-menu-model'))

expect(await screen.findByTestId('model-family-codex-official')).toHaveTextContent('我的CodeX')
expect(screen.getByTestId('model-family-claude')).toBeInTheDocument()
expect(screen.getByTestId('model-selector-submenu')).not.toHaveClass('min-h-48')
expect(screen.queryByTestId(`model-option-${SAMPLE_MODEL.name}`)).not.toBeInTheDocument()

fireEvent.mouseEnter(screen.getByTestId('model-family-claude'))

const familySubmenu = await screen.findByTestId('model-selector-family-submenu')
expect(familySubmenu).toHaveTextContent('Claude')
expect(familySubmenu).not.toHaveClass('min-h-48')
const submenu = await screen.findByTestId('model-selector-submenu')
expect(submenu).toHaveTextContent('我的CodeX')
expect(submenu).toHaveTextContent('Claude')
expect(screen.getByTestId(`model-option-${SECOND_FAMILY_MODEL.name}`)).toBeInTheDocument()
expect(screen.queryByTestId(`model-option-${SAMPLE_MODEL.name}`)).not.toBeInTheDocument()
})

test('keeps the family model submenu above the viewport bottom', async () => {
createShellElement({ hidden: true })
const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect
const originalScrollHeight = Object.getOwnPropertyDescriptor(
HTMLElement.prototype,
'scrollHeight'
)
HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
if (this.getAttribute('data-testid') === 'model-selector-menu') {
return {
top: 600,
left: 600,
right: 856,
bottom: 856,
width: 256,
height: 256,
x: 600,
y: 600,
toJSON: () => {},
} as DOMRect
}
if (this.getAttribute('data-testid') === 'model-selector-family-submenu') {
return {
top: 800,
left: 888,
right: 1176,
bottom: 1040,
width: 288,
height: 240,
x: 888,
y: 800,
toJSON: () => {},
} as DOMRect
}
return originalGetBoundingClientRect.call(this)
}
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
get() {
return this.getAttribute('data-testid') === 'model-selector-family-submenu' ? 240 : 0
},
})

try {
render(
<ModelSelector
models={[SAMPLE_MODEL, SECOND_FAMILY_MODEL]}
selectedModel={SAMPLE_MODEL}
selectedModelOptions={{}}
disabled={false}
onSelectModel={vi.fn()}
onSelectModelOption={vi.fn()}
/>
)

fireEvent.click(screen.getByTestId('model-selector-button'))
fireEvent.mouseEnter(await screen.findByTestId('model-control-menu-model'))
fireEvent.mouseEnter(await screen.findByTestId('model-family-claude'))

const familySubmenu = await screen.findByTestId('model-selector-family-submenu')
const wrapper = screen.getByTestId('model-selector-menu').parentElement
await waitFor(() => {
expect(
parseInt(wrapper!.style.top, 10) + parseInt(familySubmenu.style.top, 10) + 240
).toBeLessThanOrEqual(WINDOW_INNER_HEIGHT - 16)
})
} finally {
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect
if (originalScrollHeight) {
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', originalScrollHeight)
}
}
expect(screen.getByTestId(`model-option-${SAMPLE_MODEL.name}`)).toBeInTheDocument()
})

test('caps the trigger width when maxClosedWidth is provided', () => {
Expand Down
105 changes: 10 additions & 95 deletions wework/src/components/chat/composer/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ export function ModelSelector({
const desktopMenuWrapperRef = useRef<HTMLDivElement>(null)
const menuPanelRef = useRef<HTMLDivElement>(null)
const submenuPanelRef = useRef<HTMLDivElement>(null)
const familySubmenuPanelRef = useRef<HTMLDivElement>(null)
const mobileMenuRef = useRef<HTMLDivElement>(null)
const mobileCloseButtonRef = useRef<HTMLButtonElement>(null)
const modelButtonRef = useRef<HTMLButtonElement>(null)
Expand All @@ -113,9 +112,6 @@ export function ModelSelector({
const [activeDesktopSubmenu, setActiveDesktopSubmenu] = useState<DesktopSubmenuTarget | null>(
null
)
const [activeDesktopFamilyId, setActiveDesktopFamilyId] = useState<string | null>(null)
const [desktopFamilyOffset, setDesktopFamilyOffset] = useState(0)
const [desktopFamilyTop, setDesktopFamilyTop] = useState(0)
const [advancedOpen, setAdvancedOpen] = useState(readModelSelectorPowerViewPreference)
const [powerSliderInteracting, setPowerSliderInteracting] = useState(false)
const modelSelectorShortcut = useConfiguredKeybinding(TOGGLE_MODEL_SELECTOR_COMMAND)
Expand All @@ -140,7 +136,6 @@ export function ModelSelector({
setOpen(false)
setMobileQuery('')
setActiveDesktopSubmenu(null)
setActiveDesktopFamilyId(null)
setPowerSliderInteracting(false)
}, [setActiveDesktopSubmenu, setMobileQuery, setOpen, setPowerSliderInteracting])
const handleSelectModelOption = useCallback(
Expand Down Expand Up @@ -277,11 +272,9 @@ export function ModelSelector({
)
const activateModels = useCallback(() => {
setActiveDesktopSubmenu({ type: 'models' })
setActiveDesktopFamilyId(null)
}, [setActiveDesktopSubmenu, setActiveDesktopFamilyId])
}, [setActiveDesktopSubmenu])
const clearDesktopSubmenu = useCallback(() => {
setActiveDesktopSubmenu({ type: 'none' })
setActiveDesktopFamilyId(null)
}, [setActiveDesktopSubmenu])
const activateMobileFamily = useCallback(
(familyId: string) => {
Expand Down Expand Up @@ -395,39 +388,6 @@ export function ModelSelector({
activeDesktopSubmenu?.type === 'control'
? desktopControls.find(control => control.id === activeDesktopSubmenu.id)
: undefined
const activeDesktopFamily = activeDesktopFamilyId
? familyGroups.find(group => group.config.id === activeDesktopFamilyId)
: undefined
const desktopFamilySubmenuWidth = SUBMENU_WIDTH
const desktopFamilySubmenuLeft = (() => {
const modelSubmenuWidth = submenuWidth ?? SUBMENU_WIDTH
if (submenuLeft < 0) {
return submenuLeft - desktopFamilySubmenuWidth
}

const rightSideLeft = submenuLeft + modelSubmenuWidth
const rightSideEdge = desktopMenuLeft + rightSideLeft + desktopFamilySubmenuWidth
return rightSideEdge <= getDesktopViewportRightBoundary() - VIEWPORT_MARGIN
? rightSideLeft
: -desktopFamilySubmenuWidth
})()

useLayoutEffect(() => {
if (!open || isMobile || !activeDesktopFamily) return

const familySubmenu = familySubmenuPanelRef.current
if (!familySubmenu) return

const submenuHeight = Math.min(
familySubmenu.scrollHeight || familySubmenu.getBoundingClientRect().height,
SUBMENU_MAX_HEIGHT,
Math.max(0, window.innerHeight - SUBMENU_VIEWPORT_VERTICAL_GAP)
)
const preferredTop = submenuOffset + desktopFamilyOffset
const minTop = DESKTOP_MENU_VIEWPORT_TOP - desktopMenuTop
const maxTop = window.innerHeight - VIEWPORT_MARGIN - submenuHeight - desktopMenuTop
setDesktopFamilyTop(Math.round(Math.max(minTop, Math.min(preferredTop, maxTop))))
}, [activeDesktopFamily, desktopFamilyOffset, desktopMenuTop, isMobile, open, submenuOffset])

useLayoutEffect(() => {
if (!open || isMobile) return
Expand Down Expand Up @@ -1030,7 +990,7 @@ export function ModelSelector({
data-enter-animation="submenu"
style={{ top: submenuOffset, left: submenuLeft, width: submenuWidth }}
className={cn(
'absolute max-h-[min(28rem,calc(100vh-8rem))] w-72 overflow-y-auto rounded-2xl border border-border bg-background p-2 shadow-[0_16px_44px_rgba(0,0,0,0.16)]',
'absolute max-h-[min(28rem,calc(100vh-8rem))] min-h-48 w-72 overflow-y-auto rounded-2xl border border-border bg-background p-2 shadow-[0_16px_44px_rgba(0,0,0,0.16)]',
styles.submenu
)}
>
Expand All @@ -1040,59 +1000,14 @@ export function ModelSelector({
<div className="space-y-0.5">
{familyGroups.length <= 1
? renderDesktopModelOptions(desktopModels)
: familyGroups.map(group => {
const active = activeDesktopFamilyId === group.config.id
return (
<button
key={group.config.id}
type="button"
data-testid={`model-family-${group.config.id}`}
onMouseEnter={event => {
setActiveDesktopFamilyId(group.config.id)
setDesktopFamilyOffset(event.currentTarget.offsetTop)
}}
onPointerEnter={event => {
setActiveDesktopFamilyId(group.config.id)
setDesktopFamilyOffset(event.currentTarget.offsetTop)
}}
onFocus={event => {
setActiveDesktopFamilyId(group.config.id)
setDesktopFamilyOffset(event.currentTarget.offsetTop)
}}
onClick={event => {
setActiveDesktopFamilyId(group.config.id)
setDesktopFamilyOffset(event.currentTarget.offsetTop)
}}
className={cn(
'flex h-8 w-full items-center gap-2 rounded-lg px-3 text-left text-sm font-medium leading-[18px]',
active ? 'bg-muted text-text-primary' : 'hover:bg-muted'
)}
>
<span className="min-w-0 flex-1 truncate">{group.config.label}</span>
<ChevronRight className="h-4 w-4 shrink-0 text-text-muted" />
</button>
)
})}
</div>
</div>
) : null}
{activeDesktopSubmenu?.type === 'models' && activeDesktopFamily ? (
<div
key={`family:${activeDesktopFamily.config.id}`}
ref={familySubmenuPanelRef}
data-testid="model-selector-family-submenu"
data-enter-animation="submenu"
style={{ top: desktopFamilyTop, left: desktopFamilySubmenuLeft }}
className={cn(
'absolute max-h-[min(28rem,calc(100vh-8rem))] w-72 overflow-y-auto rounded-2xl border border-border bg-background p-2 shadow-[0_16px_44px_rgba(0,0,0,0.16)]',
styles.submenu
)}
>
<div className="px-3 pb-1.5 pt-0.5 text-sm font-semibold leading-[18px] text-text-muted">
{activeDesktopFamily.config.label}
</div>
<div className="space-y-0.5">
{renderDesktopModelOptions(activeDesktopFamily.models)}
: familyGroups.map(group => (
<div key={group.config.id} className="pb-1 last:pb-0">
<div className="px-3 pb-1 pt-2 text-xs font-medium text-text-muted first:pt-0">
{group.config.label}
</div>
{renderDesktopModelOptions(group.models)}
</div>
))}
</div>
</div>
) : null}
Expand Down
Loading