Skip to content

Chat Tool Streaming#174

Merged
arul28 merged 2 commits into
mainfrom
ade/chat-tool-streaming-7888245c
Apr 22, 2026
Merged

Chat Tool Streaming#174
arul28 merged 2 commits into
mainfrom
ade/chat-tool-streaming-7888245c

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Apr 22, 2026

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

Summary by CodeRabbit

  • New Features

    • Collapsed tool-call clusters now display a visual stack indicator instead of text previews for better clarity.
  • Improvements

    • Enhanced tool card deduplication and grouping for more stable timeline organization.
    • Tool card expansion behavior refined; running cards no longer auto-expand.
  • Tests

    • Added test coverage for tool-card stability and timeline grouping with logical identifiers.

Greptile Summary

This PR introduces two related improvements: (1) a logicalItemId field is now used as a stable key for tool/command/webSearch cards so that streaming updates (call → result) collapse onto the same card rather than creating duplicates, and (2) the collapsed group view replaces a text-based mini-preview with a purely visual stacked-card backdrop effect.

The deduplication logic is well-factored into workStableTimelineItemId overloads with appropriate test coverage, and the intentional fileChange carve-out (keeping raw itemId to preserve per-file identity under a shared logicalItemId) is documented and regression-tested.

Confidence Score: 5/5

Safe to merge — changes are well-scoped, covered by regression tests, and prior review concerns have been addressed.

All findings from previous review threads are addressed (auto-expand preserved for running cards, backdrop padding added, shared-function comment noted). No new P0/P1 issues found. Good test coverage across both code paths (event mapping and transcript parser).

No files require special attention.

Important Files Changed

Filename Overview
apps/ios/ADE/Views/Work/WorkEventMapping.swift Adds two workStableTimelineItemId overloads and wires them into toolCall, toolResult, webSearch, and command event mappings; fileChange intentionally keeps raw itemId.
apps/ios/ADE/Views/Work/WorkTranscriptParser.swift Reads logicalItemId from the JSON envelope and applies workStableTimelineItemId for tool/command/webSearch events; file_change is correctly left on raw itemId.
apps/ios/ADE/Views/Work/WorkChatRichCardViews.swift Replaces the text-based collapsedPreview with a purely decorative stack-backdrop effect; VStack wrapped in ZStack with 10pt bottom padding to accommodate backdrop offsets.
apps/ios/ADETests/ADETests.swift Adds 5 new test cases covering logical-ID stability for tool cards, file_change per-file identity, timeline grouping, and transcript-parser path end-to-end.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[AgentChatEvent / JSON transcript] --> B{Event type}
    B -->|toolCall / toolResult\nwebSearch / command| C[workStableTimelineItemId\nitemId + logicalItemId]
    B -->|fileChange| D[Keep raw itemId\nper-file identity preserved]
    C --> E{logicalItemId\nnon-empty?}
    E -->|Yes| F[Use logicalItemId\nas stable card key]
    E -->|No| G[Fall back to itemId\nor workFallbackItemID]
    F --> H[buildWorkToolCards\nbuildWorkCommandCards\nbuildWorkTimelineSnapshot]
    G --> H
    D --> I[buildWorkFileChangeCards\none card per file]
    H --> J[WorkToolGroupCardView\nCollapsed: stack backdrop\nExpanded: full member rows]
    I --> J
Loading

Reviews (2): Last reviewed commit: "Address PR #174 review: keep file-change..." | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ade Ready Ready Preview, Comment Apr 22, 2026 11:23pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

Updates iOS work chat to display visual stack hints instead of text previews in collapsed tool-call clusters. Adds stable timeline item ID logic that prefers logicalItemId over itemId in event mapping and parsing. Extends multiple event types and introduces test coverage for tool card deduplication and timeline grouping behavior.

Changes

