Skip to content

Move sandbox run details to compact end-of-message card#1610

Open
ivaavimusic wants to merge 3 commits intorecoupable:mainfrom
ivaavimusic:chat-sandbox-run-card
Open

Move sandbox run details to compact end-of-message card#1610
ivaavimusic wants to merge 3 commits intorecoupable:mainfrom
ivaavimusic:chat-sandbox-run-card

Conversation

@ivaavimusic
Copy link
Copy Markdown

@ivaavimusic ivaavimusic commented Mar 30, 2026

Summary

This PR improves how sandbox task results appear inside chat responses.

Changes

  • moves sandbox result cards to the end of the assistant response
  • introduces a compact horizontal summary row for sandbox runs in chat
  • makes the row expandable and collapsible for full details
  • preserves the full standalone run layout on the dedicated task run page

Why

Inline sandbox details were easy to miss and visually heavy. This makes the run status easier to scan while still keeping full logs and error details available on demand.

Risk

Low to medium. The change is scoped to chat rendering for sandbox task results and does not alter backend task execution.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced collapsible run details card with status, duration, and summary information in chat context.
    • Added improved loading skeleton UI for task run details.
  • Refactor

    • Optimized message part ordering for better content presentation in chat.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 30, 2026

@ivaavimusic is attempting to deploy a commit to the Recoupable Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

This PR introduces compact UI components and utilities for displaying task run details in a chat context. It adds a new CompactRunDetails component with a "chat-compact" variant to RunDetails, creates utility functions for message part ordering and deferred sandbox detection, and updates the sandbox command polling component to use the compact rendering variant.

Changes

Cohort / File(s) Summary
Compact Run UI Components
components/TasksPage/Run/CompactRunDetails.tsx, components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx
New components for displaying collapsible run details card and skeleton loading state in compact form for chat integration.
RunDetails Variant & State Logic
components/TasksPage/Run/RunDetails.tsx, components/TasksPage/Run/runDetailsConstants.ts
Added variant prop to RunDetails enabling "chat-compact" rendering path; introduced state management for terminal status detection, computed display properties (duration, badge styling, summary text), and extracted shared detailsContent.
Message Part Ordering Utilities
components/VercelChat/getOrderedMessageParts.ts, components/VercelChat/isDeferredSandboxResultPart.ts
New utility functions to partition and reorder message parts, detecting deferred sandbox results by tool state and name matching.
Updated Message Part Rendering
components/VercelChat/MessageParts.tsx
Refactored to use ordered message parts; changed streaming indicator and retry action visibility conditions from index-based to part-reference-based checks.
Sandbox Polling UI Update
components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx
Replaced loading skeleton with CompactRunSkeleton and added variant="chat-compact" prop to RunDetails for compact chat integration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • sweetmantech

Poem

💬 A chat-compact run unfolds,
Collapsible cards with stories to be told,
Deferred results now flow just right,
Sandboxes shine in compact light! ✨

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Pull request contains three critical violations: unstable React keys causing state loss on reorder, missing security attributes enabling tabnabbing, and SRP violation in RunDetails component. Fix React keys using stable indices from getOrderedMessageParts, add rel="noopener noreferrer" to CompactRunDetails link, and extract variant logic into separate wrapper components.

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

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bad20d018e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 86 to 88
isStreaming={
status === "streaming" &&
partIndex === message.parts.length - 1
status === "streaming" && partIndex === orderedParts.length - 1
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve reasoning streaming flag after reordering parts

When deferred sandbox tool results are moved to the end, partIndex === orderedParts.length - 1 no longer identifies the actively streaming reasoning chunk in messages that continue with reasoning/text after a sandbox result. In that case EnhancedReasoning receives isStreaming=false while the assistant is still streaming, so shimmer/auto-collapse behavior can stop early and show reasoning as finalized before generation actually completes.

Useful? React with 👍 / 👎.

Comment on lines +27 to +61
const TERMINAL_STATUSES = new Set([
"COMPLETED",
"FAILED",
"CRASHED",
"CANCELED",
"SYSTEM_FAILURE",
"INTERRUPTED",
]);

const STATUS_BADGE_CLASSES: Record<string, string> = {
COMPLETED:
"border-green-200 bg-green-50 text-green-700 dark:border-green-900/60 dark:bg-green-950/50 dark:text-green-300",
FAILED:
"border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300",
CRASHED:
"border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300",
SYSTEM_FAILURE:
"border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300",
INTERRUPTED:
"border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300",
CANCELED:
"border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300",
EXECUTING:
"border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/60 dark:bg-yellow-950/50 dark:text-yellow-300",
REATTEMPTING:
"border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/60 dark:bg-yellow-950/50 dark:text-yellow-300",
QUEUED:
"border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300",
DELAYED:
"border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300",
FROZEN:
"border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300",
PENDING_VERSION:
"border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300",
};
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.

Open Closed Principle

  • actual: adding new const definitions to an existing RunDetails component.
  • required: move new const definitions to new files.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I added those constants directly while building the compact run view, but I agree they should be extracted instead of expanding RunDetails further. I’ll move them into dedicated files.

);

