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
1 change: 1 addition & 0 deletions wework/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Any Wework UI, Tauri command, local-runtime, IPC, or desktop integration behavio
pnpm --filter wework ai:verify start
pnpm --filter wework ai:verify snapshot --session <session-path>
pnpm --filter wework ai:verify click --session <session-path> --selector '[data-testid="..."]'
pnpm --filter wework ai:verify drag --session <session-path> --selector '[data-testid="..."]' --target '[data-testid="..."]'
pnpm --filter wework ai:verify fill --session <session-path> --selector '[data-testid="..."]' --value '...'
pnpm --filter wework ai:verify hover --session <session-path> --selector '[data-testid="..."]'
pnpm --filter wework ai:verify pointer-move --session <session-path> --selector 'body'
Expand Down
4 changes: 3 additions & 1 deletion wework/scripts/ai-verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ const corsHeaders = {
function usage() {
console.error(`Usage:
pnpm --filter wework ai:verify start
pnpm --filter wework ai:verify <capture|snapshot|click|close-to-tray|fill|hover|pointer-move|press|select-text|wait-for|text|status|stop> --session PATH [options]
pnpm --filter wework ai:verify <capture|snapshot|click|close-to-tray|drag|fill|hover|pointer-move|press|select-text|wait-for|text|status|stop> --session PATH [options]

Options:
--selector CSS_SELECTOR Target selector (required by click, fill, press and wait-for)
--value TEXT Replacement value for fill
--target SELECTOR Event target selector for pointer-move (default: body)
Required destination selector for drag
--key KEY Keyboard key for press
--output PATH PNG output path for capture
--text TEXT Expected text for wait-for
Expand Down Expand Up @@ -310,6 +311,7 @@ async function main() {
snapshot: 'snapshot',
click: 'click',
'close-to-tray': 'closeMainWindowToTray',
drag: 'drag',
fill: 'fill',
hover: 'hover',
'pointer-move': 'pointerMove',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const BottomWorkspacePanel = memo(function BottomWorkspacePanel({
onTerminalTabsEmpty,
}: BottomWorkspacePanelProps) {
const { t } = useTranslation('common')
const { height, resizing, handleResizeStart } = useResizableBottomPanel()
const { height, resizing, panelRef, handleResizeStart } = useResizableBottomPanel()
const terminalSequenceRef = useRef(2)
const [tabs, setTabs] = useState<BottomWorkspacePanelTab[]>(() => [createTerminalTab(1)])
const [activeTabId, setActiveTabId] = useState('terminal-1')
Expand Down Expand Up @@ -120,6 +120,7 @@ export const BottomWorkspacePanel = memo(function BottomWorkspacePanel({

return (
<section
ref={panelRef}
data-testid={testId('bottom-workspace-panel')}
className={cn(
'relative flex shrink-0 flex-col overflow-hidden ease-out',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import { useResizableBottomPanel } from './useResizableWorkspacePanel'

describe('useResizableBottomPanel', () => {
test('resizes the panel imperatively without rerendering terminal content while dragging', () => {
const frames: FrameRequestCallback[] = []
const requestAnimationFrameSpy = vi
.spyOn(window, 'requestAnimationFrame')
.mockImplementation(callback => {
frames.push(callback)
return frames.length
})
const cancelAnimationFrameSpy = vi
.spyOn(window, 'cancelAnimationFrame')
.mockImplementation(() => undefined)
let renderCount = 0

function Harness() {
const { height, resizing, panelRef, handleResizeStart } = useResizableBottomPanel()
renderCount += 1

return (
<section ref={panelRef} data-testid="panel" style={{ height }} data-resizing={resizing}>
<div data-testid="handle" onPointerDown={handleResizeStart} />
</section>
)
}

const { unmount } = render(<Harness />)
fireEvent.pointerDown(screen.getByTestId('handle'), { clientY: 700 })
expect(renderCount).toBe(2)

fireEvent.pointerMove(document, { clientY: 660 })
fireEvent.pointerMove(document, { clientY: 620 })
expect(frames).toHaveLength(1)

act(() => frames[0](performance.now()))
expect(screen.getByTestId('panel')).toHaveStyle({ height: '400px' })
expect(renderCount).toBe(2)

fireEvent.pointerUp(document)
expect(renderCount).toBe(3)
expect(screen.getByTestId('panel')).toHaveStyle({ height: '400px' })

fireEvent.pointerDown(screen.getByTestId('handle'), { clientY: 620 })
expect(document.body.style.cursor).toBe('row-resize')
unmount()
expect(document.body.style.cursor).toBe('')
expect(document.body.style.userSelect).toBe('')

requestAnimationFrameSpy.mockRestore()
cancelAnimationFrameSpy.mockRestore()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,13 @@ export function useResizableRightSplitChat({
export function useResizableBottomPanel() {
const [height, setHeight] = useState(BOTTOM_DEFAULT_HEIGHT)
const [resizing, setResizing] = useState(false)
const panelRef = useRef<HTMLElement | null>(null)
const resizeFrameRef = useRef<number | null>(null)
const activeResizeCleanupRef = useRef<(() => void) | null>(null)

useEffect(() => {
return () => {
activeResizeCleanupRef.current?.()
if (resizeFrameRef.current !== null) {
window.cancelAnimationFrame(resizeFrameRef.current)
}
Expand All @@ -156,11 +159,28 @@ export function useResizableBottomPanel() {

const handleResizeStart = (event: PointerEvent<HTMLDivElement>) => {
event.preventDefault()
activeResizeCleanupRef.current?.()

const resizeHandle = event.currentTarget
if (typeof resizeHandle.setPointerCapture === 'function') {
try {
resizeHandle.setPointerCapture(event.pointerId)
} catch {
// Synthetic verification events do not create an active browser pointer.
}
}

const startY = event.clientY
const startHeight = height
let nextHeight = startHeight

const applyHeight = () => {
resizeFrameRef.current = null
if (panelRef.current) {
panelRef.current.style.height = `${nextHeight}px`
}
}

const handleMove = (moveEvent: globalThis.PointerEvent) => {
nextHeight = clamp(
startHeight + startY - moveEvent.clientY,
Expand All @@ -169,31 +189,45 @@ export function useResizableBottomPanel() {
)
if (resizeFrameRef.current !== null) return

resizeFrameRef.current = window.requestAnimationFrame(() => {
resizeFrameRef.current = null
setHeight(nextHeight)
})
resizeFrameRef.current = window.requestAnimationFrame(applyHeight)
}

const handleUp = () => {
const cleanupResize = () => {
document.removeEventListener('pointermove', handleMove)
document.removeEventListener('pointerup', handleUp)
document.removeEventListener('pointercancel', handleCancel)
if (resizeFrameRef.current !== null) {
window.cancelAnimationFrame(resizeFrameRef.current)
resizeFrameRef.current = null
}
setHeight(nextHeight)
setResizing(false)
if (resizeHandle.hasPointerCapture?.(event.pointerId)) {
resizeHandle.releasePointerCapture(event.pointerId)
}
document.body.style.cursor = ''
document.body.style.userSelect = ''
activeResizeCleanupRef.current = null
}

const finishResize = () => {
cleanupResize()
if (panelRef.current) {
panelRef.current.style.height = `${nextHeight}px`
}
setHeight(nextHeight)
setResizing(false)
}

const handleUp = () => finishResize()
const handleCancel = () => finishResize()

setResizing(true)
document.body.style.cursor = 'row-resize'
document.body.style.userSelect = 'none'
document.addEventListener('pointermove', handleMove)
document.addEventListener('pointerup', handleUp)
document.addEventListener('pointercancel', handleCancel)
activeResizeCleanupRef.current = cleanupResize
}

return { height, resizing, handleResizeStart }
return { height, resizing, panelRef, handleResizeStart }
}
20 changes: 19 additions & 1 deletion wework/src/e2e/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type DesktopControlAction =
| 'clickWhenEnabled'
| 'closeMainWindowToTray'
| 'dispatchLocalModelSettingsChanged'
| 'drag'
| 'fill'
| 'getText'
| 'hover'
Expand Down Expand Up @@ -341,7 +342,7 @@ function desktopControlEventOptions(element: HTMLElement): MouseEventInit & Poin
}

function dispatchDesktopControlPointerEvent(
element: HTMLElement,
element: EventTarget,
type: string,
options: MouseEventInit & PointerEventInit
) {
Expand Down Expand Up @@ -373,6 +374,21 @@ function moveDesktopControlPointer(command: DesktopControlCommand): string {
return element.textContent?.trim() ?? ''
}

function dragDesktopControlElement(command: DesktopControlCommand): string {
const element = findDesktopControlElements(command.selector)[0]
if (!element) throw new Error(`Unable to find selector "${command.selector}"`)
if (!command.target) throw new Error('Drag requires a target selector')
const target = findDesktopControlElements(command.target)[0]
if (!target) throw new Error(`Unable to find target selector "${command.target}"`)

const startOptions = { ...desktopControlEventOptions(element), buttons: 1 }
const endOptions = { ...desktopControlEventOptions(target), buttons: 1 }
dispatchDesktopControlPointerEvent(element, 'pointerdown', startOptions)
dispatchDesktopControlPointerEvent(document, 'pointermove', endOptions)
dispatchDesktopControlPointerEvent(document, 'pointerup', { ...endOptions, buttons: 0 })
return element.textContent?.trim() ?? ''
}

async function waitForDesktopControlElement(command: DesktopControlCommand): Promise<string> {
const timeoutMs = command.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS
const startedAt = Date.now()
Expand Down Expand Up @@ -474,6 +490,8 @@ async function executeDesktopControlCommand(command: DesktopControlCommand): Pro
case 'dispatchLocalModelSettingsChanged':
window.dispatchEvent(new CustomEvent(LOCAL_MODEL_SETTINGS_CHANGED_EVENT))
return ''
case 'drag':
return dragDesktopControlElement(command)
case 'waitFor':
return waitForDesktopControlElement(command)
case 'getText':
Expand Down
Loading