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
6 changes: 6 additions & 0 deletions docs/en/user-guide/wework-automations.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,11 @@ and executed by the current device's local Executor.
later run to that task. Regular history and a task that is merely open are
not selected automatically; pin the task in the sidebar first.

## Viewing details

When **Automations** opens, Wework displays the first task after the initial
load. After you close the detail pane with its top-right close button, the pane
stays closed. Select a task in the list to open its details again.

Automations run unattended by default. If Codex requests user input, Wework does
not guess an answer and marks the run as **Needs attention**.
5 changes: 5 additions & 0 deletions docs/zh/user-guide/wework-automations.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,10 @@ Wework 自动化可以按固定时间、固定间隔或指定时间启动 Codex
- **每次新建任务**:每次运行创建独立的 Codex 任务,适合日报、巡检和定期分析。
- **现有任务**:从已固定、可继续的本地任务中选择一个目标,后续运行会持续向该任务追加消息。普通历史任务和仅在当前页面打开的任务不会自动成为候选;需要先在侧边栏将任务置顶。

## 查看详情

打开“已安排”时,Wework 会在首次加载后显示第一条任务的详情。点击详情右上角的关闭
按钮后,详情区域会保持关闭;需要再次查看时,点击列表中的任务即可重新打开。

自动化任务默认无人值守执行。若 Codex 请求人工输入,Wework 不会猜测答案,而是将
运行标记为“需要关注”。
120 changes: 120 additions & 0 deletions wework/src/pages/AutomationsPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import type { Automation } from '@/types/automation'
import { AutomationsPage } from './AutomationsPage'

const automation: Automation = {
id: 'local:daily-reminder',
version: 1,
source: 'local',
name: 'Daily reminder',
description: '',
prompt: 'Say hello',
schedule: { type: 'cron', expression: '0 9 * * *' },
timezone: 'Asia/Shanghai',
enabled: true,
conversationMode: 'independent',
notificationPolicy: 'all_runs',
taskPayload: {},
createdAt: '2026-08-09T00:00:00Z',
updatedAt: '2026-08-09T00:00:00Z',
}

const automationApi = {
listAutomations: vi.fn().mockResolvedValue({ items: [automation] }),
listAutomationRuns: vi.fn().mockResolvedValue({ items: [] }),
}

const workbenchMock = {
state: {
user: { id: 1, user_name: 'alice', preferences: {} },
projects: [],
devices: [],
runtimeWork: { projects: [], chats: [], totalTasks: 0 },
currentProject: null,
currentRuntimeTask: null,
standaloneDeviceId: null,
standaloneWorkspacePath: null,
defaultTeam: null,
},
services: { automationApi },
projectChat: {
models: [],
selectedModel: null,
selectedModelOptions: {},
},
cloudWorkStatus: null,
refreshWorkLists: vi.fn().mockResolvedValue(undefined),
}

vi.mock('@/features/workbench/useWorkbench', () => ({
useWorkbench: () => workbenchMock,
}))

vi.mock('@/features/auth/useAuth', () => ({
useAuth: () => ({ logout: vi.fn() }),
}))

vi.mock('@/hooks/useIsMobile', () => ({
useIsMobile: () => false,
}))

vi.mock('@/lib/runtime-environment', () => ({
isTauriRuntime: () => false,
}))

vi.mock('@/components/layout/useDesktopSidebarCollapsed', () => ({
useDesktopSidebarCollapsed: () => ({
sidebarCollapsed: false,
setSidebarCollapsed: vi.fn(),
}),
}))

vi.mock('@/components/layout/DesktopSidebar', () => ({
DesktopSidebar: () => <aside data-testid="desktop-sidebar" />,
}))

vi.mock('@/components/layout/WorkbenchSearchDialog', () => ({
WorkbenchSearchDialog: () => null,
}))

vi.mock('@/features/automations/AutomationDetailWorkspace', () => ({
AutomationDetailWorkspace: ({ onClose }: { onClose: () => void }) => (
<section data-testid="automation-detail-panel">
<button type="button" data-testid="automation-detail-close" onClick={onClose}>
Close
</button>
</section>
),
}))

describe('AutomationsPage', () => {
beforeEach(() => {
vi.clearAllMocks()
automationApi.listAutomations.mockResolvedValue({ items: [automation] })
automationApi.listAutomationRuns.mockResolvedValue({ items: [] })
})

test('keeps the detail panel closed after the user dismisses it', async () => {
vi.useFakeTimers()
try {
render(<AutomationsPage />)
await act(() => vi.advanceTimersByTimeAsync(0))

expect(screen.getByTestId('automation-detail-panel')).toBeInTheDocument()

fireEvent.click(screen.getByTestId('automation-detail-close'))

expect(screen.queryByTestId('automation-detail-panel')).not.toBeInTheDocument()
expect(screen.getByText('选择一个任务查看详情')).toBeInTheDocument()
expect(automationApi.listAutomations).toHaveBeenCalledTimes(1)

await act(() => vi.advanceTimersByTimeAsync(30_000))

expect(automationApi.listAutomations).toHaveBeenCalledTimes(2)
expect(screen.queryByTestId('automation-detail-panel')).not.toBeInTheDocument()
} finally {
vi.useRealTimers()
}
})
})
7 changes: 5 additions & 2 deletions wework/src/pages/AutomationsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export function AutomationsPage() {
const [drawerOpen, setDrawerOpen] = useState(false)
const [settingsOpen, setSettingsOpen] = useState(false)
const [searchOpen, setSearchOpen] = useState(false)
const initialAutomationLoadCompletedRef = useRef(false)
const runListSignatureRef = useRef('')
const refreshWorkListsRef = useRef(refreshWorkLists)
const locale = i18n.language
Expand All @@ -124,7 +125,9 @@ export function AutomationsPage() {
try {
const response = await automationApi.listAutomations()
setAutomations(response.items)
if (!selectedAutomationId && response.items[0]) {
const shouldSelectInitialAutomation = !initialAutomationLoadCompletedRef.current
initialAutomationLoadCompletedRef.current = true
if (shouldSelectInitialAutomation && response.items[0]) {
setSelectedAutomationId(response.items[0].id)
setEditing(response.items[0])
setDraft(automationDraftFromAutomation(response.items[0]))
Expand All @@ -140,7 +143,7 @@ export function AutomationsPage() {
} finally {
setLoading(false)
}
}, [automationApi, selectedAutomationId, t])
}, [automationApi, t])

const loadRuns = useCallback(
async (automationId: string) => {
Expand Down
Loading