if (variant === "chat-compact") {
return (
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.

Open Closed Principle

  • actual: new components defined inline of an existing component file.
  • required: new component file for this new code.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I agree those new pieces should be extracted into separate component files. I’ll split them out.

import RunDetails from "@/components/TasksPage/Run/RunDetails";
import RunPageSkeleton from "@/components/TasksPage/Run/RunPageSkeleton";

function CompactRunSkeleton() {
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.

Open Closed Principle

  • actual: CompactRunSkeleton defined within RunSandboxCommandResultWithPolling
  • required: new component file for CompactRunSkeleton

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed. I’ll extract it into its own file.

"prompt_sandbox",
]);

function isDeferredSandboxResultPart(
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.

Single Responsibility Principle

  • actual: function isDeferredSandboxResultPart is defined within a component file.
  • required: new standalone function file for isDeferredSandboxResultPart.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Understood. That helper was added inline for the sandbox-result reordering, but I agree it should be extracted so MessageParts stays focused on rendering. I’ll move it into a standalone function file.

Copy link
Copy Markdown
Contributor

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
components/TasksPage/Run/CompactRunDetails.tsx (2)

5-5: Verify icon-library consistency for this component tier.

If this component is considered part of the UI-component layer, replace lucide-react here with @radix-ui/react-icons to match the repo convention.

As per coding guidelines, "Use @radix-ui/react-icons for UI component icons (not Lucide React)".

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

In `@components/TasksPage/Run/CompactRunDetails.tsx` at line 5, This file imports
ChevronDown from lucide-react which violates the repo guideline to use
`@radix-ui/react-icons` for UI-component layer icons; replace the import of
ChevronDown with the equivalent icon from `@radix-ui/react-icons` (or a suitable
Radix icon name) in CompactRunDetails (and update any JSX references to that
symbol if renamed), run a quick search in the component for ChevronDown usage to
ensure the symbol name matches the new import, and update imports-only (no other
logic changes) so the component consumes the Radix icon library consistently.

12-24: Export CompactRunDetailsProps for consistency.

The type name is good; exporting it would align with the project’s TS component API convention.

As per coding guidelines, "Export component prop types with explicit ComponentNameProps naming convention".

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

In `@components/TasksPage/Run/CompactRunDetails.tsx` around lines 12 - 24, The
CompactRunDetailsProps interface is currently unexported; update it to a named
export so it follows the project's ComponentNameProps convention and can be
reused externally—change the declaration of CompactRunDetailsProps to an
exported interface (export interface CompactRunDetailsProps { ... }) in the
CompactRunDetails.tsx file and keep the existing shape and name exactly the
same.
components/TasksPage/Run/RunDetails.tsx (2)

18-22: Export the props type for API clarity.

RunDetailsProps already uses the right naming; exporting it would align with the shared TS component contract pattern.

As per coding guidelines, "Export component prop types with explicit ComponentNameProps naming convention".

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

In `@components/TasksPage/Run/RunDetails.tsx` around lines 18 - 22, Export the
RunDetailsProps interface so the prop type is available to other modules and
matches the ComponentNameProps convention; update the declaration for
RunDetailsProps to be exported (export interface RunDetailsProps { ... }) and
ensure any usages/imports of the RunDetails component or its props (e.g.,
RunDetails) reference the exported RunDetailsProps type where needed.

36-50: Scope disclosure state to the compact variant path.

isOpen and the useEffect run even for variant="full" where they are unused. Consider moving this state into CompactRunDetails (or initializing only when variant === "chat-compact") to keep Line 36–50 variant-specific and reduce unnecessary state work.

As per coding guidelines, "Create single-responsibility components with obvious data flow" and "Write minimal code - use only the absolute minimum code needed".

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

In `@components/TasksPage/Run/RunDetails.tsx` around lines 36 - 50, The open/close
state and effect are being created in RunDetails even when variant !==
"chat-compact"; move the isOpen useState and its useEffect into the
compact-specific component (e.g., CompactRunDetails) or conditionally initialize
them only when variant === "chat-compact" to avoid unnecessary state;
specifically relocate/remove isOpen, setIsOpen, and the useEffect from
RunDetails and implement the same logic inside CompactRunDetails (or wrap
useState/useEffect in a guard checking variant) so the terminal-checking logic
(isTerminal = TERMINAL_STATUSES.has(data.status)) and summaryText remain in
RunDetails while compact-only UI state lives in the compact variant.
components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx (1)

3-12: Hide decorative skeleton blocks from screen readers.

This skeleton is visual-only; consider marking the wrapper aria-hidden="true" so assistive tech doesn’t read placeholder structure (Line 3).

♻️ Suggested tweak
-    <div className="w-full rounded-2xl border bg-background/80 px-4 py-3 shadow-sm">
+    <div
+      aria-hidden="true"
+      className="w-full rounded-2xl border bg-background/80 px-4 py-3 shadow-sm"
+    >

As per coding guidelines, "Provide proper ARIA roles/states and test with screen readers" and "Start with semantic HTML first, then augment with ARIA if needed".

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

In `@components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx` around lines 3 -
12, The outer wrapper div in CompactRunSkeleton.tsx is purely decorative and
should be hidden from assistive tech; update the top-level div (the one with
className "w-full rounded-2xl border bg-background/80 px-4 py-3 shadow-sm") to
include aria-hidden="true" so screen readers ignore the placeholder skeleton,
and ensure none of the inner elements are focusable or contain interactive
attributes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/TasksPage/Run/CompactRunDetails.tsx`:
- Around line 91-95: The external link rendering in the Link component (the JSX
element with href={`/tasks/${runId}`} and target="_blank`} in CompactRunDetails)
is missing the rel attribute; update that Link to include rel="noopener
noreferrer" to prevent tabnabbing and ensure safe external/new-tab behavior.