Cohort / File(s) Summary
Tool Card UI
apps/ios/ADE/Views/Work/WorkChatRichCardViews.swift
Replaces mini text previews in collapsed tool-call stacks with a purely visual collapsedStackBackdrop. Converts container to ZStack with conditional rendering and adjusts padding. Modifies per-member expansion to prevent auto-expand of running cards.
Stable Item ID Logic
apps/ios/ADE/Views/Work/WorkEventMapping.swift, apps/ios/ADE/Views/Work/WorkTranscriptParser.swift
Adds helper function workStableTimelineItemId() that prefers trimmed, non-empty logicalItemId over raw itemId. Extends toolCall, toolResult, webSearch, command, and fileChange event cases to extract and use stable item IDs throughout the mapping and parsing pipeline.
Test Coverage
apps/ios/ADETests/ADETests.swift
Introduces three new XCTest cases validating tool-card deduplication by logicalItemId, status/args/result presence, and timeline grouping of consecutive tool cards into collapsed toolGroup snapshots.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

ios

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Chat Tool Streaming' does not align with the actual changes, which focus on stabilizing timeline item IDs and collapsing tool card clusters. The commit message 'Stabilize iOS work timeline' better reflects the real work. Revise the title to accurately reflect the primary change, such as 'Stabilize tool card timeline grouping and identity' or 'Deduplicate tool cards using logical item IDs'.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 ade/chat-tool-streaming-7888245c

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
apps/ios/ADE/Views/Work/WorkChatRichCardViews.swift (1)

197-217: Minor: drop the explicit else { EmptyView() }.

SwiftUI's @ViewBuilder already treats a missing else branch as empty, so the explicit EmptyView() branch is noise. The rest of the ZStack/backdrop layout with allowsHitTesting(false) and the compensating .padding(.bottom, 6) reads well.

