Skip to content

feat(webview): add batchNearby utility for smarter tool ask batching - #1005

Open
easonLiangWorldedtech wants to merge 3 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/batch-nearby-tool-asks
Open

feat(webview): add batchNearby utility for smarter tool ask batching#1005
easonLiangWorldedtech wants to merge 3 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/batch-nearby-tool-asks

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

UI tool ask batching fails when models insert API rows between tool calls

Problem

When using models like qwen, the UI shows multiple separate prompts for the same type of tool call:

Zoo wants to read this file (a.ts)
Zoo wants to read this file (b.ts)
Zoo wants to edit this file (c.ts)

Instead of a single batched prompt:

Zoo wants to read these files:
- a.ts
- b.ts

Root Cause

The existing batchConsecutive() utility only merges tool asks that are truly adjacent in the message array. However, models like qwen insert low-information messages between tool calls during streaming:

  • say: "api_req_started" — API request metadata row
  • say: "api_req_finished" — API request completion row
  • say: "text" with empty content — partial streaming rows
  • say: "reasoning" — hidden reasoning text

These messages break the consecutive chain, so batchConsecutive stops merging.

Current Code (ChatView.tsx ~line 1272)

const readFileBatched = batchConsecutive(filtered, isReadFileAsk, synthesizeReadFileBatch)
const listFilesBatched = batchConsecutive(readFileBatched, isListFilesAsk, synthesizeListFilesBatch)
const result = batchConsecutive(listFilesBatched, isEditFileAsk, synthesizeEditFileBatch)

Proposed Solution: batchNearby() utility

A new utility that merges same-type tool asks even when separated by ignorable messages. It uses three predicates:

  1. isTarget — matches the target tool type (readFile, listFiles, editFile)
  2. isIgnorableBetweenTargets — skips over low-info messages without breaking the batch
  3. isBoundary — stops merging when hitting a semantic boundary

Ignorable Messages (skipped during batching)

  • api_req_started / api_req_finished
  • Empty text rows (say: "text" with no content)
  • Reasoning rows (say: "reasoning")

Semantic Boundaries (stop merging)

  • User feedback (user_feedback, user_feedback_diff)
  • Visible assistant text (text with content)
  • Completion result (completion_result)
  • Checkpoint saved (checkpoint_saved)
  • Errors (error)
  • Context condensation (condense_context)
  • Codebase search results (codebase_search_result)

New Usage in ChatView.tsx

const readFileBatched = batchNearby(filtered, {
  isTarget: isReadFileAsk,
  isIgnorableBetweenTargets, // api_req_started/finished, empty text, reasoning
  isBoundary,                // user_feedback, visible text, completion_result, error, etc.
  synthesize: synthesizeReadFileBatch,
})

Files Changed

  • New: webview-ui/src/utils/batchNearby.ts — the batching utility function
  • New: webview-ui/src/utils/__tests__/batchNearby.spec.ts — 23 comprehensive tests
  • Modified: webview-ui/src/components/chat/ChatView.tsx — replace batchConsecutive with batchNearby

Testing

All 23 unit tests pass, covering:

  • Empty input / no matches / single match passthrough
  • Consecutive vs non-consecutive matching
  • Ignorable messages between targets (api_req_started/finished, empty text, reasoning)
  • Semantic boundaries stopping the merge (user_feedback, visible text, completion_result, checkpoint_saved, error)
  • Multiple batches separated by boundaries
  • Realistic qwen scenarios with API rows between tool calls

Long-term Improvement (separate issue)

Add toolCallGroupId metadata to backend messages so UI can use explicit grouping instead of heuristic-based merging. This would make batching work consistently across all model providers regardless of streaming format differences.

Fixes #1004

Screen Cap

image

Summary by CodeRabbit

Summary by CodeRabbit

  • Improvements
    • Updated chat streaming behavior to batch related tool requests more reliably when low-information rows appear between them.
    • Added clearer “boundary” handling to prevent mixing across key semantic moments (e.g., user feedback, visible assistant text, completion outputs, checkpoints, and errors).
  • Tests
    • Added a comprehensive new test suite covering nearby batching rules, including ignorable rows, boundary stopping, multi-batch segmentation, and immutability.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7dfd0e9-7e18-47da-aeed-44b7da62915e

📥 Commits

Reviewing files that changed from the base of the PR and between 4ef7a8c and 26b3351.

📒 Files selected for processing (5)
  • webview-ui/src/components/chat/ChatView.tsx
  • webview-ui/src/utils/__tests__/batchConsecutive.spec.ts
  • webview-ui/src/utils/__tests__/batchNearby.spec.ts
  • webview-ui/src/utils/batchConsecutive.ts
  • webview-ui/src/utils/batchNearby.ts