In `@components/VercelChat/MessageParts.tsx`:
- Around line 30-37: The render keys are unstable because getOrderedMessageParts
reorders originalParts but keys are derived from the rendered partIndex; change
getOrderedMessageParts to attach the original index (e.g., add originalIndex or
sourceIndex property to each returned part) and update all consumers (the render
loop and lastTextPartIndex computation) to use that preserved originalIndex as
the React key and for identity checks instead of the current partIndex; this
ensures components (e.g., expandable run card) keep stable identity when parts
are moved.

---

Nitpick comments:
In `@components/TasksPage/Run/CompactRunDetails.tsx`:
- Line 5: This file imports ChevronDown from lucide-react which violates the
repo guideline to use `@radix-ui/react-icons` for UI-component layer icons;
replace the import of ChevronDown with the equivalent icon from
`@radix-ui/react-icons` (or a suitable Radix icon name) in CompactRunDetails (and
update any JSX references to that symbol if renamed), run a quick search in the
component for ChevronDown usage to ensure the symbol name matches the new
import, and update imports-only (no other logic changes) so the component
consumes the Radix icon library consistently.
- Around line 12-24: The CompactRunDetailsProps interface is currently
unexported; update it to a named export so it follows the project's
ComponentNameProps convention and can be reused externally—change the
declaration of CompactRunDetailsProps to an exported interface (export interface
CompactRunDetailsProps { ... }) in the CompactRunDetails.tsx file and keep the
existing shape and name exactly the same.

In `@components/TasksPage/Run/RunDetails.tsx`:
- Around line 18-22: Export the RunDetailsProps interface so the prop type is
available to other modules and matches the ComponentNameProps convention; update
the declaration for RunDetailsProps to be exported (export interface
RunDetailsProps { ... }) and ensure any usages/imports of the RunDetails
component or its props (e.g., RunDetails) reference the exported RunDetailsProps
type where needed.
- Around line 36-50: The open/close state and effect are being created in
RunDetails even when variant !== "chat-compact"; move the isOpen useState and
its useEffect into the compact-specific component (e.g., CompactRunDetails) or
conditionally initialize them only when variant === "chat-compact" to avoid
unnecessary state; specifically relocate/remove isOpen, setIsOpen, and the
useEffect from RunDetails and implement the same logic inside CompactRunDetails
(or wrap useState/useEffect in a guard checking variant) so the
terminal-checking logic (isTerminal = TERMINAL_STATUSES.has(data.status)) and
summaryText remain in RunDetails while compact-only UI state lives in the
compact variant.

