Change Summary

The card Cursor shows after a coding turn: file count, a Review action, a few rows with +/- stats, and “Show N more”. Pair it with WorkedFor for the elapsed-time badge.

Open in
Worked for 1m 12s
7 Files Changed
message-parts.tsx+4-2
message-markdown.css+11
message.tsx+25-7
page.tsx+19-5

Installation

npx shadcn@latest add miskibin/chat-components/change-summary

Also installs collapsible, context-menu, file-icon.

Usage

import {
  ChangeSummary,
  WorkedFor,
} from "@/components/ui/change-summary"

export function AfterTurn() {
  return (
    <>
      <WorkedFor seconds={72} />
      <ChangeSummary
        files={[
          { path: "lib/reducer.ts", additions: 4, deletions: 2 },
          { path: "lib/reducer.test.ts", additions: 11 },
        ]}
        onAction={() => openReview()}
      />
    </>
  )
}

Examples

Derive files from tools

fileChangesFromTools reads Edit / Write / ApplyPatch rows (path + output stats or a unified diff) so you do not have to assemble the list by hand. Message already does this when changes is omitted.

import { fileChangesFromTools } from "@/components/ui/change-summary"

<ChangeSummary files={fileChangesFromTools(tools)} />

Right-click a row

fileActions puts a context menu on every row. The card only shows the menu; what “open” or “reveal” means on this machine is the host's call, which is why each action is handed the path and nothing else.

3 Files ChangedReview

Preview count and styling

previewCount decides how many rows show before the “Show N more” toggle, onAction turns the header label into a button, and onFileClick turns every row into one. Rows, stats, and the header each have a slot, so the card can go monospace or take a tint without a fork; the glyph is a FileIcon, whose brand colours are fixed on purpose, while each row also tags itself with data-kind — code, style, data, text — so opt into colour from the outside when you want it.

Proposed edits
message.tsx+25-7
message-markdown.css+11
4 Files ChangedReview

Notes

API reference

ChangeSummary

files*ChangeSummaryFile[]
Each item is { path, additions?, deletions? }. The filename is derived from the path.
titleReact.ReactNode
Defaults to “N Files Changed”.
actionLabelReact.ReactNode = "Review"
Right-aligned header label.
onAction() => void
Makes the header label a button. Omit it to keep Review as plain text.
onFileClick(file: ChangeSummaryFile) => void
Turns every row into a button — open the file in a FilePreview panel. Rows stay plain text without it.
fileActionsFileActionItem[]
Right-click menu on every row — open in an editor, reveal in the file manager, copy the path. Each action gets the row's path back. Keep the array stable: the rows are memoized.
previewCountnumber = 4
How many rows to show before “Show N more”.

FileActionItem

id / label / iconstring / string / React.ReactNode
One menu entry.
onSelect*(path: string) => void
Called with the path of the file the menu was opened on.
destructive / separatorBeforeboolean
Red text for the entry; a rule above it.

FileContextMenu

path*string
Wraps any trigger in the same menu the rows use — FilePreview and MessageMarkdown share it. With no actions the child renders untouched.

WorkedFor

seconds*number
Elapsed time for the finished turn.

Data slots

Every part below is addressable from a parent, so you can restyle internals without forking the component.

change-summarychange-summary-headerchange-summary-actionchange-summary-listchange-summary-restchange-summary-filechange-summary-file-iconchange-summary-statschange-summary-morefile-context-menuworked-for