Rework message metadata, timestamps, and tool work log rows#3022
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
🚀 Expo continuous deployment is ready!
|
ApprovabilityVerdict: Needs human review Substantial UI refactor with unresolved medium-severity review comments identifying bugs where tool status indicators may incorrectly show completed state for in-flight or interrupted tools. These correctness issues warrant human review. You can customize Macroscope's approvability policy. Learn more. |
0d5617b to
245fb15
Compare
Dismissing prior approval to re-evaluate 245fb15
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Neutral work rows never render
- WorkGroupSection now reads TimelineRowActivityCtx and only filters out neutral entries while the turn is in progress; once the turn settles, neutral entries are kept and rendered by SimpleWorkEntryRow with the success indicator.
- ✅ Fixed: Stopped tools treated as hidden
- Added 'stopped' to the failure lifecycle statuses in workEntryIndicatesToolFailure so stopped tools are no longer classified as neutral and are always visible in the timeline.
Or push these changes by commenting:
@cursor push 32d98dbf3d
Preview (32d98dbf3d)
diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -591,10 +591,13 @@
groupedEntries: Extract<MessagesTimelineRow, { kind: "work" }>["groupedEntries"];
}) {
const { workspaceRoot } = use(TimelineRowCtx);
+ const activity = use(TimelineRowActivityCtx);
const [isExpanded, setIsExpanded] = useState(false);
+ const turnSettled = !activity.activeTurnInProgress;
const nonEmptyEntries = useMemo(
- () => groupedEntries.filter((entry) => !workEntryIndicatesToolNeutralStatus(entry)),
- [groupedEntries],
+ () =>
+ groupedEntries.filter((entry) => turnSettled || !workEntryIndicatesToolNeutralStatus(entry)),
+ [groupedEntries, turnSettled],
);
const hasOverflow = nonEmptyEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES;
const visibleEntries =
diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts
--- a/apps/web/src/session-logic.ts
+++ b/apps/web/src/session-logic.ts
@@ -202,7 +202,7 @@
return true;
}
const ls = entry.toolLifecycleStatus;
- if (ls === "failed" || ls === "declined") {
+ if (ls === "failed" || ls === "declined" || ls === "stopped") {
return true;
}
if (!workLogEntryIsToolLike(entry)) {
@@ -240,9 +240,6 @@
if (ls === "inProgress") {
return false;
}
- if (ls === "stopped") {
- return false;
- }
return true;
}You can send follow-ups to the cloud agent here.
245fb15 to
ee8a5c5
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: User messages lost markdown rendering
- Restored ChatMarkdown rendering with markdownCwd in all UserMessageBody code paths (default, review comment text, terminal context inline segments, and review comment card) that were accidentally replaced with SkillInlineText during the layout refactor.
Or push these changes by commenting:
@cursor push d14ba348c6
Preview (d14ba348c6)
diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -6,6 +6,7 @@
} from "@t3tools/contracts";
import {
createContext,
+ Fragment,
memo,
use,
useCallback,
@@ -83,7 +84,6 @@
formatInlineTerminalContextLabel,
textContainsInlineTerminalContextLabels,
} from "./userMessageTerminalContexts";
-import { SkillInlineText } from "./SkillInlineText";
import { formatWorkspaceRelativePath } from "../../filePathDisplay";
import {
buildReviewCommentRenderablePatch,
@@ -394,6 +394,7 @@
text={displayedUserMessage.visibleText}
terminalContexts={terminalContexts}
skills={ctx.skills}
+ markdownCwd={ctx.markdownCwd}
/>
</div>
<div className="flex w-full max-w-[80%] items-center justify-end pe-1 text-xs tabular-nums opacity-0 transition-opacity duration-200 focus-within:opacity-100 group-hover:opacity-100">
@@ -784,6 +785,7 @@
text: string;
terminalContexts: ParsedTerminalContextEntry[];
skills: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;
+ markdownCwd: string | undefined;
footer?: ReactNode;
}) {
const [expanded, setExpanded] = useState(false);
@@ -813,6 +815,7 @@
text={props.text}
terminalContexts={props.terminalContexts}
skills={props.skills}
+ markdownCwd={props.markdownCwd}
/>
</div>
) : null}
@@ -850,7 +853,34 @@
text: string;
terminalContexts: ParsedTerminalContextEntry[];
skills: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;
+ markdownCwd: string | undefined;
}) {
+ const renderInlineMarkdownSegment = (text: string, key: string) => {
+ const leadingWhitespace = /^\s+/.exec(text)?.[0] ?? "";
+ const textWithoutLeadingWhitespace = text.slice(leadingWhitespace.length);
+ const trailingWhitespace = /\s+$/.exec(textWithoutLeadingWhitespace)?.[0] ?? "";
+ const content = textWithoutLeadingWhitespace.slice(
+ 0,
+ textWithoutLeadingWhitespace.length - trailingWhitespace.length,
+ );
+
+ return (
+ <Fragment key={key}>
+ {leadingWhitespace ? <span aria-hidden="true">{leadingWhitespace}</span> : null}
+ {content ? (
+ <ChatMarkdown
+ text={content}
+ cwd={props.markdownCwd}
+ skills={props.skills}
+ className="text-foreground"
+ lineBreaks
+ />
+ ) : null}
+ {trailingWhitespace ? <span aria-hidden="true">{trailingWhitespace}</span> : null}
+ </Fragment>
+ );
+ };
+
const reviewCommentSegments = parseReviewCommentMessageSegments(props.text);
if (reviewCommentSegments.some((segment) => segment.kind === "review-comment")) {
return (
@@ -858,8 +888,14 @@
{reviewCommentSegments.map((segment) =>
segment.kind === "text" ? (
segment.text.trim().length > 0 ? (
- <div key={segment.id} className="whitespace-pre-wrap wrap-break-word">
- <SkillInlineText text={segment.text.trim()} skills={props.skills} />
+ <div key={segment.id} className="wrap-break-word">
+ <ChatMarkdown
+ text={segment.text.trim()}
+ cwd={props.markdownCwd}
+ skills={props.skills}
+ className="text-foreground"
+ lineBreaks
+ />
</div>
) : null
) : (
@@ -890,9 +926,10 @@
}
if (matchIndex > cursor) {
inlineNodes.push(
- <span key={`user-terminal-context-inline-before:${context.header}:${cursor}`}>
- <SkillInlineText text={props.text.slice(cursor, matchIndex)} skills={props.skills} />
- </span>,
+ renderInlineMarkdownSegment(
+ props.text.slice(cursor, matchIndex),
+ `user-terminal-context-inline-before:${context.header}:${cursor}`,
+ ),
);
}
inlineNodes.push(
@@ -907,9 +944,10 @@
if (inlineNodes.length > 0) {
if (cursor < props.text.length) {
inlineNodes.push(
- <span key={`user-message-terminal-context-inline-rest:${cursor}`}>
- <SkillInlineText text={props.text.slice(cursor)} skills={props.skills} />
- </span>,
+ renderInlineMarkdownSegment(
+ props.text.slice(cursor),
+ `user-message-terminal-context-inline-rest:${cursor}`,
+ ),
);
}
@@ -937,9 +975,14 @@
if (props.text.length > 0) {
inlineNodes.push(
- <span key="user-message-terminal-context-inline-text">
- <SkillInlineText text={props.text} skills={props.skills} />
- </span>,
+ <ChatMarkdown
+ key="user-message-terminal-context-inline-text"
+ text={props.text}
+ cwd={props.markdownCwd}
+ skills={props.skills}
+ className="text-foreground"
+ lineBreaks
+ />,
);
} else if (inlinePrefix.length === 0) {
return null;
@@ -957,9 +1000,13 @@
}
return (
- <div className="whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-foreground">
- <SkillInlineText text={props.text} skills={props.skills} />
- </div>
+ <ChatMarkdown
+ text={props.text}
+ cwd={props.markdownCwd}
+ skills={props.skills}
+ className="text-foreground"
+ lineBreaks
+ />
);
});
@@ -981,8 +1028,14 @@
</div>
</div>
{comment.text.length > 0 && (
- <div className="whitespace-pre-wrap wrap-break-word text-sm">
- <SkillInlineText text={comment.text} skills={ctx.skills} />
+ <div className="wrap-break-word text-sm">
+ <ChatMarkdown
+ text={comment.text}
+ cwd={ctx.markdownCwd}
+ skills={ctx.skills}
+ className="text-foreground"
+ lineBreaks
+ />
</div>
)}
{renderablePatch?.kind === "files" &&You can send follow-ups to the cloud agent here.
Ported from #2451. Co-authored-by: ss <ss@barekey.dev> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: codex <codex@users.noreply.github.com>
User prompts were rendering markdown literally after the switch to SkillInlineText. Restore the ChatMarkdown-based rendering (with markdownCwd threading) from main and re-add the markdown/file-link browser tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ee8a5c5 to
4b1a774
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Changed files shown twice
- Restored the
previewIsChangedFilesguard (true when no command/detail exists) so the chip list is suppressed when the preview line already displays the changed file paths.
- Restored the
Or push these changes by commenting:
@cursor push 8eda89eafa
Preview (8eda89eafa)
diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -1246,6 +1246,7 @@
const expandedBody = buildToolCallExpandedBody(workEntry);
const canExpand = expandedBody !== null;
const hasChangedFiles = (workEntry.changedFiles?.length ?? 0) > 0;
+ const previewIsChangedFiles = hasChangedFiles && !workEntry.command && !workEntry.detail;
const showFailedIndicator = workEntryIndicatesToolFailure(workEntry);
const showDestructiveRowStyle =
showFailedIndicator &&
@@ -1430,7 +1431,7 @@
</pre>
</div>
) : null}
- {hasChangedFiles && (
+ {hasChangedFiles && !previewIsChangedFiles && (
<div
className="mt-1 flex flex-wrap gap-1"
onClick={stopRowToggle}You can send follow-ups to the cloud agent here.
| className="mt-1 flex flex-wrap gap-1" | ||
| onClick={stopRowToggle} | ||
| onPointerDown={stopRowToggle} | ||
| > |
There was a problem hiding this comment.
Changed files shown twice
Low Severity
The work log row again renders the changed-files chip list whenever hasChangedFiles is true, but the guard that skipped chips when the preview already came from changedFiles was removed. File-only tool rows now show the same path in the summary line and again as chips below.
Reviewed by Cursor Bugbot for commit 4b1a774. Configure here.
Codex-style turn folding in the chat timeline, derived entirely from
existing client-side data (no backend changes):
- Settled turns collapse their commentary messages and tool work log
behind a 'Worked for Xs' row anchored where the turn's work begins;
the terminal assistant message stays visible below the fold. Clicking
the row toggles the hidden history.
- The active turn is never folded — live commentary and collapsed tool
groups render exactly as before, with the working timer still at the
bottom.
- An interrupted latest turn folds with 'You stopped after Xs' (state
comes from latestTurn, the only turn whose interruption survives in
client data). Pressing stop leaves that turn expanded so the user
keeps their place; the next turn or a reload folds it.
- The completion divider ('Worked for ...' between hairlines) is
replaced by the fold row; deriveCompletionDividerBeforeEntryId,
completionSummary plumbing, and hasToolActivityForTurn are removed.
- Work log entries now carry turnId (from the underlying activity) so
tool activity can be attributed to a turn's fold.
- Commentary (non-terminal assistant) rows lose the reserved metadata
spacing.
Folding concept ported from PR #2962 (stale, full-stack, Codex-specific
assistantPhase plumbing) reimplemented web-only per discussion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Missing lifecycle shows success early
- Added a guard in
workEntryIndicatesToolSuccessso that whentoolLifecycleStatusis undefined, the function returns false instead of falling through to return true.
- Added a guard in
- ✅ Fixed: Settled neutrals show completed check
- Removed the
(turnSettled && workEntryIndicatesToolNeutralStatus(workEntry))clause fromshowSuccessIndicatorso neutral entries (including inProgress and stopped) are no longer promoted to the success checkmark when the turn settles.
- Removed the
Or push these changes by commenting:
@cursor push 8e4f101ceb
Preview (8e4f101ceb)
diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -1322,9 +1322,7 @@
: "font-medium text-foreground";
const turnSettled = !activity.activeTurnInProgress;
const showNeutralIndicator = !turnSettled && workEntryIndicatesToolNeutralStatus(workEntry);
- const showSuccessIndicator =
- workEntryIndicatesToolSuccess(workEntry) ||
- (turnSettled && workEntryIndicatesToolNeutralStatus(workEntry));
+ const showSuccessIndicator = workEntryIndicatesToolSuccess(workEntry);
const rowToggleProps = canExpand
? {
role: "button" as const,
diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts
--- a/apps/web/src/session-logic.test.ts
+++ b/apps/web/src/session-logic.test.ts
@@ -1466,7 +1466,6 @@
},
});
});
-
});
describe("deriveWorkLogEntries context window handling", () => {
diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts
--- a/apps/web/src/session-logic.ts
+++ b/apps/web/src/session-logic.ts
@@ -235,6 +235,9 @@
return false;
}
const ls = entry.toolLifecycleStatus;
+ if (!ls) {
+ return false;
+ }
if (ls === "failed" || ls === "declined") {
return false;
}You can send follow-ups to the cloud agent here.
| if (ls === "stopped") { | ||
| return false; | ||
| } | ||
| return true; |
There was a problem hiding this comment.
Missing lifecycle shows success early
Medium Severity
workEntryIndicatesToolSuccess treats tool-like rows with no toolLifecycleStatus as successful, and the work-log row renders the muted “Completed” check whenever that helper is true. In-flight tool.updated rows often omit status, so users can see a completed indicator while the turn is still running and the tool has not finished.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c42975f. Configure here.
| const showNeutralIndicator = !turnSettled && workEntryIndicatesToolNeutralStatus(workEntry); | ||
| const showSuccessIndicator = | ||
| workEntryIndicatesToolSuccess(workEntry) || | ||
| (turnSettled && workEntryIndicatesToolNeutralStatus(workEntry)); |
There was a problem hiding this comment.
Settled neutrals show completed check
Medium Severity
When activeTurnInProgress is false, showSuccessIndicator also turns on for any row that workEntryIndicatesToolNeutralStatus still classifies as neutral. That includes inProgress and stopped lifecycle states, so interrupted or unfinished tools can show the “Completed” check after the turn ends even though they never succeeded.
Reviewed by Cursor Bugbot for commit c42975f. Configure here.
Expanding a fold inserts rows between the trigger and the final message. With maintainScrollAtEnd active and the user scrolled to the end, LegendList re-pinned the bottom content on the data change, yanking the trigger out of view; an imperative scroll correction can only land after the list 'settles' (runScrollWithPromise defers scrolls issued during a data change), which painted one frame late and popped. Since everything above the trigger is unchanged by the toggle, the trigger stays put on its own as long as the list doesn't re-anchor to the end. Suppress maintainScrollAtEnd for the toggle's settling frames instead of fighting it with a deferred counter-scroll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Assistant messages no longer show the timestamp/copy metadata row
while their turn is still in progress — the latest message is only
provisionally terminal during work, and the row caused stray padding
and a stale '0s ago' under streaming commentary.
- Replace relative chat timestamps ('12s ago') with absolute short
times (formatShortTimestamp, honoring the timestamp format setting).
The relative labels never re-rendered, so they showed stale values.
The long-form tooltip stays. formatChatTimestamp is removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sending a message flips isWorking before the server creates the new turn, so activeTurnId still pointed at the previous, settled turn. The fold-skip condition keyed on activeTurnId treated that turn as in progress, transiently unfolding it — dozens of rows inserted and then removed again once the new turn id arrived, flinging the scroll position into the middle of the old turn's history. Key turn-progress checks on the turn's own lifecycle instead: the only unsettled turn is the latest turn without a recorded completion (or still in state 'running'). Folding, assistant metadata, and copy streaming all derive from that, and the racy activeTurnInProgress/ activeTurnId inputs are dropped from the row derivation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deriveWorkLogEntries previously filtered activities to the latest turn, so expanding a historic turn's fold showed only its commentary messages. Now that settled turns fold their work behind the Worked-for row, keep work log entries for every turn; each settled turn's tool calls collapse into its fold and appear on expand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g#3022) Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- merged upstream message metadata/timestamps/tool work log rework (pingdotgg#3022) - re-applied fork changes on top across orchestration, provider adapters, and web components



Ports the messages-timeline part of #2451 by @sandersonstabo, rebased onto current main.
Message metadata
Tool work log
MAX_VISIBLE_WORK_LOG_ENTRIES6 → 1) — per Theo's "tool calls should be uninteresting" feedback on Polish chat UI and provider prompts #2451Not ported (per discussion in #2451): the LLM-generated tool-call summaries and the backend turn-lifecycle changes.
Part of splitting #2451 into reviewable frontend-only pieces.
🤖 Generated with Claude Code
Note
Medium Risk
Large UI-only change to core chat rendering and timeline row derivation; behavior shifts (all-turn work logs, fold state, metadata timing) are covered by tests but regressions in scroll/fold edge cases are possible.
Overview
Settled turns in the chat timeline are collapsed behind a clickable Worked for … row (or You stopped after … when interrupted); expanding reveals mid-turn assistant commentary and tool/work entries while the final assistant reply stays visible.
MessagesTimelinenow takeslatestTurnand localexpandedTurnIdsinstead of completion-divider props; fold toggles temporarily disablemaintainScrollAtEndso the list does not jump.Message chrome drops the completion pill and live elapsed timer. Only the terminal assistant message per turn shows hover metadata (ghost copy + short timestamp with a long tooltip via
formatChatTimestampTooltip). User bubbles move timestamp/copy/revert to a row below with ghost buttons.Work log is reworked:
deriveWorkLogEntriesreturns all turns withturnId,toolLifecycleStatus, andsourceActivityKind; new helpers classify rows as success/failure/neutral (lifecycle + output heuristics). The UI shows a compact card (default one visible tool row), hides neutral rows, adds expand-for-detail, and status icons.ChatViewno longer computescompletionSummary/hasToolActivityForTurn.Reviewed by Cursor Bugbot for commit 52b9cf1. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Rework chat timeline to fold settled turns and add tool call success/failure indicators
deriveWorkLogEntriesin session-logic.ts now returns entries across all turns, each tagged withturnId,sourceActivityKind, andtoolLifecycleStatus; thelatestTurnIdfilter and completion divider logic are removed.workEntryIndicatesToolFailure,workEntryIndicatesToolSuccess, andworkEntryIndicatesToolNeutralStatusclassify each work row;SimpleWorkEntryRowuses these to show ✓/✗/− status icons and supports expanding to show full command/detail text.WorkGroupSectionhides neutral-status tool entries by default and shows a count header with expand/collapse;MAX_VISIBLE_WORK_LOG_ENTRIESdrops from 6 to 1.MessagesTimelinemust replaceactiveTurnId,completionDividerBeforeEntryId, andcompletionSummaryprops withlatestTurn.Macroscope summarized 52b9cf1.