💤 Files with no reviewable changes (2)
  • webview-ui/src/utils/batchConsecutive.ts
  • webview-ui/src/utils/tests/batchConsecutive.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • webview-ui/src/utils/batchNearby.ts
  • webview-ui/src/utils/tests/batchNearby.spec.ts
  • webview-ui/src/components/chat/ChatView.tsx

📝 Walkthrough

Walkthrough

The chat view now batches same-type tool asks across ignorable streaming rows while preserving semantic boundaries. A generic batchNearby utility replaces batchConsecutive, with tests covering grouping, interruption, ordering, and streamed tool-call sequences.

Changes

Nearby tool-ask batching

Layer / File(s) Summary
Nearby batching utility
webview-ui/src/utils/batchNearby.ts
Adds configurable target, ignorable-row, boundary, and synthesis callbacks, then groups nearby targets while preserving interrupted and boundary rows.
Chat tool-ask integration
webview-ui/src/components/chat/ChatView.tsx
Uses nearby batching for read, list, and edit tool asks, allowing low-information streaming rows between matching requests.
Batching behavior validation
webview-ui/src/utils/__tests__/batchNearby.spec.ts
Tests passthrough, synthesis, separators, boundaries, interruption behavior, immutability, callback grouping, and realistic streamed tool-call sequences.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: navedmerchant, taltas, hannesrudolph, edelauna, jamesrobert20

Sequence Diagram(s)

sequenceDiagram
  participant ChatView
  participant batchNearby
  participant ToolBatchSynthesizer
  ChatView->>batchNearby: classify streamed tool-ask rows
  batchNearby->>batchNearby: skip ignorable rows and stop at boundaries
  batchNearby->>ToolBatchSynthesizer: synthesize grouped tool asks
  ToolBatchSynthesizer-->>batchNearby: return batched tool-ask row
  batchNearby-->>ChatView: return processed chat rows
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: introducing batchNearby for smarter tool ask batching in webview.
Description check ✅ Passed The PR description covers the issue, rationale, implementation, and testing, though it doesn't use the repository's exact template headings.
Linked Issues check ✅ Passed The changes implement the requested batching fix, including ignorable rows, semantic boundaries, and comprehensive qwen-focused tests for #1004.
Out of Scope Changes check ✅ Passed The removals and test additions are directly tied to replacing batchConsecutive with batchNearby, with no clear unrelated changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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.

@easonLiangWorldedtech
easonLiangWorldedtech marked this pull request as draft July 24, 2026 15:29
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
webview-ui/src/components/chat/ChatView.tsx 80.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@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: 1

🤖 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 `@webview-ui/src/utils/batchNearby.ts`:
- Around line 42-67: Update batchNearby in webview-ui/src/utils/batchNearby.ts
(lines 42-67) to track skipped isIgnorableBetweenTargets items in
pendingIgnorable, clear them only when a subsequent target is found, and restore
remaining items to result after finalizing the batch. Make no code change in
webview-ui/src/components/chat/ChatView.tsx (lines 1311-1328). Update the
boundary-message test in webview-ui/src/utils/__tests__/batchNearby.spec.ts
(lines 135-152) to expect four items, including the restored api_req_started
item between match-1 and visible text.
🪄 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: 421939d2-6c08-49b5-989a-af52340d01f0

📥 Commits

Reviewing files that changed from the base of the PR and between 78c1410 and bd3efaa.

📒 Files selected for processing (3)
  • webview-ui/src/components/chat/ChatView.tsx
  • webview-ui/src/utils/__tests__/batchNearby.spec.ts
  • webview-ui/src/utils/batchNearby.ts

Comment thread webview-ui/src/utils/batchNearby.ts
@easonLiangWorldedtech
easonLiangWorldedtech marked this pull request as ready for review July 24, 2026 15:54
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 24, 2026
expect(result[1].text).toBe("done")
})

