Skip to content

fix(wework): prevent horizontal conversation drift - #2109

Merged
qdaxb merged 13 commits into
wecode-ai:mainfrom
Micro66:human/cobra-20260722-105257
Jul 22, 2026
Merged

qdaxb merged 13 commits into
wecode-ai:mainfrom
Micro66:human/cobra-20260722-105257

Conversation

@Micro66

@Micro66 Micro66 commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Added an execution-device control to the project composer (cloud vs local), including localized “Runs on {location} · {device}” labeling.
    • Updated project-work composer UI so cloud standalone devices open the project-work menu for new tasks.
  • Bug Fixes
    • Improved live/runtime transcript merging to avoid duplicates and preserve stable ordering.
    • Reset horizontal scrolling when switching workbench tasks and prevented unwanted horizontal overflow.
    • Refined running-task transcript behavior to avoid unnecessary polling while live streaming owns the active task.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f378294-8c30-4f55-a75c-713a27b7c98c

📥 Commits

Reviewing files that changed from the base of the PR and between efb72e4 and 9ba39ef.

📒 Files selected for processing (1)
  • wework/e2e/desktop/task-flow.e2e.mjs

📝 Walkthrough

Walkthrough

Adds execution-device controls to the project composer, centralizes runtime transcript merging, removes running-task transcript polling, resets desktop horizontal scrolling, repairs legacy task timestamps, makes the settings sidebar vertically scrollable, and waits for marketplace controls before E2E snapshots.

Changes

Workbench UI updates

