Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
156 changes: 154 additions & 2 deletions wework/src/features/workbench/WorkbenchProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,9 @@ function ArchiveRemoteRuntimeTaskProbe() {
>
archive remote task
</button>
<button type="button" onClick={() => void workbench.refreshWorkLists()}>
refresh work lists
</button>
</div>
)
}
Expand Down Expand Up @@ -6476,7 +6479,7 @@ describe('WorkbenchProvider runtime tasks', () => {
await waitFor(() => expect(screen.getByTestId('archive-result')).toHaveTextContent('archived'))
})

test('does not restore an archived remote task from the previous cloud snapshot', async () => {
test('archives a remote task locally without triggering a cloud sync', async () => {
const remoteRuntimeWork: RuntimeWorkListResponse = {
projects: [
{
Expand Down Expand Up @@ -6540,15 +6543,164 @@ describe('WorkbenchProvider runtime tasks', () => {
await userEvent.click(screen.getByText('archive remote task'))

await waitFor(() => expect(runtimeWorkApi.archiveConversation).toHaveBeenCalledTimes(1))
await waitFor(() => expect(cloudListRuntimeWork).toHaveBeenCalledTimes(2))
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('')
expect(cloudListRuntimeWork).toHaveBeenCalledTimes(1)

await userEvent.click(screen.getByText('refresh work lists'))
await waitFor(() => expect(cloudListRuntimeWork).toHaveBeenCalledTimes(2))
postArchiveCloudWork.resolve({ projects: [], chats: [], totalTasks: 0 })
await waitFor(() =>
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('')
)
})

test('keeps an archived remote task hidden when the local list refresh fails', async () => {
const remoteRuntimeWork: RuntimeWorkListResponse = {
projects: [
{
project: { key: 'remote-project', name: 'Remote Wegent' },
deviceWorkspaces: [
{
deviceId: 'remote-device',
deviceName: '10.201.3.200',
deviceStatus: 'online',
available: true,
workspacePath: '/srv/Wegent',
workspaceSource: 'remote',
remoteHostId: 'remote-device',
tasks: [
{
taskId: 'remote-task',
workspacePath: '/srv/Wegent',
title: 'Remote task',
runtime: 'codex',
},
],
},
],
totalTasks: 1,
},
],
chats: [],
totalTasks: 1,
}
const runtimeWorkApi = createRuntimeWorkApiMock({
listRuntimeWork: vi.fn().mockRejectedValue(new Error('local list unavailable')),
})
const services = createWorkbenchServices({
runtimeWorkApi: runtimeWorkApi as WorkbenchServices['runtimeWorkApi'],
cloudBackgroundApi: {
listTeams: vi.fn().mockResolvedValue([]),
listDevices: vi.fn().mockResolvedValue([
createDevice({
id: 2,
device_id: 'remote-device',
name: '10.201.3.200',
status: 'online',
is_default: false,
device_type: 'remote',
}),
]),
listRuntimeWork: vi.fn().mockResolvedValue(remoteRuntimeWork),
},
})

renderWorkbench(<ArchiveRemoteRuntimeTaskProbe />, services)

await waitFor(() =>
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('Remote task')
)
await userEvent.click(screen.getByText('archive remote task'))

await waitFor(() => expect(runtimeWorkApi.archiveConversation).toHaveBeenCalledTimes(1))
await waitFor(() =>
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('')
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

test('does not trigger a cloud sync for stream events on an archived task', async () => {
const remoteRuntimeWork: RuntimeWorkListResponse = {
projects: [
{
project: { key: 'remote-project', name: 'Remote Wegent' },
deviceWorkspaces: [
{
deviceId: 'remote-device',
deviceName: '10.201.3.200',
deviceStatus: 'online',
available: true,
workspacePath: '/srv/Wegent',
workspaceSource: 'remote',
remoteHostId: 'remote-device',
tasks: [
{
taskId: 'remote-task',
workspacePath: '/srv/Wegent',
title: 'Remote task',
runtime: 'codex',
},
],
},
],
totalTasks: 1,
},
],
chats: [],
totalTasks: 1,
}
let streamHandlers: ChatStreamHandlers = {}
const subscribe = vi.fn((handlers: ChatStreamHandlers) => {
if (hasRuntimeStreamHandler(handlers)) streamHandlers = handlers
return vi.fn()
})
const cloudListRuntimeWork = vi.fn().mockResolvedValue(remoteRuntimeWork)
const runtimeWorkApi = createRuntimeWorkApiMock({
listRuntimeWork: vi.fn().mockResolvedValue({ projects: [], chats: [], totalTasks: 0 }),
})
const services = createWorkbenchServices({
runtimeWorkApi: runtimeWorkApi as WorkbenchServices['runtimeWorkApi'],
chatStream: {
subscribe,
} as unknown as WorkbenchServices['chatStream'],
cloudBackgroundApi: {
listTeams: vi.fn().mockResolvedValue([]),
listDevices: vi.fn().mockResolvedValue([
createDevice({
id: 2,
device_id: 'remote-device',
name: '10.201.3.200',
status: 'online',
is_default: false,
device_type: 'remote',
}),
]),
listRuntimeWork: cloudListRuntimeWork,
},
})

renderWorkbench(<ArchiveRemoteRuntimeTaskProbe />, services)

await waitFor(() =>
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('Remote task')
)
await userEvent.click(screen.getByText('archive remote task'))
await waitFor(() => expect(runtimeWorkApi.archiveConversation).toHaveBeenCalledTimes(1))
await waitFor(() =>
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('')
)
expect(cloudListRuntimeWork).toHaveBeenCalledTimes(1)

await act(async () => {
streamHandlers.onRuntimeGoalCleared?.({
deviceId: 'remote-device',
taskId: 'remote-task',
})
})

expect(cloudListRuntimeWork).toHaveBeenCalledTimes(1)
expect(screen.getByTestId('archive-remote-task-titles')).toHaveTextContent('')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
})

test('renders streaming runtime task chunks when the socket connects after chat start', async () => {
let streamHandlers: ChatStreamHandlers = {}
const subscribe = vi.fn((handlers: ChatStreamHandlers) => {
Expand Down
4 changes: 2 additions & 2 deletions wework/src/features/workbench/WorkbenchProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ export function WorkbenchProvider({
remoteProjectSyncSignatureRef.current = signature
void executorClient.runtime
.syncRuntimeRemoteProjects({ deviceId: localRuntimeStateDeviceId, projects })
.then(refreshWorkLists)
.then(() => refreshWorkLists())
.catch(error => {
remoteProjectSyncSignatureRef.current = ''
console.warn('[Wework] Failed to sync remote projects into Codex global state', error)
Expand Down Expand Up @@ -1215,7 +1215,7 @@ export function WorkbenchProvider({
)
const stableRefreshWorkLists = useStableEvent(refreshWorkLists)
const refreshRuntimeWorkLists = useStableEvent((address: RuntimeTaskAddress) => {
void stableRefreshWorkLists().catch(error => {
void stableRefreshWorkLists({ syncCloud: false }).catch(error => {
console.warn('[Wework] Runtime work list refresh failed', {
deviceId: address.deviceId,
taskId: address.taskId,
Expand Down
128 changes: 70 additions & 58 deletions wework/src/features/workbench/useWorkbenchDataRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
runtimeWorkContainsTask,
} from './workbenchRuntimeHelpers'
import type { WorkbenchServices } from './workbenchServices'
import type { RefreshWorkLists } from './workbenchContextTypes'
import {
readCachedRemoteRuntimeWork,
reconcileCachedRemoteRuntimeWork,
Expand Down Expand Up @@ -623,64 +624,75 @@ export function useWorkbenchDataRefresh({
user,
])

const refreshWorkLists = useCallback(async () => {
const [devicesResult, runtimeWorkResult] = await Promise.all([
executorClient.commands.listDevices().catch(error => {
const cachedDevices = readCachedDeviceList()
if (cachedDevices.length === 0) throw error
return cachedDevices
}),
executorClient.runtime.listRuntimeWork().catch(() => undefined),
])
const devices = resolveDeviceListWithCache(devicesResult)
const visibleDevices = resolveDeviceListWithCache(
selectVisibleDevices(devices, cloudRuntimeStateRef.current)
)
const filteredRuntimeWorkResult = runtimeWorkResult
? filterRemovedRuntimeProjects(runtimeWorkResult)
: undefined
if (filteredRuntimeWorkResult) {
localRuntimeWorkRef.current = filteredRuntimeWorkResult
}
const localRuntimeWork = filteredRuntimeWorkResult ?? state.runtimeWork ?? EMPTY_RUNTIME_WORK
if (filteredRuntimeWorkResult && !services.cloudBackgroundApi?.listRuntimeWork) {
releaseConfirmedArchivedRuntimeTasks(filteredRuntimeWorkResult)
}
const runtimeWork = filteredRuntimeWorkResult
? selectVisibleRuntimeWork(localRuntimeWork, cloudRuntimeStateRef.current, visibleDevices)
: hasCloudBackgroundApi
? localRuntimeWork
: filterDisconnectedRemoteRuntimeWork(localRuntimeWork)
debugRuntimeSidebarState('refresh-resolved', {
source: filteredRuntimeWorkResult ? 'executor' : 'current-state',
executorTaskIds: summarizeRuntimeWorkTaskIds(filteredRuntimeWorkResult ?? null),
visibleTaskIds: summarizeRuntimeWorkTaskIds(runtimeWork),
})
dispatch({
type: 'lists_refreshed',
projects: state.projects,
devices: visibleDevices,
runtimeWork,
standaloneDeviceId: getPreferredStandaloneDeviceId(visibleDevices, state.standaloneDeviceId),
})
void refreshCloudBackgroundData(devices, localRuntimeWork, {
projects: state.projects,
standaloneDeviceId: state.standaloneDeviceId,
trigger: 'manual-refresh',
}).catch(() => undefined)
}, [
dispatch,
executorClient,
filterRemovedRuntimeProjects,
refreshCloudBackgroundData,
hasCloudBackgroundApi,
releaseConfirmedArchivedRuntimeTasks,
selectVisibleRuntimeWork,
services.cloudBackgroundApi,
state.projects,
state.runtimeWork,
state.standaloneDeviceId,
])
const refreshWorkLists: RefreshWorkLists = useCallback(
async options => {
const [devicesResult, runtimeWorkResult] = await Promise.all([
executorClient.commands.listDevices().catch(error => {
const cachedDevices = readCachedDeviceList()
if (cachedDevices.length === 0) throw error
return cachedDevices
}),
executorClient.runtime.listRuntimeWork().catch(() => undefined),
])
const devices = resolveDeviceListWithCache(devicesResult)
const visibleDevices = resolveDeviceListWithCache(
selectVisibleDevices(devices, cloudRuntimeStateRef.current)
)
const filteredRuntimeWorkResult = runtimeWorkResult
? filterRemovedRuntimeProjects(runtimeWorkResult)
: undefined
if (filteredRuntimeWorkResult) {
localRuntimeWorkRef.current = filteredRuntimeWorkResult
}
const localRuntimeWork = filteredRuntimeWorkResult ?? state.runtimeWork ?? EMPTY_RUNTIME_WORK
if (filteredRuntimeWorkResult && !services.cloudBackgroundApi?.listRuntimeWork) {
releaseConfirmedArchivedRuntimeTasks(filteredRuntimeWorkResult)
}
const runtimeWork = filteredRuntimeWorkResult
? selectVisibleRuntimeWork(localRuntimeWork, cloudRuntimeStateRef.current, visibleDevices)
: removeRuntimeTasks(
hasCloudBackgroundApi
? localRuntimeWork
: filterDisconnectedRemoteRuntimeWork(localRuntimeWork),
archivedRuntimeTaskAddressesRef.current
)
debugRuntimeSidebarState('refresh-resolved', {
source: filteredRuntimeWorkResult ? 'executor' : 'current-state',
executorTaskIds: summarizeRuntimeWorkTaskIds(filteredRuntimeWorkResult ?? null),
visibleTaskIds: summarizeRuntimeWorkTaskIds(runtimeWork),
})
dispatch({
type: 'lists_refreshed',
projects: state.projects,
devices: visibleDevices,
runtimeWork,
standaloneDeviceId: getPreferredStandaloneDeviceId(
visibleDevices,
state.standaloneDeviceId
),
})
if (options?.syncCloud !== false) {
void refreshCloudBackgroundData(devices, localRuntimeWork, {
projects: state.projects,
standaloneDeviceId: state.standaloneDeviceId,
trigger: 'manual-refresh',
}).catch(() => undefined)
}
},
[
dispatch,
executorClient,
filterRemovedRuntimeProjects,
refreshCloudBackgroundData,
hasCloudBackgroundApi,
releaseConfirmedArchivedRuntimeTasks,
selectVisibleRuntimeWork,
services.cloudBackgroundApi,
state.projects,
state.runtimeWork,
state.standaloneDeviceId,
]
)

const loadDevicesForRefresh = useCallback(
async (options?: { useCacheFallback?: boolean }): Promise<DeviceInfo[]> => {
Expand Down
5 changes: 3 additions & 2 deletions wework/src/features/workbench/useWorkbenchRuntimeTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
ArchiveRuntimeTaskOptions,
ArchiveRuntimeTaskResult,
ArchiveRuntimeConversationsResult,
RefreshWorkLists,
} from './workbenchContextTypes'
import { evictRuntimeConversation } from './runtimeConversationCache'
import type { RuntimeTaskLifecycleStore } from './runtimeTaskLifecycle'
Expand All @@ -53,7 +54,7 @@ interface UseWorkbenchRuntimeTasksOptions {
services: WorkbenchServices
lifecycleStore: RuntimeTaskLifecycleStore
markRuntimeTasksArchived: (addresses: RuntimeTaskAddress[]) => void
refreshWorkLists: () => Promise<void>
refreshWorkLists: RefreshWorkLists
}

const runtimeTranscriptRequests = new Map<string, Promise<RuntimePaneTranscript>>()
Expand Down Expand Up @@ -265,7 +266,7 @@ export function useWorkbenchRuntimeTasks({
findRuntimeTaskWorktrees(state.runtimeWork, archivedAddresses)
)
clearCurrentRuntimeTaskIfArchived(archivedAddresses)
await refreshWorkLists()
await refreshWorkLists({ syncCloud: false })
}
const failedResult = results.find(result => !result.response?.accepted)
if (!failedResult) return { status: 'archived' }
Expand Down
4 changes: 3 additions & 1 deletion wework/src/features/workbench/workbenchContextTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export type ArchiveRuntimeTaskResult = {
status: 'archived' | 'dirty_worktree' | 'failed'
}

export type RefreshWorkLists = (options?: { syncCloud?: boolean }) => Promise<void>

export type ArchiveRuntimeConversationsResult = ArchiveRuntimeTaskResult

export interface SendCurrentInputOptions {
Expand Down Expand Up @@ -229,7 +231,7 @@ export interface WorkbenchContextValue {
address: RuntimeTaskAddress
) => Promise<RuntimeTaskIMNotificationSubscriptionResponse>
rememberExecutionDevice: (deviceId: string) => void
refreshWorkLists: () => Promise<void>
refreshWorkLists: RefreshWorkLists
refreshDevices: () => Promise<void>
getRemoteDeviceStartupCommand: () => Promise<DockerRemoteDeviceCommandResponse>
upgradeDevice: (deviceId: string) => Promise<void>
Expand Down
Loading