♻️ Optional cleanup
     ZStack(alignment: .bottom) {
       if !effectiveExpanded && group.count > 1 {
         collapsedStackBackdrop
-      } else {
-        EmptyView()
       }

       VStack(alignment: .leading, spacing: 10) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Work/WorkChatRichCardViews.swift` around lines 197 - 217,
Remove the redundant else branch returning EmptyView() inside the ZStack: locate
the conditional that checks `if !effectiveExpanded && group.count > 1` (used
with `collapsedStackBackdrop`) and delete the `else { EmptyView() }` branch so
the `@ViewBuilder` implicitly treats the missing else as empty; keep the
surrounding ZStack, `collapsedStackBackdrop`, `effectiveExpanded`, and the
trailing `.padding(.bottom, !effectiveExpanded && group.count > 1 ? 6 : 0)`
unchanged.
apps/ios/ADE/Views/Work/WorkTranscriptParser.swift (1)

32-34: Consider reusing workStableTimelineItemId for consistency.

This inline logic duplicates the workStableTimelineItemId helper in apps/ios/ADE/Views/Work/WorkEventMapping.swift (lines 5-8). Since itemId here is optional while the helper takes String, a small wrapper keeps the two call sites in lockstep and avoids drift if the preference rule changes later:

♻️ Proposed refactor
       let itemId = eventDict["itemId"] as? String
-      let logicalItemId = (eventDict["logicalItemId"] as? String)?
-        .trimmingCharacters(in: .whitespacesAndNewlines)
-      let stableToolItemId = (logicalItemId?.isEmpty == false ? logicalItemId : nil) ?? itemId
+      let logicalItemId = (eventDict["logicalItemId"] as? String)?
+        .trimmingCharacters(in: .whitespacesAndNewlines)
+      let stableToolItemId: String? = {
+        if let logical = logicalItemId, !logical.isEmpty { return logical }
+        return itemId
+      }()

Or, if you expose an optional overload of the helper, route both files through it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Work/WorkTranscriptParser.swift` around lines 32 - 34, The
logic computing stableToolItemId duplicates workStableTimelineItemId; replace it
by calling that helper to keep behavior consistent: wrap or add an optional
overload around workStableTimelineItemId so you can pass itemId (which is
Optional<String>) from WorkTranscriptParser (where logicalItemId and itemId are
available), and then use the helper to compute stableToolItemId instead of
inlining the trimming/isEmpty logic; update the callsite in WorkTranscriptParser
to call the new wrapper/overload and remove the duplicated logic around
logicalItemId and itemId.
apps/ios/ADETests/ADETests.swift (1)

5689-5706: Tighten the payload-preservation assertions.

These tests prove dedupe/status, but XCTAssertNotNil would still pass if args/result text were wrong or empty. Please assert the expected payload content on both direct event and parser paths.

🧪 Proposed assertion tightening
     XCTAssertEqual(cards.count, 1)
     XCTAssertEqual(cards.first?.id, "tool-logical-1")
     XCTAssertEqual(cards.first?.status, .completed)
-    XCTAssertNotNil(cards.first?.argsText)
-    XCTAssertNotNil(cards.first?.resultText)
+    XCTAssertTrue(cards.first?.argsText?.contains("pwd") == true)
+    XCTAssertTrue(cards.first?.resultText?.contains("/tmp/project") == true)
@@
     XCTAssertEqual(cards.count, 1)
     XCTAssertEqual(cards.first?.id, "tool-logical-1")
     XCTAssertEqual(cards.first?.status, .completed)
+    XCTAssertTrue(cards.first?.argsText?.contains("pwd") == true)
+    XCTAssertTrue(cards.first?.resultText?.contains("/tmp/project") == true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADETests/ADETests.swift` around lines 5689 - 5706, Replace the loose
XCTAssertNotNil assertions with exact-content checks: assert that
cards.first?.argsText equals the expected args string (e.g., the JSON or
"cmd":"pwd" payload) and that cards.first?.resultText equals the expected result
string (e.g., "/tmp/project"), and also add assertions against the parsed
transcript (the output of parseWorkChatTranscript(raw)) to verify the
corresponding tool_call and tool_result event payloads contain those exact
args/result values; update
testParseWorkChatTranscriptUsesLogicalItemIdForStableToolCards to check both the
parsed transcript entries and the built WorkToolCard fields (using
parseWorkChatTranscript, buildWorkToolCards, raw, transcript, and cards
identifiers).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/ios/ADE/Views/Work/WorkChatRichCardViews.swift`:
- Around line 197-217: Remove the redundant else branch returning EmptyView()
inside the ZStack: locate the conditional that checks `if !effectiveExpanded &&
group.count > 1` (used with `collapsedStackBackdrop`) and delete the `else {
EmptyView() }` branch so the `@ViewBuilder` implicitly treats the missing else as
empty; keep the surrounding ZStack, `collapsedStackBackdrop`,
`effectiveExpanded`, and the trailing `.padding(.bottom, !effectiveExpanded &&
group.count > 1 ? 6 : 0)` unchanged.

In `@apps/ios/ADE/Views/Work/WorkTranscriptParser.swift`:
- Around line 32-34: The logic computing stableToolItemId duplicates
workStableTimelineItemId; replace it by calling that helper to keep behavior
consistent: wrap or add an optional overload around workStableTimelineItemId so
you can pass itemId (which is Optional<String>) from WorkTranscriptParser (where
logicalItemId and itemId are available), and then use the helper to compute
stableToolItemId instead of inlining the trimming/isEmpty logic; update the
callsite in WorkTranscriptParser to call the new wrapper/overload and remove the
duplicated logic around logicalItemId and itemId.

In `@apps/ios/ADETests/ADETests.swift`:
- Around line 5689-5706: Replace the loose XCTAssertNotNil assertions with
exact-content checks: assert that cards.first?.argsText equals the expected args
string (e.g., the JSON or "cmd":"pwd" payload) and that cards.first?.resultText
equals the expected result string (e.g., "/tmp/project"), and also add
assertions against the parsed transcript (the output of
parseWorkChatTranscript(raw)) to verify the corresponding tool_call and
tool_result event payloads contain those exact args/result values; update
testParseWorkChatTranscriptUsesLogicalItemIdForStableToolCards to check both the
parsed transcript entries and the built WorkToolCard fields (using
parseWorkChatTranscript, buildWorkToolCards, raw, transcript, and cards
identifiers).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9ddd8b62-0279-48c4-9639-fb86be834273

📥 Commits

Reviewing files that changed from the base of the PR and between 7bcc088 and 57c9c91.

📒 Files selected for processing (4)
  • apps/ios/ADE/Views/Work/WorkChatRichCardViews.swift
  • apps/ios/ADE/Views/Work/WorkEventMapping.swift
  • apps/ios/ADE/Views/Work/WorkTranscriptParser.swift
  • apps/ios/ADETests/ADETests.swift

Comment thread apps/ios/ADE/Views/Work/WorkChatRichCardViews.swift Outdated
Comment thread apps/ios/ADE/Views/Work/WorkTranscriptParser.swift Outdated
Comment thread apps/ios/ADE/Views/Work/WorkChatRichCardViews.swift
Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 1 comment

case .fileChange(let path, let diff, let kind, let itemId, _, let turnId, let status):
return .fileChange(path: path, diff: diff, kind: kind.rawValue, status: toolStatus(from: status ?? "running"), itemId: itemId, turnId: turnId)
case .fileChange(let path, let diff, let kind, let itemId, let logicalItemId, let turnId, let status):
return .fileChange(path: path, diff: diff, kind: kind.rawValue, status: toolStatus(from: status ?? "running"), itemId: workStableTimelineItemId(itemId: itemId, logicalItemId: logicalItemId), turnId: turnId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

Normalizing file_change events to logicalItemId turns distinct per-file updates into the same iOS card key. The changed code now does:

// apps/ios/ADE/Views/Work/WorkEventMapping.swift
case .fileChange(let path, let diff, let kind, let itemId, let logicalItemId, let turnId, let status):
  return .fileChange(path: path, diff: diff, kind: kind.rawValue, status: toolStatus(from: status ?? "running"), itemId: workStableTimelineItemId(itemId: itemId, logicalItemId: logicalItemId), turnId: turnId)

I verified that buildWorkFileChangeCards stores one WorkFileChangeCardModel per itemId (@apps/ios/ADE/Views/Work/WorkTimelineHelpers.swift:376-393), while the desktop emitter can intentionally produce multiple file changes with unique raw IDs but one shared logical ID (itemId: ${part.id}:${file}, logicalItemId: part.id in @apps/desktop/src/main/services/chat/agentChatService.ts:7629-7636). After this change, those files all collapse onto the same iOS itemId, so earlier paths are overwritten and only the last changed file remains visible in both live mapping and transcript parsing. Keep raw itemId for file_change until the iOS file-change card builder can merge multiple paths under one logical item.

- File-change events deliberately keep raw itemId: OpenCode's patch emitter
  produces one event per file with a shared logicalItemId but distinct raw
  IDs. Collapsing to logicalItemId was dropping all but the last file.
- Restore streaming progress by auto-expanding running tool cards inside
  groups (localExpanded || .running), same as before the timeline cleanup.
- Route WorkTranscriptParser through a new optional-itemId overload of
  workStableTimelineItemId so both paths share the resolution policy.
- Drop redundant EmptyView else branch in the collapsed-stack ZStack.
- Bump collapsed-stack bottom padding to 10pt so the y:8 backdrop offset
  stays inside the layout on smaller devices.
- Add regression tests covering distinct files under a shared logicalItemId
  through both the direct-event and transcript-parser paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28 arul28 merged commit 0112f51 into main Apr 22, 2026
23 checks passed
@arul28 arul28 deleted the ade/chat-tool-streaming-7888245c branch April 23, 2026 16:41
@coderabbitai coderabbitai Bot mentioned this pull request Apr 29, 2026
5 tasks
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