In `@components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx`:
- Around line 3-12: The outer wrapper div in CompactRunSkeleton.tsx is purely
decorative and should be hidden from assistive tech; update the top-level div
(the one with className "w-full rounded-2xl border bg-background/80 px-4 py-3
shadow-sm") to include aria-hidden="true" so screen readers ignore the
placeholder skeleton, and ensure none of the inner elements are focusable or
contain interactive attributes.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: de087a20-f73a-4ddf-b35a-a0fda1c01d6a

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6fc82 and d1e2d3a.

📒 Files selected for processing (8)
  • components/TasksPage/Run/CompactRunDetails.tsx
  • components/TasksPage/Run/RunDetails.tsx
  • components/TasksPage/Run/runDetailsConstants.ts
  • components/VercelChat/MessageParts.tsx
  • components/VercelChat/getOrderedMessageParts.ts
  • components/VercelChat/isDeferredSandboxResultPart.ts
  • components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx
  • components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx

Comment on lines +91 to +95
<Link
href={`/tasks/${runId}`}
target="_blank"
className="text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
>
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.

⚠️ Potential issue | 🟡 Minor

Add rel for the new-tab link.

Line 93 opens a new tab but omits rel. Please add rel="noopener noreferrer" to harden against tabnabbing.

🔐 Suggested fix
             <Link
               href={`/tasks/${runId}`}
               target="_blank"
+              rel="noopener noreferrer"
               className="text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/TasksPage/Run/CompactRunDetails.tsx` around lines 91 - 95, The
external link rendering in the Link component (the JSX element with
href={`/tasks/${runId}`} and target="_blank`} in CompactRunDetails) is missing
the rel attribute; update that Link to include rel="noopener noreferrer" to
prevent tabnabbing and ensure safe external/new-tab behavior.

Comment on lines +30 to +37
const originalParts = message.parts ?? [];
const orderedParts = getOrderedMessageParts(originalParts);
const lastOriginalPart = originalParts[originalParts.length - 1];
const lastTextPartIndex = orderedParts.reduce(
(lastIndex, part, partIndex) =>
part.type === "text" ? partIndex : lastIndex,
-1,
);
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that reordered parts are rendered from `orderedParts`
# while the row key still derives from the rendered index.
# Expected: one match for `getOrderedMessageParts(...)` and one match
# showing `message-${message.id}-part-${partIndex}`.
rg -n -C2 'getOrderedMessageParts|message-\$\{message\.id\}-part-\$\{partIndex\}' components/VercelChat/MessageParts.tsx

Repository: recoupable/chat

Length of output: 799


🏁 Script executed:

# Check the implementation of getOrderedMessageParts
find . -name "*getOrderedMessageParts*" -type f

Repository: recoupable/chat

Length of output: 108


🏁 Script executed:

# Also check the complete key derivation logic and map structure
rg -n "orderedParts\.map|getOrderedMessagePart" components/VercelChat/MessageParts.tsx -A 5

Repository: recoupable/chat

Length of output: 822


🏁 Script executed:

cat components/VercelChat/getOrderedMessageParts.ts

Repository: recoupable/chat

Length of output: 653


Use stable keys based on part identity, not rendered position.

The getOrderedMessageParts function reorders parts by moving deferred sandbox results to the end of the array. Since the key is derived from partIndex (the current rendered index), React will remount components when parts shift—resetting local UI state in stateful children like the expandable run card.

Preserve the original index through reordering and use it as the key source instead:

♻️ Suggested approach
+export interface OrderedMessagePart {
+  part: UIMessagePart<UIDataTypes, UITools>;
+  originalIndex: number;
+}
+
 export function getOrderedMessageParts(
   parts: UIMessagePart<UIDataTypes, UITools>[],
-) {
-  const regularParts: UIMessagePart<UIDataTypes, UITools>[] = [];
-  const deferredSandboxParts: UIMessagePart<UIDataTypes, UITools>[] = [];
+): OrderedMessagePart[] {
+  const regularParts: OrderedMessagePart[] = [];
+  const deferredSandboxParts: OrderedMessagePart[] = [];
 
-  for (const part of parts) {
+  for (const [originalIndex, part] of parts.entries()) {
+    const orderedPart = { part, originalIndex };
     if (isDeferredSandboxResultPart(part)) {
-      deferredSandboxParts.push(part);
+      deferredSandboxParts.push(orderedPart);
       continue;
     }
 
-    regularParts.push(part);
+    regularParts.push(orderedPart);
   }
 
   return [...regularParts, ...deferredSandboxParts];
 }

Then update the render loop:

-      {orderedParts.map(
-        (part: UIMessagePart<UIDataTypes, UITools>, partIndex) => {
+      {orderedParts.map(({ part, originalIndex }, partIndex) => {
           const { type } = part;
-          const key = `message-${message.id}-part-${partIndex}`;
+          const key = `message-${message.id}-part-${originalIndex}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/VercelChat/MessageParts.tsx` around lines 30 - 37, The render keys
are unstable because getOrderedMessageParts reorders originalParts but keys are
derived from the rendered partIndex; change getOrderedMessageParts to attach the
original index (e.g., add originalIndex or sourceIndex property to each returned
part) and update all consumers (the render loop and lastTextPartIndex
computation) to use that preserved originalIndex as the React key and for
identity checks instead of the current partIndex; this ensures components (e.g.,
expandable run card) keep stable identity when parts are moved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants