Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions wework/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,10 @@ The active-conversation capture is also normative:
multiple markers may be active when content from multiple turns is visible.
- The bottom Composer shares the thread column and stays visible. It uses the
same input hierarchy as home but without the home project-selector layer.
- Runtime stream lifecycle events update only the affected task's in-memory
status. They must not refresh the whole sidebar work list. A generated task
title is authoritative over older list requests already in flight until the
local executor confirms the same title.
- When opening, closing, or resizing a side panel reflows conversation content,
preserve the reader's visible message or content anchor. Continue following
the bottom only when the reader was already at the bottom before the reflow.
Expand Down
162 changes: 118 additions & 44 deletions wework/src/features/workbench/WorkbenchProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2047,7 +2047,7 @@ describe('WorkbenchProvider runtime tasks', () => {
await waitFor(() => expect(getComposerApps()).toEqual([]))
})

test('keeps a background runtime task settled when its terminal refresh is stale', async () => {
test('settles a background runtime task without refreshing runtime work', async () => {
let backgroundStreamHandlers: ChatStreamHandlers | null = null
const subscribe = vi.fn((handlers: ChatStreamHandlers) => {
if (hasRuntimeStreamHandler(handlers)) {
Expand Down Expand Up @@ -2085,12 +2085,7 @@ describe('WorkbenchProvider runtime tasks', () => {
],
totalTasks: 1,
})
const staleTerminalRefresh = deferred<RuntimeWorkListResponse>()
const listRuntimeWork = vi
.fn()
.mockResolvedValue(runningRuntimeWork)
.mockResolvedValueOnce(runningRuntimeWork)
.mockImplementationOnce(() => staleTerminalRefresh.promise)
const listRuntimeWork = vi.fn().mockResolvedValue(runningRuntimeWork)
const runtimeWorkApi = createRuntimeWorkApiMock({ listRuntimeWork })
const services = createWorkbenchServices({
runtimeWorkApi: runtimeWorkApi as WorkbenchServices['runtimeWorkApi'],
Expand All @@ -2115,14 +2110,10 @@ describe('WorkbenchProvider runtime tasks', () => {
})
})

await waitFor(() => expect(listRuntimeWork).toHaveBeenCalledTimes(2))
await act(async () => {
staleTerminalRefresh.resolve(runningRuntimeWork)
await staleTerminalRefresh.promise
})
await waitFor(() =>
expect(screen.getByTestId('runtime-running-task-ids')).toHaveTextContent('none')
)
expect(listRuntimeWork).toHaveBeenCalledTimes(1)
})

test('settles guidance applied while its runtime pane is in the background', async () => {
Expand Down Expand Up @@ -2667,6 +2658,83 @@ describe('WorkbenchProvider runtime tasks', () => {
)
})

test('does not let a stale cloud refresh roll back a generated runtime task title', async () => {
let streamHandlers: ChatStreamHandlers = {}
const subscribe = vi.fn((handlers: ChatStreamHandlers) => {
if (hasRuntimeStreamHandler(handlers)) streamHandlers = handlers
return vi.fn()
})
const cloudRuntimeWork = deferred<RuntimeWorkListResponse>()
const localRuntimeWork = createRuntimeWork({
projects: [
{
project: { id: 7, name: 'Wegent' },
deviceWorkspaces: [
{
id: 22,
projectId: 7,
deviceId: 'device-1',
deviceName: 'Project Device',
deviceStatus: 'online',
workspacePath: '/workspace/project-alpha',
mapped: true,
available: true,
tasks: [
{
taskId: 'runtime-a',
workspacePath: '/workspace/project-alpha',
title: '解决冲突',
runtime: 'codex',
},
],
},
],
totalTasks: 1,
},
],
totalTasks: 1,
})
const services = createWorkbenchServices({
runtimeWorkApi: createRuntimeWorkApiMock({
listRuntimeWork: vi.fn().mockResolvedValue(localRuntimeWork),
}),
chatStream: {
subscribe,
} as unknown as WorkbenchServices['chatStream'],
cloudBackgroundApi: {
listTeams: vi.fn().mockResolvedValue([]),
listDevices: vi.fn().mockResolvedValue([]),
listRuntimeWork: vi.fn(() => cloudRuntimeWork.promise),
},
})

renderWorkbench(<ProjectSendProbe />, services)

await waitFor(() =>
expect(screen.getByTestId('runtime-task-titles')).toHaveTextContent('解决冲突')
)
await waitFor(() => expect(streamHandlers.onRuntimeTaskTitleUpdated).toBeDefined())

act(() => {
streamHandlers.onRuntimeTaskTitleUpdated?.({
taskId: 'runtime-a',
subtaskId: 'friendly-title',
deviceId: 'device-1',
title: '解决分支冲突',
})
})
expect(screen.getByTestId('runtime-task-titles')).toHaveTextContent('解决分支冲突')

await act(async () => {
cloudRuntimeWork.resolve({ projects: [], chats: [], totalTasks: 0 })
})

await waitFor(() =>
expect(screen.getByTestId('runtime-task-titles')).toHaveTextContent('解决分支冲突')
)
expect(screen.getByTestId('runtime-task-titles')).not.toHaveTextContent('解决冲突')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

test('cancels an in-flight cloud sync before a manual device refresh', async () => {
const runtimeWork = deferred<RuntimeWorkListResponse>()
const manualDevices = deferred<DeviceInfo[]>()
Expand Down Expand Up @@ -8976,7 +9044,7 @@ describe('WorkbenchProvider runtime tasks', () => {
expect(sendRuntimeMessage).not.toHaveBeenCalled()
})

test('refreshes runtime work when the current runtime task starts streaming', async () => {
test('marks the current runtime task running without refreshing runtime work', async () => {
let streamHandlers: Parameters<WorkbenchServices['chatStream']['subscribe']>[0] | null = null
const subscribe = vi.fn(handlers => {
if (hasRuntimeStreamHandler(handlers)) streamHandlers = handlers
Expand Down Expand Up @@ -9010,7 +9078,10 @@ describe('WorkbenchProvider runtime tasks', () => {
})
})

await waitFor(() => expect(listRuntimeWork).toHaveBeenCalledTimes(callsBeforeStart + 1))
await waitFor(() =>
expect(screen.getByTestId('current-runtime-task-running')).toHaveTextContent('running')
)
expect(listRuntimeWork).toHaveBeenCalledTimes(callsBeforeStart)
})

test('hides the runtime goal when the settled task reports the goal complete', async () => {
Expand Down Expand Up @@ -9649,37 +9720,38 @@ describe('WorkbenchProvider runtime tasks', () => {
})
const updateTaskTrackingStatus = vi.fn().mockResolvedValue(null)
const updateTaskTrackingTitle = vi.fn().mockResolvedValue(null)
const listRuntimeWork = vi.fn().mockResolvedValue(
createRuntimeWork({
projects: [
{
project: { id: 7, name: 'Wegent' },
deviceWorkspaces: [
{
deviceId: 'device-1',
deviceName: 'Project Device',
deviceStatus: 'online',
workspacePath: '/workspace/project-alpha',
mapped: true,
available: true,
tasks: [
{
taskId: 'runtime-a',
workspacePath: '/workspace/project-alpha',
title: 'Runtime A',
runtime: 'codex',
running: false,
status: 'active',
},
],
},
],
},
],
totalTasks: 1,
})
)
const runtimeWorkApi = createRuntimeWorkApiMock({
listRuntimeWork: vi.fn().mockResolvedValue(
createRuntimeWork({
projects: [
{
project: { id: 7, name: 'Wegent' },
deviceWorkspaces: [
{
deviceId: 'device-1',
deviceName: 'Project Device',
deviceStatus: 'online',
workspacePath: '/workspace/project-alpha',
mapped: true,
available: true,
tasks: [
{
taskId: 'runtime-a',
workspacePath: '/workspace/project-alpha',
title: 'Runtime A',
runtime: 'codex',
running: false,
status: 'active',
},
],
},
],
},
],
totalTasks: 1,
})
),
listRuntimeWork,
})
const services = createWorkbenchServices({
runtimeWorkApi: runtimeWorkApi as WorkbenchServices['runtimeWorkApi'],
Expand All @@ -9693,6 +9765,7 @@ describe('WorkbenchProvider runtime tasks', () => {

renderWorkbench(<RuntimeTopLevelStreamLifecycleProbe />, services)
await waitFor(() => expect(streamHandlers.onChatStart).toBeDefined())
await waitFor(() => expect(listRuntimeWork).toHaveBeenCalledTimes(1))

act(() => {
streamHandlers.onChatStart?.({
Expand Down Expand Up @@ -9738,6 +9811,7 @@ describe('WorkbenchProvider runtime tasks', () => {
'修复登录回调'
)
)
expect(listRuntimeWork).toHaveBeenCalledTimes(1)
})

test('sends queued runtime messages when the task becomes idle', async () => {
Expand Down
28 changes: 27 additions & 1 deletion wework/src/features/workbench/WorkbenchProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,8 @@ export function WorkbenchProvider({
clearRuntimeProjectRemoval,
refreshWorkLists,
refreshDevices,
updateLocalRuntimeTaskExecution,
updateLocalRuntimeTaskTitle,
getRemoteDeviceStartupCommand,
} = useWorkbenchDataRefresh({
user,
Expand Down Expand Up @@ -1571,6 +1573,13 @@ export function WorkbenchProvider({
settleRuntimeConversationAcceptedMessage(address)
markRuntimeConversationAssistantStarted(address)
lifecycleStore.turnStarted(address, turnId)
updateLocalRuntimeTaskExecution(address, true, 'active')
dispatch({
type: 'runtime_task_execution_updated',
address,
running: true,
status: 'active',
})
aiGenerationTelemetry.onAssistantStart(address, turnId)
},
onAssistantFirstToken: (address, turnId) => {
Expand All @@ -1582,6 +1591,21 @@ export function WorkbenchProvider({
onAssistantSettled: (address, turnId, outcome) => {
settleRuntimeConversationSubagents(address)
lifecycleStore.turnSettled(address, turnId, outcome)
const running = lifecycleStore.getTask(address)?.derived.isRunning ?? false
const status = running
? 'active'
: outcome === 'succeeded'
? 'done'
: outcome === 'failed'
? 'failed'
: 'cancelled'
updateLocalRuntimeTaskExecution(address, running, status)
dispatch({
type: 'runtime_task_execution_updated',
address,
running,
status,
})
aiGenerationTelemetry.onAssistantSettled(
address,
turnId,
Expand All @@ -1591,6 +1615,7 @@ export function WorkbenchProvider({
onContextUsageUpdated: updateCanonicalRuntimeContextUsage,
onSubagentActivity: applyRuntimeConversationSubagentActivity,
onRuntimeTaskTitleUpdated: (address, payload) => {
updateLocalRuntimeTaskTitle(address, payload.title)
dispatch({
type: 'runtime_task_title_updated',
address,
Expand Down Expand Up @@ -1618,7 +1643,6 @@ export function WorkbenchProvider({
},
onRuntimePlanUpdated: setRuntimeConversationTaskPlan,
onRuntimeTransportReplaced: publishRuntimeTransportReplaced,
onRefreshWorkLists: refreshRuntimeWorkLists,
})
),
[
Expand All @@ -1630,6 +1654,8 @@ export function WorkbenchProvider({
settleCanonicalRuntimeGuidance,
syncRuntimeTaskTitle,
updateCanonicalRuntimeContextUsage,
updateLocalRuntimeTaskExecution,
updateLocalRuntimeTaskTitle,
]
)

Expand Down
11 changes: 1 addition & 10 deletions wework/src/features/workbench/runtimePaneMessages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,9 @@ describe('createRuntimeTaskStreamHandlers', () => {
deviceId: 'device-1',
taskId: 'runtime-task-1',
}
const onRefreshWorkLists = vi.fn()
const onRuntimeTaskTitleUpdated = vi.fn()
const handlers = createRuntimeTaskStreamHandlers(address, {
onMessageAction: vi.fn(),
onRefreshWorkLists,
onRuntimeTaskTitleUpdated,
})

Expand All @@ -180,7 +178,6 @@ describe('createRuntimeTaskStreamHandlers', () => {
title: '测试标题生成功能',
})

expect(onRefreshWorkLists).not.toHaveBeenCalled()
expect(onRuntimeTaskTitleUpdated).toHaveBeenCalledWith({
taskId: 'runtime-task-1',
subtaskId: 'friendly-title-turn',
Expand Down Expand Up @@ -506,18 +503,16 @@ describe('createRuntimeTaskStreamHandlers', () => {
)
})

test('passes context compaction through regular block created actions', () => {
test('passes context compaction through regular block created actions without refreshing work', () => {
const address: RuntimeTaskAddress = {
deviceId: 'device-1',
taskId: 'runtime-task-1',
}
const actions: RuntimePaneMessageAction[] = []
const onAssistantSettled = vi.fn()
const onRefreshWorkLists = vi.fn()
const handlers = createRuntimeTaskStreamHandlers(address, {
onMessageAction: action => actions.push(action),
onAssistantSettled,
onRefreshWorkLists,
})

handlers.onBlockCreated?.({
Expand Down Expand Up @@ -548,7 +543,6 @@ describe('createRuntimeTaskStreamHandlers', () => {
subtaskId: 'runtime-task-1-context-compact',
})
expect(onAssistantSettled).toHaveBeenCalledTimes(1)
expect(onRefreshWorkLists).toHaveBeenCalledTimes(1)
})

test('passes reclassified assistant text identity to the conversation reducer', () => {
Expand Down Expand Up @@ -596,11 +590,9 @@ describe('createRuntimeTaskStreamHandlers', () => {
}
const actions: RuntimePaneMessageAction[] = []
const onAssistantSettled = vi.fn()
const onRefreshWorkLists = vi.fn()
const handlers = createRuntimeTaskStreamHandlers(address, {
onMessageAction: action => actions.push(action),
onAssistantSettled,
onRefreshWorkLists,
})

handlers.onBlockCreated?.({
Expand Down Expand Up @@ -628,7 +620,6 @@ describe('createRuntimeTaskStreamHandlers', () => {
},
})
expect(onAssistantSettled).not.toHaveBeenCalled()
expect(onRefreshWorkLists).not.toHaveBeenCalled()
})

test('preserves request user input render payload on block created events', () => {
Expand Down
Loading
Loading