Skip to content

feat: show elapsed time for tool calls and reasoning box in AI chat - #508

Closed
lixun910 wants to merge 8 commits into
mainfrom
507-enh-show-elapsed-time-for-tool-calls-in-ai-chat-ui
Closed

lixun910 wants to merge 8 commits into
mainfrom
507-enh-show-elapsed-time-for-tool-calls-in-ai-chat-ui

Conversation

@lixun910

@lixun910 lixun910 commented Mar 31, 2026 •

Copy link
Copy Markdown
Collaborator
Screenshot 2026-03-30 at 11 27 15 PM

Summary by CodeRabbit

  • New Features
    • Tool execution timings are captured, persisted with assistant messages, and injected on save/error so elapsed displays survive reloads/exports.
    • Live/frozen elapsed-time badges for individual tool calls, agent sub-steps, and grouped tool sequences.
    • Reasoning/group headers now show processing state, live subtitles, and support custom tool display name mappings; default label changed to "Processing".
    • Added compact duration formatting and a hook to compute elapsed-time strings.

@lixun910 lixun910 linked an issue Mar 31, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Mar 31, 2026 •

Copy link
Copy Markdown
Contributor

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
📝 Walkthrough

Walkthrough

Adds per-tool-call timing: records startedAt/completedAt during tool execution, stores timings in AI state and restores them from persisted assistant message metadata, injects timings into message metadata on persist/error, and surfaces formatted elapsed durations in the UI via a new hook and utilities.

Changes

Cohort / File(s) Summary
Types & Utils
packages/ai-core/src/types.ts, packages/utils/src/helpers.ts, packages/utils/src/index.ts
Added ToolTimingEntry and AssistantMessageMetadata types; added formatShortDuration(ms) and re-exported it.
AI State & Persistence
packages/ai-core/src/AiSlice.ts
Added ai.toolTimings map, setToolTiming/getToolTimings methods, collector to restore timings from persisted session uiMessages, and reseed toolTimings on setConfig.
Agent Streaming / Tool Call Timing
packages/ai-core/src/agents/AgentUtils.ts
Added optional input, startedAt, completedAt on UIMessageChunk/AgentToolCall; processAgentStream sets startedAt when first-seen, attaches input (parsing JSON), and sets completedAt on success/error chunks.
Transport: Capture & Embed Timings
packages/ai-core/src/chatTransport.ts
Added writeToolTimingsToMetadata; record started/completed times around tool.execute; ensure incomplete timings are closed on errors/aborts; inject toolTimings into assistant message metadata before persisting or on errors.
Hook: Elapsed Time
packages/ai-core/src/hooks/useElapsedTime.ts
New useElapsedTime(isRunning, startedAt?, completedAt?) hook that updates every second when running and returns formatted elapsed string or undefined.
Tool Grouping & Titles
packages/ai-core/src/hooks/useToolGrouping.tsx
ToolGroup now includes isRunning, toolCallIds, agentToolCallId; useToolGrouping accepts toolDisplayNames; title generation adjusted for running/completed states and truncation helper truncateToFit added.
UI: Components Updated
packages/ai-core/src/components/...
packages/ai-core/src/components/GroupedMessageParts.tsx, packages/ai-core/src/components/ReasoningBox.tsx, packages/ai-core/src/components/ToolCallInfo.tsx, packages/ai-core/src/components/ToolPartRenderer.tsx, packages/ai-core/src/components/AnalysisResult.tsx, packages/ai-core/src/components/AnalysisResultsContainer.tsx, packages/ai-core/src/components/AnalysisResult.tsx
Prop surfaces extended (containerWidth, isRunning, toolCallIds, stableKey, hideElapsed, toolDisplayNames); ReasoningBox title default changed to "Processing"; components read ai.toolTimings and use useElapsedTime to render elapsed indicators and live subtitles.
Tool Part / Agent UI Enhancements
packages/ai-core/src/components/ToolPartRenderer.tsx
Added AgentStepElapsed live/static badge; switched agent progress shapes to AgentToolCall[]; per-sub-step reasoning rendering; pass stableKey to ToolCallInfo.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client/UI
    participant Transport as chatTransport
    participant Store as AI Store (ai.toolTimings)
    participant Tool as Tool Executor
    participant Hook as useElapsedTime

    UI->>Transport: initiate tool call
    Transport->>Store: setToolTiming(toolCallId, { startedAt })
    Transport->>Tool: tool.execute(...)
    Tool-->>Transport: result / error
    Transport->>Store: setToolTiming(toolCallId, { startedAt, completedAt })
    Store->>Hook: ai.toolTimings read
    Hook->>UI: formatted elapsed text (updates while running)
    UI->>Transport: persist session
    Transport->>Store: getToolTimings()
    Transport->>Transport: writeToolTimingsToMetadata(messages, toolTimings)
    Transport->>Persist: save messages with metadata
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • ilyabo
  • mihajlo-stojkovic
  • dmitriy-kostianetskiy

