Lunia DS

AI output

File changes

A compact row of chips, one per file an agent changed, with lines added and removed, where each chip grows in after the run and opens a short diff preview on hover or focus.

Loading preview

An agent finishes a run and five changed files appear as chips, three shown and a more button; hovering a chip opens its diff preview, and the more button shows the other two. Replay runs the run end again, and a second demo shows a count rising while the agent is still editing.

Installation

Run this in your app after the one-time setup on the Getting started page.

$ npx @ganisigit/lunia-ds add file-changes

Usage

The demo above, as code.

demo.tsx
'use client';

import * as React from 'react';
import { type DiffLine, type FileChange, FileChanges } from './file-changes';

const themeLines: DiffLine[] = [
  { kind: 'context', text: 'export const theme = {' },
  { kind: 'remove', text: "  radius: 'sm'," },
  { kind: 'add', text: "  radius: 'md'," },
  { kind: 'add', text: "  motion: 'calm'," },
  { kind: 'context', text: '};' },
];
const buttonLines: DiffLine[] = [
  { kind: 'context', text: 'export function Button(props) {' },
  { kind: 'remove', text: '  return <button {...props} />;' },
  { kind: 'add', text: '  const label = props.label ?? "Save";' },
  { kind: 'add', text: '  return <button {...props}>{label}</button>;' },
  { kind: 'context', text: '}' },
];
const menuLines: DiffLine[] = [
  { kind: 'add', text: 'export function openMenu(id: string) {' },
  { kind: 'add', text: '  return menus.get(id)?.open();' },
  { kind: 'add', text: '}' },
];
const FILES: FileChange[] = [
  { path: 'src/theme.ts', additions: 14, deletions: 3, lines: themeLines },
  { path: 'src/components/button.tsx', additions: 8, deletions: 2, lines: buttonLines },
  { path: 'src/menu.ts', additions: 22, deletions: 0, lines: menuLines },
  { path: 'tests/menu.test.ts', additions: 31, deletions: 0 },
  { path: 'README.md', additions: 3, deletions: 1 },
];
const LIVE_END = 27;

export default function FileChangesDemo({ replay = 0 }: { replay?: number }) {
  const [live, setLive] = React.useState(LIVE_END);

  // Replay runs the end of a run again, and a count that rises while the agent is still editing.
  React.useEffect(() => {
    if (replay === 0) return;
    setLive(4);
    const timer = window.setInterval(() => {
      setLive((n) => {
        if (n >= LIVE_END) {
          window.clearInterval(timer);
          return LIVE_END;
        }
        return Math.min(LIVE_END, n + 5);
      });
    }, 450);
    return () => window.clearInterval(timer);
  }, [replay]);

  return (
    <div className="flex w-full max-w-[560px] flex-col gap-6 rounded-xl border border-secondary bg-primary p-6">
      <div className="flex flex-col gap-3">
        <p className="text-sm font-medium text-primary">Run finished, 5 files changed</p>
        <FileChanges key={replay} files={FILES} />
      </div>
      <div className="flex flex-col gap-3">
        <p className="text-sm font-medium text-primary">Agent is editing</p>
        <FileChanges files={[{ path: 'src/app.tsx', additions: live, deletions: 2 }]} />
      </div>
    </div>
  );
}

API

Parts: root (list), file chip (button): file name, lines added, lines removed, more button, diff preview (popover): header with path and counts, diff lines with a + or - gutter.

PropTypeDefaultDescription
filesArray<{ path: string; additions: number; deletions: number; lines?: Array<{ text: string; kind: "add" | "remove" | "context" }> }>[]Changed files in the order the agent changed them. lines feeds the preview; without it the chip has no preview.
visiblenumber3Chips shown before the more button.
expandedbooleanundefinedControlled state of the full list.
defaultExpandedbooleanfalseFull list shown at first when not controlled.
previewDelaynumber300Hover time in ms before the preview opens; focus opens it at once.
EventDescription
fileOpenThe person pressed a chip (click, Enter or Space). Payload: the file. Use it to open diff-review or the file.
expandedChangeThe full list opened or closed. Payload: the new state.
SlotDescription
previewCustom preview body instead of the default diff lines.

Motion

Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.

IDTriggerElementPropertyFrom → toTimingReduced motion
FCH1List appears or a file is addedfile chipopacity0 → 1duration-md (260)ease-outdelay stagger-step x chipShows at once
FCH2List appears or a file is addedfile chipscalescale-press → 1duration-xl (380)ease-settledelay stagger-step x chipShows at once
FCH3List appearsmore buttonopacity0 → 1duration-md (260)ease-outdelay stagger-step x visibleShows at once
FCH4Counts change while the agent workslines added, lines removednumber rollold count → new countduration-5xl (640)ease-outSwaps at once
FCH5Hover or pressfile chip, more buttonbackground colorbg-primary_alt → bg-tertiaryduration-2xs (120)ease-outSwaps at once
FCH6Pressfile chipscale1 → scale-pressduration-2xs (120)ease-outStays still
FCH7Preview opensdiff previewopacity0 → 1duration-sm (200)ease-outShows at once
FCH8Preview opensdiff preview (origin: chip corner)scale, translate yscale-press, distance-rise → 1, 0duration-md (260)ease-settleShows at once
FCH9Preview closesdiff previewopacity1 → 0duration-2xs (120)ease-exitHides at once
FCH10Expand or collapse the listrootheightone row → content heightduration-2xl (420)ease-settleResizes at once
FCH11Expandextra file chipsopacity0 → 1duration-md (260)ease-outdelay duration-2xs + stagger-step x chipShows at once
FCH12Collapseextra file chipsopacity1 → 0duration-2xs (120)ease-exitHides at once
FCH13Expand or collapsemore button labeltext morph+2 more → Show fewerduration-3xl (460)ease-outSwaps at once

Accessibility

  • The root is a list labeled "Changed files"; each chip is a list item holding one button.
  • Each chip button has an accessible name with the numbers in words, for example "menu.ts, 8 lines added, 2 lines removed"; the visible + and - signs carry the meaning, so color is never the only cue.
  • The preview opens on hover after previewDelay and at once on keyboard focus, closes on Escape, blur and pointer leave, and is linked to its chip with aria-describedby; it holds no focusable content, so Tab moves to the next chip.
  • The more button has aria-expanded and an accessible name that says the count, for example "Show 2 more files"; after collapse, focus stays on it.
  • Chips and the more button are 28px tall (target at least 24px) with a visible focus ring (Focus rings/focus-ring).
  • Diff lines use utility-green-700 on bg-success-primary and utility-red-700 on bg-error-primary, both at least 4.5:1 in light and dark, plus the + and - gutter.