test("non-ignorable non-target message stops batching", () => {

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.

This test has no ignorable message before command_output, so pendingIgnorable is empty when the else break fires — the restore path at batchNearby.ts:68–71 is never actually exercised here. What happens with input like [match-1, api_req_started, command_output, match-2]? The api_req_started should be restored to the output but no test currently covers that shape.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is already covered now. The regression test starts on batchNearby.spec.ts and uses exactly the shape you described:

The assertions verify the restore path by checking that api_req_started is preserved in the output before command_output: batchNearby.spec.ts through batchNearby.spec.ts. This exercises the restore branch in batchNearby.ts, where pending ignorable messages are pushed back when the bridge fails.

if (msg.type !== "say") return false
return (
msg.say === "api_req_started" ||
msg.say === "api_req_finished" ||

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.

Is api_req_finished reachable here? combineApiRequests (applied when building modifiedMessages) absorbs every api_req_finished into its paired api_req_started or drops it as an orphan, and visibleMessages adds a second filter at lines 1047–1050. If that's right, this arm can never fire and could be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already addressed on this branch — the isIgnorableBetweenTargets arm in ChatView no longer treats api_req_finished as ignorable. It now only allows api_req_started, empty text, and reasoning rows: ChatView.tsx.

That means an api_req_finished row cannot be used as a bridge for batching tool asks. In practice it should still be unreachable there because modifiedMessages is built through combineApiRequests(...), and visibleMessages also filters api_req_finished out before rendering: ChatView.tsx, ChatView.tsx.

m.say === "checkpoint_saved" ||
m.say === "error" ||
m.say === "condense_context"
)

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.

The production isBoundary in ChatView.tsx includes codebase_search_result but this fixture doesn't. Worth keeping them in sync — and a test with [readFileAsk, codebase_search_result_msg, readFileAsk] would confirm it stops the batch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already covered. The test fixture isBoundary now includes codebase_search_result on batchNearby.spec.ts, matching the production boundary predicate in ChatView.tsx.

There is also a regression test for the exact boundary shape starting on batchNearby.spec.ts: it uses [match-1, codebase_search_result, match-2] on lines 272-274, then asserts the messages remain separate on lines 282-285. So codebase_search_result stops batching rather than being bridged.

Comment thread webview-ui/src/utils/batchNearby.ts Outdated
/**
* Batch tool asks that are near each other, allowing ignorable messages in between.
*
* Unlike `batchConsecutive` which only merges truly adjacent items, this function

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.

Since ChatView.tsx was the only caller of batchConsecutive, that file and its spec are now dead code. Can you delete them in this PR to avoid confusing future readers. Dead code will start being enforced via knip soon.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed

@github-actions github-actions Bot added awaiting-author PR is waiting for the author to address requested changes and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Jul 26, 2026
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fix/batch-nearby-tool-asks branch from 9d4172a to bbc9723 Compare July 27, 2026 12:37
@github-actions github-actions Bot removed the awaiting-author PR is waiting for the author to address requested changes label Jul 27, 2026
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 27, 2026
taltas and others added 3 commits July 30, 2026 01:24
Replace batchConsecutive with batchNearby in ChatView.tsx to allow
merging same-type tool asks even when separated by ignorable messages
(api_req_started/finished, empty text rows, reasoning).

Semantic boundaries (user_feedback, visible text, completion_result,
checkpoint_saved, error) stop the merge, preserving correct ordering.

This fixes UX where models like qwen insert API request rows between
tool calls, causing UI to show multiple 'Zoo wants to read this file'
instead of a single batched prompt.
When a single target is followed by ignorable messages and then a
boundary (no second target found), the original code silently dropped
those ignorable items — contradicting the documented contract that all
items are preserved in-order.

Fix: track skipped ignorable items in pendingIgnorable, flush them after
the batch result so they're never lost. When bridge succeeds (second
target found), pending items are consumed into the synthesized batch as
expected.
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fix/batch-nearby-tool-asks branch from 4ef7a8c to 26b3351 Compare July 29, 2026 17:25

@edelauna edelauna 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.

Looks good - can you add some additional JSDOM state tests please

if (m.type !== "say") return false
return (
m.say === "api_req_started" ||
m.say === "api_req_finished" ||

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.

The latest commit dropped api_req_finished from production isIgnorableBetweenTargets (ChatView.tsx:1289), but the fixture here still lists it — and the tests below (~lines 350, 367, 386) rely on it being ignorable. Since it can't reach batchNearby in production anyway (combineApiRequests merges/drops it and the visibleMessages filter strips it again), should we drop it from the fixture too so the tests model the real predicate?

// - reasoning rows (hidden from user by default)
const isIgnorableBetweenTargets = (msg: ClineMessage): boolean => {
if (msg.type !== "say") return false
return msg.say === "api_req_started" || (msg.say === "text" && !msg.text?.trim()) || msg.say === "reasoning"

@edelauna edelauna Jul 30, 2026

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.

These predicates are inline closures, and the spec's copies are hand-synced — which has now drifted twice (the codebase_search_result addition, and the api_req_finished comment on the spec). Two suggestions:

  • Export isIgnorableBetweenTargets/isBoundary from a small shared module so the spec imports the real predicates instead of copies.
  • Add a ChatView JSDOM test that feeds [readFile ask, api_req_started, readFile ask] and asserts one batched approval row renders instead of two — pins the UI tool ask batching fails when models insert API rows between tool calls #1004 regression where it lives (behavior-only per webview-ui/AGENTS.md, so Vitest rather than visual or e2e).

@github-actions github-actions Bot added awaiting-author PR is waiting for the author to address requested changes and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-author PR is waiting for the author to address requested changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UI tool ask batching fails when models insert API rows between tool calls

4 participants