"I hopped in code at break of dawn,
Timers set as tools raced on,
Ticks recorded, finished chimes,
Elapsed time shows in UI lines,
🐇⏱️"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature: displaying elapsed time for tool calls and reasoning boxes in the AI chat interface, which aligns with the changeset's primary objective.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 507-enh-show-elapsed-time-for-tool-calls-in-ai-chat-ui

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.

@lixun910 lixun910 changed the title Feat: show elapsed time for tool calls and reasoning box in AI chat feat: show elapsed time for tool calls and reasoning box in AI chat Mar 31, 2026
@lixun910
lixun910 requested a review from ilyabo March 31, 2026 06:28

@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

Caution

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

⚠️ Outside diff range comments (2)
packages/ai-core/src/chatTransport.ts (2)

303-364: ⚠️ Potential issue | 🟠 Major

Record completedAt for failed tool executions too.

The new timing flow only writes completedAt after a successful tool.execute. If tool.execute throws, the entry stays as {startedAt} even though the UI part becomes output-error, which makes elapsed time render-time dependent instead of execution-time dependent and persists incomplete timing data. Stamp completedAt in the catch path before emitting the error result.

Suggested fix
     }: {
       sessionId: string;
       toolCall: ToolCall;
       addToolResult?: AddToolResult;
     }) => {
       const {input, toolCallId, toolName} = toolCall;
+      let startedAt: number | undefined;
       try {
         const state = store.getState();
         const session = getSessionById(store, sessionId);
         state.ai.setToolCallSession(toolCallId, sessionId);
@@
         const tool = state.ai.tools[toolName];
         if (tool?.execute) {
           const sessionMessages = (session?.uiMessages ?? []) as UIMessage[];
-          const startedAt = Date.now();
+          startedAt = Date.now();
           state.ai.setToolTiming(toolCallId, {startedAt});
@@
       } catch (error) {
+        if (startedAt != null) {
+          store.getState().ai.setToolTiming(toolCallId, {
+            startedAt,
+            completedAt: Date.now(),
+          });
+        }
         const isAbortError = error instanceof ToolAbortError;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/src/chatTransport.ts` around lines 303 - 364, The timing
entry only sets completedAt on success; update the catch block to also record
completedAt by calling state.ai.setToolTiming(toolCallId, {startedAt:
existingStartedAt?, completedAt: Date.now()}) before emitting the error result
so failed executions have finished timestamps; locate the try/catch around
tool.execute in chatTransport.ts (references: tool.execute,
state.ai.setToolTiming, addToolResult, ToolAbortError, TOOL_CALL_CANCELLED,
getErrorMessageForDisplay, and store.getState().ai.setToolCallSession) and
ensure completedAt is stamped prior to calling addToolResult and rethrowing the
error.

380-447: ⚠️ Potential issue | 🟠 Major

Persist timings when the run ends in onChatError as well.

writeToolTimingsToMetadata(...) is only invoked from onChatFinish. When the transport falls through onChatError after one or more tool calls have already run, targetSession.uiMessages gets stored without metadata.toolTimings, and packages/ai-core/src/AiSlice.ts has nothing to rehydrate from on reload. Please run the same enrichment step before writing messages in the error handler.

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

In `@packages/ai-core/src/chatTransport.ts` around lines 380 - 447, When handling
the aborted/error path in onChatError (the branch that builds completedMessages
from sessionMessages and calls state.ai.setSessionUiMessages), run the same
enrichment step as onChatFinish: call fixIncompleteToolCalls to produce
completedMessages (already present) and then call
writeToolTimingsToMetadata(completedMessages, state.ai.getToolTimings()) before
state.ai.setSessionUiMessages(sessionId, completedMessages); this ensures
metadata.toolTimings is persisted for rehydration. Use the existing helpers
fixIncompleteToolCalls and writeToolTimingsToMetadata and keep the rest of the
abort/error handling (setIsRunning, setAbortController, and the produce(...)
update) unchanged.
🧹 Nitpick comments (1)
packages/ai-core/src/components/ToolCallInfo.tsx (1)

62-80: Give the disclosure trigger explicit button semantics.

Line 62 toggles a collapsible region, but the trigger still lacks type="button" and doesn't expose its open state. Adding type="button" plus aria-expanded (and ideally aria-controls) makes this safe inside forms and announces the expand/collapse state to assistive tech.

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

In `@packages/ai-core/src/components/ToolCallInfo.tsx` around lines 62 - 80, The
disclosure button in ToolCallInfo (the <button> that calls toggle and renders
ChevronDownIcon/ChevronRightIcon using isExpanded, toolName, showElapsed,
elapsedText) needs explicit button semantics: add type="button" to prevent
accidental form submissions, add aria-expanded={isExpanded} to expose open
state, and add aria-controls pointing to the collapsible region's id (create or
reuse a stable id for the content panel) so assistive tech can associate the
trigger with the controlled region.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ai-core/src/components/ToolPartRenderer.tsx`:
- Around line 109-121: The timer badge in ToolPartRenderer currently calculates
elapsed inline and won't auto-update for pending tool calls; extract the badge
into a new child component (or reuse the pattern from ToolCallInfo) that accepts
toolCall.startedAt and toolCall.completedAt and uses the existing useElapsedTime
hook to drive re-renders, then render formatted output via
formatShortDuration(elapsed). Specifically, replace the inline IIFE that
computes elapsed with a component (e.g., ToolPartElapsedBadge) which imports
useElapsedTime, computes elapsed = useElapsedTime(startedAt, completedAt),
decides visibility with completedAt != null || elapsed >= 1000, and returns the
same <span className="ml-1 text-[0.85em]
text-gray-400">{formatShortDuration(elapsed)}</span> so pending tool calls
update live.

---

Outside diff comments:
In `@packages/ai-core/src/chatTransport.ts`:
- Around line 303-364: The timing entry only sets completedAt on success; update
the catch block to also record completedAt by calling
state.ai.setToolTiming(toolCallId, {startedAt: existingStartedAt?, completedAt:
Date.now()}) before emitting the error result so failed executions have finished
timestamps; locate the try/catch around tool.execute in chatTransport.ts
(references: tool.execute, state.ai.setToolTiming, addToolResult,
ToolAbortError, TOOL_CALL_CANCELLED, getErrorMessageForDisplay, and
store.getState().ai.setToolCallSession) and ensure completedAt is stamped prior
to calling addToolResult and rethrowing the error.
- Around line 380-447: When handling the aborted/error path in onChatError (the
branch that builds completedMessages from sessionMessages and calls
state.ai.setSessionUiMessages), run the same enrichment step as onChatFinish:
call fixIncompleteToolCalls to produce completedMessages (already present) and
then call writeToolTimingsToMetadata(completedMessages,
state.ai.getToolTimings()) before state.ai.setSessionUiMessages(sessionId,
completedMessages); this ensures metadata.toolTimings is persisted for
rehydration. Use the existing helpers fixIncompleteToolCalls and
writeToolTimingsToMetadata and keep the rest of the abort/error handling
(setIsRunning, setAbortController, and the produce(...) update) unchanged.

---

Nitpick comments:
In `@packages/ai-core/src/components/ToolCallInfo.tsx`:
- Around line 62-80: The disclosure button in ToolCallInfo (the <button> that
calls toggle and renders ChevronDownIcon/ChevronRightIcon using isExpanded,
toolName, showElapsed, elapsedText) needs explicit button semantics: add
type="button" to prevent accidental form submissions, add
aria-expanded={isExpanded} to expose open state, and add aria-controls pointing
to the collapsible region's id (create or reuse a stable id for the content
panel) so assistive tech can associate the trigger with the controlled region.
🪄 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: b59b295b-33ea-4ab4-b1e2-28618916c283

📥 Commits

Reviewing files that changed from the base of the PR and between 1bb4642 and 9d66877.

📒 Files selected for processing (12)
  • packages/ai-core/src/AiSlice.ts
  • packages/ai-core/src/agents/AgentUtils.ts
  • packages/ai-core/src/chatTransport.ts
  • packages/ai-core/src/components/GroupedMessageParts.tsx
  • packages/ai-core/src/components/ReasoningBox.tsx
  • packages/ai-core/src/components/ToolCallInfo.tsx
  • packages/ai-core/src/components/ToolPartRenderer.tsx
  • packages/ai-core/src/hooks/useElapsedTime.ts
  • packages/ai-core/src/hooks/useToolGrouping.tsx
  • packages/ai-core/src/types.ts
  • packages/utils/src/helpers.ts
  • packages/utils/src/index.ts

Comment thread packages/ai-core/src/components/ToolPartRenderer.tsx Outdated

@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 the current code and only fix it if needed.

Inline comments:
In `@packages/ai-core/src/AiSlice.ts`:
- Around line 490-497: setConfig currently replaces draft.ai.toolTimings with
collectToolTimingsFromConfig(config), which discards any in-flight timings
(entries with startedAt and no completedAt) if called mid-execution; instead,
merge restoredToolTimings with the existing state to preserve in-flight entries:
inside setConfig’s produce callback (where you set draft.ai.config and
draft.ai.toolTimings), compute a merged list by taking restoredToolTimings and
then re-attaching any timings from the prior state.toolTimings that have
startedAt && !completedAt (matching by a unique id or name if available) so
in-flight timings are not lost when setConfig runs. Ensure you reference
setConfig, collectToolTimingsFromConfig, draft.ai.config and
draft.ai.toolTimings when making the change.
🪄 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: 2a3330e7-ed9c-4437-8c06-8158fc6cef29

📥 Commits

Reviewing files that changed from the base of the PR and between 9d66877 and 6a922ba.

📒 Files selected for processing (3)
  • packages/ai-core/src/AiSlice.ts
  • packages/ai-core/src/chatTransport.ts
  • packages/ai-core/src/components/ToolPartRenderer.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ai-core/src/chatTransport.ts

Comment thread packages/ai-core/src/AiSlice.ts
isRunning = false,
toolCallIds,
}) => {
const displayTitle = title ?? 'Thought';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For the generic label I would suggest to use "Worked for 4m 18s" instead of just "Thought".

Ideally, if we could ask AI to generate a short phrase explaining what's happening, like "Finding datasets", "Analyzing data", "Creating a map", that would be fantastic.

See also this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good! Can I address this in a new PR and land this PR for now? THanks!

@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

🧹 Nitpick comments (1)
packages/ai-core/src/hooks/useToolGrouping.tsx (1)

34-35: Avoid a fresh {} default for toolDisplayNames.

Because AnalysisResult forwards this prop as undefined when it is omitted, the default object here is recreated on every render. That makes the useMemo dependency change every time, so regrouping reruns unconditionally.

♻️ Proposed fix
+const EMPTY_TOOL_DISPLAY_NAMES: Record<string, string> = {};
+
 export function useToolGrouping(
   uiMessageParts: UIMessagePart[],
   containerWidth: number = 0,
   exclude: string[] = [],
-  toolDisplayNames: Record<string, string> = {},
+  toolDisplayNames: Record<string, string> = EMPTY_TOOL_DISPLAY_NAMES,
 ): ToolGroup[] {

Also applies to: 199-199

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

In `@packages/ai-core/src/hooks/useToolGrouping.tsx` around lines 34 - 35, The
default parameter toolDisplayNames: Record<string,string> = {} in
useToolGrouping creates a new object each render; change the signature to accept
toolDisplayNames?: Record<string,string> (or default to a module‑level constant)
and introduce a single stable EMPTY_TOOL_DISPLAY_NAMES = Object.freeze({}) at
top scope, then use that constant inside useToolGrouping (referencing
toolDisplayNames ?? EMPTY_TOOL_DISPLAY_NAMES) so the memo deps are stable; make
the same change for the other occurrence that uses the same default for
toolDisplayNames.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ai-core/src/hooks/useToolGrouping.tsx`:
- Around line 173-179: The header should only show "Running ..." when this
group's parts are actually still pending, so change the running-condition logic
to require both that at least one tool part is not in an output state and that
the group's overall isRunning flag remains true; specifically, use the existing
toolParts check (groupIsRunning = toolParts.some(p => !((p as any).state ===
'output-available' || (p as any).state === 'output-error'))) AND combine it with
the group's isRunning/hasMoreToolsAfter-derived flag when deciding the
header/display; apply the same fix to the other occurrences noted (around lines
189-190 and 319-329) so finished groups (all parts in output-*) never render the
"Running ${toolLabel}..." header even if hasMoreToolsAfter is true.
- Around line 273-287: The code currently deduplicates by raw tool name (using
toolNamesList) which can produce duplicate headers when different names map to
the same display label and can produce an empty title if all names become
'unknown'; change the logic in the block that builds uniqueDisplayNames (working
with actualToolParts, getToolName, toolDisplayNames, formatToolNameFallback) to
compute the final label first (const label = toolDisplayNames[name] ??
formatToolNameFallback(name)), then deduplicate based on that label (use
seen.has(label)), pushing the label into uniqueDisplayNames only if not seen,
and ensure you supply a generic fallback label (e.g., 'Tool') when the computed
label is empty so downstream titles are never empty.

---

Nitpick comments:
In `@packages/ai-core/src/hooks/useToolGrouping.tsx`:
- Around line 34-35: The default parameter toolDisplayNames:
Record<string,string> = {} in useToolGrouping creates a new object each render;
change the signature to accept toolDisplayNames?: Record<string,string> (or
default to a module‑level constant) and introduce a single stable
EMPTY_TOOL_DISPLAY_NAMES = Object.freeze({}) at top scope, then use that
constant inside useToolGrouping (referencing toolDisplayNames ??
EMPTY_TOOL_DISPLAY_NAMES) so the memo deps are stable; make the same change for
the other occurrence that uses the same default for toolDisplayNames.
🪄 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: 5120388f-cf85-41ea-aad1-12bbd3f17837

📥 Commits

Reviewing files that changed from the base of the PR and between 6a922ba and 8a8ebfc.

📒 Files selected for processing (4)
  • packages/ai-core/src/components/AnalysisResult.tsx
  • packages/ai-core/src/components/AnalysisResultsContainer.tsx
  • packages/ai-core/src/components/ReasoningBox.tsx
  • packages/ai-core/src/hooks/useToolGrouping.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ai-core/src/components/ReasoningBox.tsx

Comment thread packages/ai-core/src/hooks/useToolGrouping.tsx
Comment thread packages/ai-core/src/hooks/useToolGrouping.tsx
@lixun910

lixun910 commented Apr 1, 2026

Copy link
Copy Markdown
Collaborator Author

Update the reasoning box wording:

Screen.Recording.2026-03-31.at.10.45.06.AM.mp4

@lixun910
lixun910 requested a review from ilyabo April 1, 2026 04:30

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

🧹 Nitpick comments (1)
packages/ai-core/src/chatTransport.ts (1)

39-78: Consider documenting the mutation expectation or returning a new array.

This function mutates msg.metadata in-place. It works correctly because all call sites pass the result of fixIncompleteToolCalls(), which returns cloned message objects. However, this implicit contract could be fragile if a future caller passes the original messages array.

Consider either:

  1. Adding a JSDoc note that the function mutates messages and expects cloned input, or
  2. Returning a new array with updated metadata (more defensive but adds allocation overhead)

The current implementation is functional—this is just a suggestion for maintainability.

📝 Option 1: Add documentation
 /**
  * Write tool timings from the store into assistant message metadata so they
  * survive serialization.
+ *
+ * **Note:** This function mutates `messages` in place. Callers should pass
+ * cloned messages (e.g., from `fixIncompleteToolCalls`).
  */
 function writeToolTimingsToMetadata(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/src/chatTransport.ts` around lines 39 - 78, The function
writeToolTimingsToMetadata mutates each UIMessage's msg.metadata in-place and
relies on callers (currently fixIncompleteToolCalls) to supply cloned messages;
add a clear JSDoc above writeToolTimingsToMetadata stating that it mutates the
messages array and expects cloned UIMessage objects (reference UIMessage,
AssistantMessageMetadata, and fixIncompleteToolCalls) so callers know not to
pass original arrays, or alternatively refactor writeToolTimingsToMetadata to be
pure by returning a new UIMessage[] with updated metadata (update call sites
accordingly) if you prefer the defensive non-mutating approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/ai-core/src/chatTransport.ts`:
- Around line 39-78: The function writeToolTimingsToMetadata mutates each
UIMessage's msg.metadata in-place and relies on callers (currently
fixIncompleteToolCalls) to supply cloned messages; add a clear JSDoc above
writeToolTimingsToMetadata stating that it mutates the messages array and
expects cloned UIMessage objects (reference UIMessage, AssistantMessageMetadata,
and fixIncompleteToolCalls) so callers know not to pass original arrays, or
alternatively refactor writeToolTimingsToMetadata to be pure by returning a new
UIMessage[] with updated metadata (update call sites accordingly) if you prefer
the defensive non-mutating approach.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d61ef17-a7f1-45c5-af37-e0c50ffcb145

📥 Commits

Reviewing files that changed from the base of the PR and between 8a8ebfc and 39bfbdb.

📒 Files selected for processing (2)
  • packages/ai-core/src/chatTransport.ts
  • packages/ai-core/src/hooks/useToolGrouping.tsx

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

🧹 Nitpick comments (4)
packages/ai-core/src/components/ReasoningBox.tsx (2)

69-94: Agent name formatting is duplicated.

The name formatting logic (lines 80-83) is identical to ReasoningBox.tsx lines 80-83. Consider extracting to a shared utility:

♻️ Extract shared formatter
// In a shared utils file
export function formatAgentName(toolName: string): string {
  return toolName
    .replace(/-/g, ' ')
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/\b\w/g, (c) => c.toUpperCase());
}

This is the same pattern as formatToolNameFallback in useToolGrouping.tsx (lines 220-225), so you could potentially reuse that function.

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

In `@packages/ai-core/src/components/ReasoningBox.tsx` around lines 69 - 94,
Extract the duplicated agent name formatting from latestSubReasoning into a
shared utility (e.g., formatAgentName) and replace the inline logic inside
latestSubReasoning with a call to that utility; update any other places using
the same pattern (refer to formatToolNameFallback in useToolGrouping.tsx) to
reuse the new function, ensuring latestSubReasoning still calls
truncateToFit(reasoning, containerWidth, prefix.length) with prefix =
`${formatAgentName(tc.toolName)}: ` and that agentProgress and containerWidth
remain in the useMemo dependency array.

39-43: Selecting entire toolTimings object may cause excess re-renders.

Line 39 selects the entire s.ai.toolTimings record. Any timing update to any tool call will trigger a re-render of this component, even if none of the toolCallIds for this box changed. Consider selecting only the relevant entries:

♻️ Scoped timing selection
-  const toolTimings = useStoreWithAi((s) => s.ai.toolTimings);
+  const relevantTimings = useStoreWithAi((s) => {
+    if (!toolCallIds?.length) return undefined;
+    const result: Record<string, { startedAt: number; completedAt?: number }> = {};
+    for (const id of toolCallIds) {
+      const t = s.ai.toolTimings[id];
+      if (t) result[id] = t;
+    }
+    return result;
+  });

Then update the useMemo to use relevantTimings instead of toolTimings. Note: The selector now derives an object, so you should use a shallow-equality check or move the derivation to useMemo with raw IDs as dependency. A cleaner approach per coding guidelines:

+  // Select raw timing entries individually to avoid selector-derived objects
+  const timingEntries = useMemo(() => {
+    if (!toolCallIds?.length) return [];
+    return toolCallIds.map((id) => toolTimings[id]).filter(Boolean);
+  }, [toolCallIds, toolTimings]);

This keeps the selector simple while the derivation happens in React.

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

In `@packages/ai-core/src/components/ReasoningBox.tsx` around lines 39 - 43, The
component currently selects the entire s.ai.toolTimings via useStoreWithAi which
causes unnecessary re-renders; change the selector to only read the raw tool
call IDs needed (e.g., select s.ai.toolTimings keys or the array of toolCallIds)
and move the derivation of relevantTimings into a useMemo that depends on those
raw IDs and the full toolTimings accessor, or alternatively adjust the selector
to return only the specific entries for this box; update the useMemo that
currently uses toolTimings to use relevantTimings (or the memoized subset) and
ensure any selector returning derived objects uses shallow equality to avoid
re-renders (refer to useStoreWithAi, toolTimings, agentToolCallId, and the
existing useMemo that computes relevantTimings).
packages/ai-core/src/components/ToolPartRenderer.tsx (2)

128-138: Consider extracting the reasoning type guard.

The inline type checking for toolCall.input.reasoning is repeated here and in ToolPartRenderer (lines 301-304) and ReasoningBox.tsx. A shared helper like getToolCallReasoning(input: unknown): string | undefined would reduce duplication.

♻️ Optional helper extraction
// In a shared utils file
export function getToolCallReasoning(input: unknown): string | undefined {
  if (
    input instanceof Object &&
    'reasoning' in input &&
    typeof (input as Record<string, unknown>).reasoning === 'string'
  ) {
    return (input as Record<string, unknown>).reasoning as string;
  }
  return undefined;
}

Then usage becomes:

-              {toolCall.input instanceof Object &&
-              'reasoning' in toolCall.input &&
-              typeof (toolCall.input as Record<string, unknown>).reasoning ===
-                'string' ? (
-                <div className="mb-1 text-sm text-gray-500">
-                  {
-                    (toolCall.input as Record<string, unknown>)
-                      .reasoning as string
-                  }
-                </div>
-              ) : null}
+              {getToolCallReasoning(toolCall.input) && (
+                <div className="mb-1 text-sm text-gray-500">
+                  {getToolCallReasoning(toolCall.input)}
+                </div>
+              )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/src/components/ToolPartRenderer.tsx` around lines 128 - 138,
Extract the repeated inline type-guard into a shared helper named
getToolCallReasoning(input: unknown): string | undefined and replace the
duplicated checks in ToolPartRenderer (the JSX block rendering
toolCall.input.reasoning) and ReasoningBox.tsx with calls to this helper;
specifically, implement getToolCallReasoning to return the reasoning string when
input is an Object with a string "reasoning" property and undefined otherwise,
then import and use getToolCallReasoning(toolCall.input) in the conditional
render and any other locations instead of repeating the
instanceof/'reasoning'/'typeof' checks.

214-217: Passing empty agentToolCalls when liveProgress is used is confusing.

When liveProgress exists, the component passes agentToolCalls={[]} but AgentProgressRenderer ignores it via displayCalls = liveProgress ?? agentToolCalls. This works but obscures the data flow. Consider passing liveProgress directly:

♻️ Clearer prop passing
   if (liveProgress && liveProgress.length > 0) {
     return (
-      <AgentProgressRenderer toolCallId={toolCallId} agentToolCalls={[]} />
+      <AgentProgressRenderer toolCallId={toolCallId} agentToolCalls={liveProgress} />
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/src/components/ToolPartRenderer.tsx` around lines 214 - 217,
ToolPartRenderer currently passes agentToolCalls={[]} when liveProgress exists,
which obscures data flow; change the call in ToolPartRenderer to pass the actual
liveProgress (e.g., AgentProgressRenderer toolCallId={toolCallId}
agentToolCalls={liveProgress}) so AgentProgressRenderer's internal displayCalls
= liveProgress ?? agentToolCalls receives the data directly and avoids the
confusing empty-array override.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/ai-core/src/components/ReasoningBox.tsx`:
- Around line 69-94: Extract the duplicated agent name formatting from
latestSubReasoning into a shared utility (e.g., formatAgentName) and replace the
inline logic inside latestSubReasoning with a call to that utility; update any
other places using the same pattern (refer to formatToolNameFallback in
useToolGrouping.tsx) to reuse the new function, ensuring latestSubReasoning
still calls truncateToFit(reasoning, containerWidth, prefix.length) with prefix
= `${formatAgentName(tc.toolName)}: ` and that agentProgress and containerWidth
remain in the useMemo dependency array.
- Around line 39-43: The component currently selects the entire s.ai.toolTimings
via useStoreWithAi which causes unnecessary re-renders; change the selector to
only read the raw tool call IDs needed (e.g., select s.ai.toolTimings keys or
the array of toolCallIds) and move the derivation of relevantTimings into a
useMemo that depends on those raw IDs and the full toolTimings accessor, or
alternatively adjust the selector to return only the specific entries for this
box; update the useMemo that currently uses toolTimings to use relevantTimings
(or the memoized subset) and ensure any selector returning derived objects uses
shallow equality to avoid re-renders (refer to useStoreWithAi, toolTimings,
agentToolCallId, and the existing useMemo that computes relevantTimings).

In `@packages/ai-core/src/components/ToolPartRenderer.tsx`:
- Around line 128-138: Extract the repeated inline type-guard into a shared
helper named getToolCallReasoning(input: unknown): string | undefined and
replace the duplicated checks in ToolPartRenderer (the JSX block rendering
toolCall.input.reasoning) and ReasoningBox.tsx with calls to this helper;
specifically, implement getToolCallReasoning to return the reasoning string when
input is an Object with a string "reasoning" property and undefined otherwise,
then import and use getToolCallReasoning(toolCall.input) in the conditional
render and any other locations instead of repeating the
instanceof/'reasoning'/'typeof' checks.
- Around line 214-217: ToolPartRenderer currently passes agentToolCalls={[]}
when liveProgress exists, which obscures data flow; change the call in
ToolPartRenderer to pass the actual liveProgress (e.g., AgentProgressRenderer
toolCallId={toolCallId} agentToolCalls={liveProgress}) so
AgentProgressRenderer's internal displayCalls = liveProgress ?? agentToolCalls
receives the data directly and avoids the confusing empty-array override.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7272c98b-91da-44b2-80b6-ea412b9e7871

📥 Commits

Reviewing files that changed from the base of the PR and between 39bfbdb and 485da0c.

📒 Files selected for processing (6)
  • packages/ai-core/src/agents/AgentUtils.ts
  • packages/ai-core/src/components/AnalysisResult.tsx
  • packages/ai-core/src/components/GroupedMessageParts.tsx
  • packages/ai-core/src/components/ReasoningBox.tsx
  • packages/ai-core/src/components/ToolPartRenderer.tsx
  • packages/ai-core/src/hooks/useToolGrouping.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/ai-core/src/components/GroupedMessageParts.tsx
  • packages/ai-core/src/components/AnalysisResult.tsx
  • packages/ai-core/src/agents/AgentUtils.ts

@lixun910 lixun910 closed this Apr 7, 2026
@lixun910

lixun910 commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator Author

Since we are going to migrate to AI SDK V6 which has different implementation of agent (with its own loop executiono f sub-tools), we can implement elapsed time in #497 directly.

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.

Enh: Show elapsed time for tool calls in AI chat UI

2 participants