Layer / File(s) Summary
Execution-device controls and menu integration
wework/src/components/chat/..., wework/src/components/layout/useWorkbenchPaneEnvironment.ts, wework/src/components/layout/useWorkbenchProjectWorkControls.ts, wework/src/i18n/locales/*/common.json
The composer resolves cloud or local devices, renders selectable or disabled controls, and opens the project menu when selection is available.
Desktop workbench scroll behavior
wework/src/components/layout/DesktopWorkbenchMain.tsx, wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
Horizontal conversation overflow is hidden and scroll position resets when the active pane changes.
Settings sidebar scrolling
wework/src/components/settings/ConnectionsSettingsPage.tsx, wework/src/components/settings/ConnectionsSettingsPage.test.tsx
The settings top bar remains fixed while sidebar navigation becomes vertically scrollable.

Runtime transcript handling

Layer / File(s) Summary
Shared runtime transcript merging
wework/src/components/layout/runtimeTranscriptMessages.ts, wework/src/components/layout/runtimeTranscriptMessages.test.ts, wework/src/components/layout/useWorkbenchPaneSession.ts
Messages are deduplicated by ID, ordered by validated runtime indices, and covered by focused tests.
Running transcript polling removal
wework/src/components/layout/useWorkbenchPaneSession.ts, wework/src/features/workbench/WorkbenchProvider.test.tsx
Running-task reconciliation state and polling are removed, and tests verify the live stream remains authoritative.

Runtime task repairs

Layer / File(s) Summary
Task timestamp repair and route updates
executor/src/runtime_work/handler.rs
Transcript reads repair legacy activity timestamps, while unchanged thread routes no longer update stored task timestamps.

Desktop end-to-end readiness

Layer / File(s) Summary
Marketplace control readiness
wework/e2e/desktop/task-flow.e2e.mjs
Plugin lifecycle verification waits for marketplace controls before capturing its initial snapshot.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProjectChatComposer
  participant ComposerToolbar
  participant ProjectWorkBar
  participant ProjectMenu
  ProjectChatComposer->>ComposerToolbar: provide resolved execution device
  ComposerToolbar->>ProjectChatComposer: invoke device selection
  ProjectChatComposer->>ProjectWorkBar: update external menu signal
  ProjectWorkBar->>ProjectMenu: close execution menu and open project menu
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and matches the main change: preventing horizontal conversation drift via scroll and layout fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wework/src/components/chat/ChatInput.test.tsx`:
- Around line 3286-3359: Extend the execution-device coverage around the
existing ChatInput tests with a real-Tauri backend E2E: select both local and
cloud devices, launch a task, verify the actual runtime target, and confirm
device switching is blocked while the task is active. Add assertions for launch
failure and subsequent recovery, and document cleanup evidence in the QA plan;
keep the current control and menu unit-test coverage intact.

In `@wework/src/components/layout/DesktopWorkbenchMain.tsx`:
- Around line 476-483: Update the dependency list of the useLayoutEffect that
resets workbenchScroll.scrollLeft to include paneKey alongside paneActive, so
horizontal scrolling resets when the active pane/session identity changes
without altering the existing reset logic.

In `@wework/src/components/layout/runtimeTranscriptMessages.ts`:
- Around line 12-22: Normalize assistant subtask IDs consistently before storing
and retrieving entries in the maps used by the runtime transcript message flow,
using a single subtaskId helper that applies String(...) and no subtask_id
alias. Update both restored-message indexing and live-message lookup to use it,
and add a regression test covering a numeric restored ID matched with an
equivalent string live ID.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 341fdc66-9f61-4e09-9f7c-5fe6df0855f4

📥 Commits

Reviewing files that changed from the base of the PR and between 08f32c1 and 3af7cc9.

📒 Files selected for processing (14)
  • wework/src/components/chat/ChatInput.test.tsx
  • wework/src/components/chat/ChatInput.tsx
  • wework/src/components/chat/composer/ComposerToolbar.tsx
  • wework/src/components/chat/composer/ProjectChatComposer.tsx
  • wework/src/components/chat/composer/ProjectWorkBar.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
  • wework/src/components/layout/DesktopWorkbenchMain.tsx
  • wework/src/components/layout/runtimeTranscriptMessages.test.ts
  • wework/src/components/layout/runtimeTranscriptMessages.ts
  • wework/src/components/layout/useWorkbenchPaneEnvironment.ts
  • wework/src/components/layout/useWorkbenchPaneSession.ts
  • wework/src/components/layout/useWorkbenchProjectWorkControls.ts
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json

Comment on lines +3286 to +3359
test('shows a subtle cloud-device control and opens device selection for a new task', async () => {
const devices: DeviceInfo[] = [
{
id: 1,
device_id: 'cloud-online',
name: 'Cloud Online',
status: 'online',
is_default: false,
device_type: 'cloud',
},
]

render(
<ChatInput
value=""
onChange={vi.fn()}
onSubmit={vi.fn()}
disabled={false}
variant="desktop"
projectWork={projectWorkControls({
devices,
currentStandaloneDeviceId: 'cloud-online',
})}
/>
)

const deviceButton = screen.getByTestId('composer-execution-device-button')
expect(deviceButton).toHaveAccessibleName('运行于云设备 · Cloud Online')
expect(deviceButton).toBeEnabled()

await userEvent.click(deviceButton)
expect(screen.getByTestId('project-work-menu')).toBeInTheDocument()
})

test('shows the actual runtime device without allowing an active task to switch it', () => {
const devices: DeviceInfo[] = [
{
id: 1,
device_id: 'local-online',
name: 'Local Online',
status: 'online',
is_default: false,
device_type: 'local',
},
{
id: 2,
device_id: 'cloud-online',
name: 'Cloud Online',
status: 'online',
is_default: false,
device_type: 'cloud',
},
]

render(
<ChatInput
value=""
onChange={vi.fn()}
onSubmit={vi.fn()}
disabled={false}
variant="desktop"
showProjectWorkBar={false}
projectWork={projectWorkControls({
devices,
currentStandaloneDeviceId: 'local-online',
currentRuntimeDeviceId: 'cloud-online',
})}
/>
)

const deviceButton = screen.getByTestId('composer-execution-device-button')
expect(deviceButton).toHaveAccessibleName('运行于云设备 · Cloud Online')
expect(deviceButton).toHaveAttribute('aria-disabled', 'true')
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Add real-Tauri execution-device launch coverage.

These tests only verify the control and menu. Add a real-backend E2E that selects local/cloud execution, launches a task, verifies the runtime target, then verifies switching is blocked while active. Include failure/recovery and cleanup evidence in the QA plan.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/chat/ChatInput.test.tsx` around lines 3286 - 3359,
Extend the execution-device coverage around the existing ChatInput tests with a
real-Tauri backend E2E: select both local and cloud devices, launch a task,
verify the actual runtime target, and confirm device switching is blocked while
the task is active. Add assertions for launch failure and subsequent recovery,
and document cleanup evidence in the QA plan; keep the current control and menu
unit-test coverage intact.

Source: Coding guidelines

Comment on lines +476 to +483
useLayoutEffect(() => {
if (!paneActive) return

const workbenchScroll = workbenchScrollRef.current
if (workbenchScroll && workbenchScroll.scrollLeft !== 0) {
workbenchScroll.scrollLeft = 0
}
}, [paneActive])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset on pane identity changes, not only activation changes.

paneKey is the existing pane/session identity, but this effect depends only on paneActive. If the mounted component switches from one active task to another while paneActive remains true, the effect will not rerun and stale horizontal scrolling can persist. Include paneKey in the dependency list.

Proposed fix
-  }, [paneActive])
+  }, [paneActive, paneKey])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useLayoutEffect(() => {
if (!paneActive) return
const workbenchScroll = workbenchScrollRef.current
if (workbenchScroll && workbenchScroll.scrollLeft !== 0) {
workbenchScroll.scrollLeft = 0
}
}, [paneActive])
useLayoutEffect(() => {
if (!paneActive) return
const workbenchScroll = workbenchScrollRef.current
if (workbenchScroll && workbenchScroll.scrollLeft !== 0) {
workbenchScroll.scrollLeft = 0
}
}, [paneActive, paneKey])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/DesktopWorkbenchMain.tsx` around lines 476 -
483, Update the dependency list of the useLayoutEffect that resets
workbenchScroll.scrollLeft to include paneKey alongside paneActive, so
horizontal scrolling resets when the active pane/session identity changes
without altering the existing reset logic.

Comment on lines +12 to +22
if (message.role === 'assistant' && message.subtaskId) {
assistantIndexesBySubtask.set(message.subtaskId, index)
}
})

for (const liveMessage of liveMessages) {
if (indexesById.has(liveMessage.id)) continue

const matchingSubtaskIndex =
liveMessage.role === 'assistant' && liveMessage.subtaskId
? assistantIndexesBySubtask.get(liveMessage.subtaskId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Normalize subtaskId before indexing and lookup.

A restored numeric ID (42) and live string ID ('42') create different Map keys, so the same assistant turn is rendered twice. Normalize both values with String(...) in a single subtaskId helper; do not add a subtask_id alias. Add a regression test for the mixed numeric/string pair.

Proposed fix
+function assistantSubtaskId(message: WorkbenchMessage): string | null {
+  if (message.role !== 'assistant' || message.subtaskId == null) return null
+  return String(message.subtaskId) || null
+}
+
   merged.forEach((message, index) => {
-    if (message.role === 'assistant' && message.subtaskId) {
-      assistantIndexesBySubtask.set(message.subtaskId, index)
+    const subtaskId = assistantSubtaskId(message)
+    if (subtaskId) {
+      assistantIndexesBySubtask.set(subtaskId, index)
     }
   })
 
   for (const liveMessage of liveMessages) {
+    const liveSubtaskId = assistantSubtaskId(liveMessage)
     if (indexesById.has(liveMessage.id)) continue
 
     const matchingSubtaskIndex =
-      liveMessage.role === 'assistant' && liveMessage.subtaskId
-        ? assistantIndexesBySubtask.get(liveMessage.subtaskId)
+      liveSubtaskId
+        ? assistantIndexesBySubtask.get(liveSubtaskId)
         : undefined

Based on learnings, normalize incoming transcript identities to strings while relying solely on subtaskId.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (message.role === 'assistant' && message.subtaskId) {
assistantIndexesBySubtask.set(message.subtaskId, index)
}
})
for (const liveMessage of liveMessages) {
if (indexesById.has(liveMessage.id)) continue
const matchingSubtaskIndex =
liveMessage.role === 'assistant' && liveMessage.subtaskId
? assistantIndexesBySubtask.get(liveMessage.subtaskId)
function assistantSubtaskId(message: WorkbenchMessage): string | null {
if (message.role !== 'assistant' || message.subtaskId == null) return null
return String(message.subtaskId) || null
}
merged.forEach((message, index) => {
const subtaskId = assistantSubtaskId(message)
if (subtaskId) {
assistantIndexesBySubtask.set(subtaskId, index)
}
})
for (const liveMessage of liveMessages) {
const liveSubtaskId = assistantSubtaskId(liveMessage)
if (indexesById.has(liveMessage.id)) continue
const matchingSubtaskIndex =
liveSubtaskId
? assistantIndexesBySubtask.get(liveSubtaskId)
: undefined
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/runtimeTranscriptMessages.ts` around lines 12 -
22, Normalize assistant subtask IDs consistently before storing and retrieving
entries in the maps used by the runtime transcript message flow, using a single
subtaskId helper that applies String(...) and no subtask_id alias. Update both
restored-message indexing and live-message lookup to use it, and add a
regression test covering a numeric restored ID matched with an equivalent string
live ID.

Source: Learnings

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@executor/src/runtime_work/handler.rs`:
- Around line 3146-3158: Update repair_legacy_task_activity_time to repair
timestamps only for terminal tasks or tasks explicitly marked as legacy, rather
than relying solely on running == false; preserve active non-running tasks
without setting completed_at. Add a regression test covering an idle active task
during transcript reads and verifying it remains resumable without stale
completion data.

In `@wework/src/components/layout/runtimeTranscriptMessages.ts`:
- Around line 7-13: Deduplicate transcript entries by ID before merging in the
runtime transcript flow: update the construction around the existing merge logic
in wework/src/components/layout/runtimeTranscriptMessages.ts lines 7-13 to
process transcriptMessages through the same ID-checked insertion path,
preserving the first occurrence and insertion order before adding
existingMessages. Add a test case in
wework/src/components/layout/runtimeTranscriptMessages.test.ts lines 17-27 with
duplicate transcript IDs and assert only the first entry remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a73cb20f-1b87-4b72-90ff-810ea11e13aa

📥 Commits

Reviewing files that changed from the base of the PR and between 3af7cc9 and efb72e4.

📒 Files selected for processing (7)
  • executor/src/runtime_work/handler.rs
  • wework/src/components/layout/runtimeTranscriptMessages.test.ts
  • wework/src/components/layout/runtimeTranscriptMessages.ts
  • wework/src/components/layout/useWorkbenchPaneSession.ts
  • wework/src/components/settings/ConnectionsSettingsPage.test.tsx
  • wework/src/components/settings/ConnectionsSettingsPage.tsx
  • wework/src/features/workbench/WorkbenchProvider.test.tsx

Comment on lines +3146 to +3158
fn repair_legacy_task_activity_time(&self, local_task_id: &str, thread: &Value) {
if self.is_active_local_task(local_task_id) {
return;
}
let Some(thread_updated_at) = timestamp_ms_field(thread, "updatedAt") else {
return;
};
self.store.update_task(local_task_id, |link| {
if !link.running && link.completed_at.is_none() && link.updated_at > thread_updated_at {
link.updated_at = thread_updated_at;
link.completed_at = Some(thread_updated_at);
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Restrict timestamp repair to terminal tasks.

running == false is not sufficient: this file also creates/restores non-terminal tasks with status = "active" and running = false (Lines 1576-1577 and 3763-3765). Since this helper runs during transcript reads (Line 1330), it can persist completed_at on an idle active task and leave stale completion data when the task is later resumed. Gate the repair on a terminal status or an explicit legacy marker, and add an active-task regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/runtime_work/handler.rs` around lines 3146 - 3158, Update
repair_legacy_task_activity_time to repair timestamps only for terminal tasks or
tasks explicitly marked as legacy, rather than relying solely on running ==
false; preserve active non-running tasks without setting completed_at. Add a
regression test covering an idle active task during transcript reads and
verifying it remains resumable without stale completion data.

Comment on lines +7 to +13
const merged = [...transcriptMessages]
const messageIds = new Set(merged.map(message => message.id))

for (const message of existingMessages) {
if (messageIds.has(message.id)) continue
messageIds.add(message.id)
merged.push(message)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Deduplicate IDs within the incoming transcript page.

merged starts as a direct copy of transcriptMessages, so repeated IDs in that page remain rendered twice. Deduplicate both inputs in insertion order, preserving the first transcript entry.

  • wework/src/components/layout/runtimeTranscriptMessages.ts#L7-L13: build merged through the same ID-checked path used for existingMessages.
  • wework/src/components/layout/runtimeTranscriptMessages.test.ts#L17-L27: add a case with two transcript entries sharing an ID and assert only the first remains.
Proposed fix
-  const merged = [...transcriptMessages]
-  const messageIds = new Set(merged.map(message => message.id))
-
-  for (const message of existingMessages) {
+  const merged: WorkbenchMessage[] = []
+  const messageIds = new Set<string>()
+  for (const message of [...transcriptMessages, ...existingMessages]) {
     if (messageIds.has(message.id)) continue
     messageIds.add(message.id)
     merged.push(message)
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const merged = [...transcriptMessages]
const messageIds = new Set(merged.map(message => message.id))
for (const message of existingMessages) {
if (messageIds.has(message.id)) continue
messageIds.add(message.id)
merged.push(message)
const merged: WorkbenchMessage[] = []
const messageIds = new Set<string>()
for (const message of [...transcriptMessages, ...existingMessages]) {
if (messageIds.has(message.id)) continue
messageIds.add(message.id)
merged.push(message)
}
📍 Affects 2 files
  • wework/src/components/layout/runtimeTranscriptMessages.ts#L7-L13 (this comment)
  • wework/src/components/layout/runtimeTranscriptMessages.test.ts#L17-L27
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/runtimeTranscriptMessages.ts` around lines 7 -
13, Deduplicate transcript entries by ID before merging in the runtime
transcript flow: update the construction around the existing merge logic in
wework/src/components/layout/runtimeTranscriptMessages.ts lines 7-13 to process
transcriptMessages through the same ID-checked insertion path, preserving the
first occurrence and insertion order before adding existingMessages. Add a test
case in wework/src/components/layout/runtimeTranscriptMessages.test.ts lines
17-27 with duplicate transcript IDs and assert only the first entry remains.

@qdaxb
qdaxb merged commit 953f2c9 into wecode-ai:main Jul 22, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants