Skip to content

fix(wework): preserve protocol tool lifecycle state - #2213

Merged
qdaxb merged 2 commits into
mainfrom
fix/wework-protocol-streaming-status
Jul 25, 2026
Merged

qdaxb merged 2 commits into
mainfrom
fix/wework-protocol-streaming-status

Conversation

@qdaxb

@qdaxb qdaxb commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • normalize tool-call IDs and history across Responses, Chat Completions, and Anthropic protocol switches
  • keep interleaved tool lifecycle events correlated so completed tools stop rendering as running
  • restore stable streaming file-edit stats, animation, and per-file elapsed time across chunk and narrative boundaries
  • add six-way real desktop model-switch coverage and document the protocol boundary contract

Verification

  • pnpm --filter wework test (2119 passed)
  • pnpm --filter wework typecheck
  • pnpm --filter wework lint
  • cargo test --manifest-path executor/Cargo.toml --lib (388 passed, 1 ignored)
  • node wework/e2e/desktop/task-flow.e2e.mjs --model-switch-only
  • isolated real-Tauri ai:verify edit flow with streaming and completed screenshots
  • pre-push ESLint, TypeScript, unit tests, cargo fmt, cargo test --all-features --lib, and cargo clippy

Summary by CodeRabbit

  • New Features
    • Improved local-model protocol compatibility with consistent tool-call and tool-result correlation across streaming and conversions.
    • Enhanced file change UI with animated per-file add/delete statistics and more accurate per-file edit duration display.
    • Added reduced-motion support for file change animations.
  • Bug Fixes
    • Fixed tool completion ordering when text and multiple tool calls interleave.
    • Stabilized merged file-change entry IDs and duration selection to keep updates consistent during streaming.
  • Tests
    • Expanded coverage for local model switching, protocol ID normalization, and interleaved tool completion behavior.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a968d701-9562-4982-a8a3-4ae3b3ea77e0

📥 Commits

Reviewing files that changed from the base of the PR and between 29def0b and 690c9b6.

📒 Files selected for processing (14)
  • docs/en/wegent/developer-guide/local-device-architecture.md
  • docs/zh/wegent/developer-guide/local-device-architecture.md
  • executor/src/runtime_work/events.rs
  • executor/src/runtime_work/transcript.rs
  • executor/src/server/local_model_proxy/chat.rs
  • executor/src/server/local_model_proxy/mod.rs
  • wework/e2e/desktop/task-flow.e2e.mjs
  • wework/src/components/chat/MessageList.tsx
  • wework/src/components/chat/blocks/ToolBlockItem.tsx
  • wework/src/components/chat/blocks/ToolBlocksDisplay.test.tsx
  • wework/src/components/chat/blocks/ToolBlocksDisplay.tsx
  • wework/src/components/chat/blocks/toolBlockActivity.test.ts
  • wework/src/components/chat/blocks/toolBlockActivity.ts
  • wework/src/styles/globals.css

📝 Walkthrough

Walkthrough

The local model proxy normalizes tool-call identifiers across request and streaming protocol conversions. Runtime correlation uses call IDs, while Wework adds per-file streaming statistics, duration selection, animations, stable merged IDs, and expanded protocol-switch and UI test coverage.

Changes

Local model protocol correlation

Layer / File(s) Summary
Request ID normalization
executor/src/server/local_model_proxy/mod.rs, docs/*/wegent/developer-guide/local-device-architecture.md
Responses history IDs are normalized with deterministic identifiers before OpenAI and Anthropic conversions.
Streaming response ID rewriting
executor/src/server/local_model_proxy/chat.rs
Provider IDs are normalized in Responses SSE and chat SSE conversion paths, with tests for canonical emitted events.
Runtime tool block correlation
executor/src/runtime_work/events.rs, executor/src/runtime_work/transcript.rs
Command blocks use tool-call IDs, and interleaved tool/text completion is covered by a regression test.
Protocol-switch E2E verification
wework/e2e/desktop/task-flow.e2e.mjs
The E2E flow validates every cross-protocol direction, converted tool histories, invalid IDs, retries, artifacts, and protocol-specific errors.

File-change rendering

Layer / File(s) Summary
File-edit duration selection
wework/src/components/chat/blocks/ToolBlocksDisplay.tsx, wework/src/components/chat/blocks/toolBlockActivity.ts, wework/src/components/chat/MessageList.tsx
Processing rows retain source IDs, merged rows use stable identities, and per-file timing selects the latest matching edit across the full timeline.
Streaming file-change UI
wework/src/components/chat/blocks/ToolBlockItem.tsx, wework/src/styles/globals.css
File rows render animated line deltas, stat blocks, anchored durations, and reduced-motion behavior.
File-change rendering validation
wework/src/components/chat/blocks/*test*
Tests cover streaming totals, duration anchoring, merged chunks, repeated files, command interleaving, narrative boundaries, and stable IDs.

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

Sequence Diagram(s)

sequenceDiagram
  participant LocalModelClient
  participant LocalModelProxy
  participant ProtocolConverter
  participant RuntimeTimeline
  participant ToolBlocksDisplay
  LocalModelClient->>LocalModelProxy: send tool history or provider SSE
  LocalModelProxy->>LocalModelProxy: normalize request and stream IDs
  LocalModelProxy->>ProtocolConverter: convert canonical history and events
  ProtocolConverter-->>RuntimeTimeline: emit correlated tool blocks
  RuntimeTimeline->>ToolBlocksDisplay: provide processing blocks
  ToolBlocksDisplay-->>RuntimeTimeline: render selected duration and streaming statistics
Loading

Possibly related PRs

Suggested reviewers: icycrystal4

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.00% 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 matches the main change: preserving tool lifecycle state across protocol conversions in Wework.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/wework-protocol-streaming-status

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.

@qdaxb
qdaxb marked this pull request as ready for review July 25, 2026 04:15

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wework/e2e/desktop/task-flow.e2e.mjs (1)

3927-3940: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle prewarm requests in model-switch states.

handleModelProtocolMatrixResponse accepts prompt-less prewarm requests, but model_switch_source and model_switch_target require the initial prompt immediately. Handle codexRequestKind(body) === 'prewarm' in these states by returning an empty successful assistant response without advancing the stage; otherwise tool-less model selection can fail during prewarm.

Also applies to: wework/e2e/desktop/task-flow.e2e.mjs:3962-3988

🤖 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/e2e/desktop/task-flow.e2e.mjs` around lines 3927 - 3940, Update
handleModelProtocolMatrixResponse in the model_switch_source and
model_switch_target state branches to detect codexRequestKind(body) ===
'prewarm', return an empty successful assistant response, and leave the current
stage unchanged; preserve the existing initial-prompt and tool-call behavior for
non-prewarm requests.
🧹 Nitpick comments (4)
wework/src/styles/globals.css (1)

557-584: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hard-coded colors bypass the neutral-first token system and read incorrectly in dark mode.

The stat block mixes tokens (--color-border, --color-bg-surface) with a fixed white inner highlight (rgb(255 255 255 / 0.85)) and literal pastel gradients, so the chip will look inverted on dark surfaces. Also, the deletion bar is orange while the deletion count next to it is text-red-500 — same semantic state rendered in two different hues. Consider driving all four values from theme tokens.

As per coding guidelines: "Follow the neutral-first visual system ... Restrict green and teal to semantic success/addition states."

🤖 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/styles/globals.css` around lines 557 - 584, The file-change stat
styles use hard-coded highlight and addition/deletion colors that bypass theme
tokens and render inconsistently in dark mode. Update the file-change stat
container and .file-change-stat-segment.is-addition/.is-deletion rules to use
the existing neutral-first theme tokens for the border, highlight, and
backgrounds, while keeping addition green and matching deletion to the red
semantic color used by the deletion count.

Source: Coding guidelines

wework/src/components/chat/blocks/ToolBlocksDisplay.test.tsx (1)

677-684: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore the requestAnimationFrame spy in afterEach so a failing assertion cannot leak it.

animationFrame.mockRestore() only runs when every preceding expect passes; otherwise window.requestAnimationFrame stays stubbed on a fake-timer implementation for the remaining tests in this file.

♻️ Proposed cleanup
   afterEach(() => {
     vi.useRealTimers()
+    vi.restoreAllMocks()
   })

Also applies to: 727-728

🤖 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/blocks/ToolBlocksDisplay.test.tsx` around lines
677 - 684, Restore the requestAnimationFrame spy in the test cleanup path, such
as the file’s afterEach hook, so it runs even when assertions fail. Use the
existing animationFrame spy from the streaming duration tests and ensure fake
timers and system time are also cleaned up consistently without relying on
assertions reaching the end of the test.
executor/src/server/local_model_proxy/chat.rs (1)

932-972: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared "normalize-id-field-if-changed" helper. Both sites reuse normalized_responses_api_id correctly but each re-implements the same small "get str field → normalize → replace if changed" wrapper independently, risking future drift between the two copies.

  • executor/src/server/local_model_proxy/chat.rs#L932-L972: keep normalize_id_field's signature but move it (or an equivalent generic helper) into mod.rs (or a shared submodule) so both files call one implementation.
  • executor/src/server/local_model_proxy/mod.rs#L616-L679: replace the inline for field in ["id", "call_id"] { ... } body in normalize_responses_request_ids with a call to the shared helper (adjusting to track the changed-count via the helper's boolean return).
♻️ Suggested shared helper
// in mod.rs, usable from both mod.rs and chat.rs via `super::normalize_id_field`
pub(super) fn normalize_id_field(object: &mut serde_json::Map<String, Value>, field: &str) -> bool {
    let Some(raw) = object.get(field).and_then(Value::as_str) else {
        return false;
    };
    let normalized = normalized_responses_api_id(raw);
    if normalized != raw {
        object.insert(field.to_owned(), Value::String(normalized));
        return true;
    }
    false
}
🤖 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/server/local_model_proxy/chat.rs` around lines 932 - 972,
Extract the shared normalize-id-field-if-changed logic into a reusable helper in
executor/src/server/local_model_proxy/mod.rs, preserving the normalize_id_field
behavior and boolean return. Update
executor/src/server/local_model_proxy/chat.rs lines 932-972 to call the shared
helper, and replace the inline id/call_id normalization loop in
normalize_responses_request_ids at executor/src/server/local_model_proxy/mod.rs
lines 616-679 with that helper while preserving changed-count tracking.
executor/src/server/local_model_proxy/mod.rs (1)

1010-1090: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add coverage for convert_custom_tools=true combined with invalid cross-protocol ids.

The new tests exercise prepare_request("openai-responses"/"openai-chat-completions"/"anthropic-messages", false, ...), but none exercise the convert_custom_tools=true branch (which routes through chat::responses_to_responses before serialization) together with invalid ids. Since that conversion runs on the already-normalized responses_body, it should carry the normalized ids through unchanged, but this path is currently untested — worth a follow-up test to lock in the guarantee this PR is establishing.

🤖 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/server/local_model_proxy/mod.rs` around lines 1010 - 1090,
Extend the existing cross-protocol ID normalization tests to cover
prepare_request for openai-responses, openai-chat-completions, and
anthropic-messages with convert_custom_tools=true. Reuse
cross_protocol_history_with_invalid_ids, verify the converted requests preserve
valid normalized call and output IDs, and assert the expected conversion result
for each protocol.
🤖 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/e2e/desktop/task-flow.e2e.mjs`:
- Around line 6176-6189: Before the model-switch evidence is written in the
MODEL_SWITCH_ONLY branch, sanitize control.modelRequests by removing or
redacting each request’s authorization header while preserving all non-sensitive
request fields; then serialize the sanitized data to model-requests.json and
leave the verification flow unchanged.

In `@wework/src/styles/globals.css`:
- Line 497: Update the affected keyframe selectors in the relevant CSS animation
block to use percentage notation as required by keyframe-selector-notation, and
change the color value from currentColor to lowercase currentcolor to satisfy
value-keyword-case. Apply these corrections to the referenced declarations and
all additionally affected selectors.

---

Outside diff comments:
In `@wework/e2e/desktop/task-flow.e2e.mjs`:
- Around line 3927-3940: Update handleModelProtocolMatrixResponse in the
model_switch_source and model_switch_target state branches to detect
codexRequestKind(body) === 'prewarm', return an empty successful assistant
response, and leave the current stage unchanged; preserve the existing
initial-prompt and tool-call behavior for non-prewarm requests.

---

Nitpick comments:
In `@executor/src/server/local_model_proxy/chat.rs`:
- Around line 932-972: Extract the shared normalize-id-field-if-changed logic
into a reusable helper in executor/src/server/local_model_proxy/mod.rs,
preserving the normalize_id_field behavior and boolean return. Update
executor/src/server/local_model_proxy/chat.rs lines 932-972 to call the shared
helper, and replace the inline id/call_id normalization loop in
normalize_responses_request_ids at executor/src/server/local_model_proxy/mod.rs
lines 616-679 with that helper while preserving changed-count tracking.

In `@executor/src/server/local_model_proxy/mod.rs`:
- Around line 1010-1090: Extend the existing cross-protocol ID normalization
tests to cover prepare_request for openai-responses, openai-chat-completions,
and anthropic-messages with convert_custom_tools=true. Reuse
cross_protocol_history_with_invalid_ids, verify the converted requests preserve
valid normalized call and output IDs, and assert the expected conversion result
for each protocol.

In `@wework/src/components/chat/blocks/ToolBlocksDisplay.test.tsx`:
- Around line 677-684: Restore the requestAnimationFrame spy in the test cleanup
path, such as the file’s afterEach hook, so it runs even when assertions fail.
Use the existing animationFrame spy from the streaming duration tests and ensure
fake timers and system time are also cleaned up consistently without relying on
assertions reaching the end of the test.

In `@wework/src/styles/globals.css`:
- Around line 557-584: The file-change stat styles use hard-coded highlight and
addition/deletion colors that bypass theme tokens and render inconsistently in
dark mode. Update the file-change stat container and
.file-change-stat-segment.is-addition/.is-deletion rules to use the existing
neutral-first theme tokens for the border, highlight, and backgrounds, while
keeping addition green and matching deletion to the red semantic color used by
the deletion count.
🪄 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 Plus

Run ID: 9816d5dc-c8dc-4316-877b-0715a141f076

📥 Commits

Reviewing files that changed from the base of the PR and between b0ddc7f and 29def0b.

📒 Files selected for processing (14)
  • docs/en/wegent/developer-guide/local-device-architecture.md
  • docs/zh/wegent/developer-guide/local-device-architecture.md
  • executor/src/runtime_work/events.rs
  • executor/src/runtime_work/transcript.rs
  • executor/src/server/local_model_proxy/chat.rs
  • executor/src/server/local_model_proxy/mod.rs
  • wework/e2e/desktop/task-flow.e2e.mjs
  • wework/src/components/chat/MessageList.tsx
  • wework/src/components/chat/blocks/ToolBlockItem.tsx
  • wework/src/components/chat/blocks/ToolBlocksDisplay.test.tsx
  • wework/src/components/chat/blocks/ToolBlocksDisplay.tsx
  • wework/src/components/chat/blocks/toolBlockActivity.test.ts
  • wework/src/components/chat/blocks/toolBlockActivity.ts
  • wework/src/styles/globals.css

Comment thread wework/e2e/desktop/task-flow.e2e.mjs Resolved
inset-inline-start: 100%;
inset-block-start: -0.8em;
margin-inline-start: 2px;
color: currentColor;

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Stylelint violations so the lint gate passes.

keyframe-selector-notation requires percentage selectors, and value-keyword-case requires lowercase currentcolor.

🔧 Proposed fix
-    color: currentColor;
+    color: currentcolor;
   `@keyframes` file-change-number-roll {
-    from {
+    0% {
       opacity: 0.2;
       transform: translateY(-0.9em);
     }
 
     58% {
       opacity: 1;
       transform: translateY(0.08em);
     }
 
-    to {
+    100% {
       transform: translateY(0);
     }
   }

Also applies to: 506-520

🧰 Tools
🪛 Stylelint (17.14.0)

[error] 497-497: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 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/styles/globals.css` at line 497, Update the affected keyframe
selectors in the relevant CSS animation block to use percentage notation as
required by keyframe-selector-notation, and change the color value from
currentColor to lowercase currentcolor to satisfy value-keyword-case. Apply
these corrections to the referenced declarations and all additionally affected
selectors.

Source: Linters/SAST tools

@qdaxb
qdaxb force-pushed the fix/wework-protocol-streaming-status branch from 29def0b to 690c9b6 Compare July 25, 2026 04:41
@qdaxb
qdaxb merged commit 11a1a88 into main Jul 25, 2026
21 of 22 checks passed
@qdaxb
qdaxb deleted the fix/wework-protocol-streaming-status branch July 25, 2026 04:43
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.

1 participant