Enable a full-featured yet lightweight editor that lazy-loads when needed on top of any File or FileDiff. All the ergonomics and customization of @pierre/diffs, with everything you need to edit in place.
16 unmodified lines1718192021222324252625 unmodified lines525354555657585960616263646565 unmodified lines13113213313413513613713813914014114214314417 unmodified lines16216316416516616716816917017117217317417517617717817918018118218318418518663 unmodified lines25025125225325425525625725825926026126226326426526618 unmodified lines28528628728828929029129229325 unmodified lines31932032132232332432532632750 unmodified lines3783793803813823833843853864 unmodified lines16 unmodified linesgetSessionGitStatus,getSessionPaths,} from './mockData';export type ThemeType = 'light' | 'dark';// The editor's stylesheet flattens every line number to one neutral colour// (`--diffs-editor-line-number-fg`) and is injected as an unlayered <style>,// so it overrides the library's per-line colouring (which lives in @layer// base). We adopt this extra, higher-specificity unlayered sheet into the25 unmodified lines// component, so it's created in an effect and torn down on session change.function ChangesTree({session,activePath,themeType,onSelect,}: {session: AuiSession;activePath: string | null;themeType: ThemeType;onSelect: (path: string) => void;}) {const containerRef = useRef<HTMLDivElement | null>(null);const treeRef = useRef<FileTree | null>(null);65 unmodified lines};}, [session]);// Inline color-scheme beats the tree's `:host { color-scheme: light dark }`,// flipping its light-dark() colours with our toggle.useEffect(() => {if (containerRef.current != null) {containerRef.current.style.colorScheme = themeType;}}, [themeType, session]);// Keep the highlighted row matched to the active file.useEffect(() => {const tree = treeRef.current;17 unmodified linesreturn <div ref={containerRef} className="aui-tree" />;}export interface AgentUiProps {// Theme is controlled by the parent so the toggle can live outside the// component (the homepage section renders its own button group).themeType: ThemeType;// Highlight themes the surrounding worker pool was initialized with. Defaults// to the shared homepage pool's themes.theme?: { dark: string; light: string };// Server-rendered diff HTML keyed by file path. When present the matching// FileDiff hydrates from this markup (already syntax-highlighted) instead of// waiting on the client worker, which also avoids an SSR/client mismatch.prerenderedDiffs?: Record<string, string>;}export function AgentUi({themeType,theme = DEFAULT_THEMES,prerenderedDiffs,}: AgentUiProps) {const session = AUI_SESSIONS[0];const [activePath, setActivePath] = useState<string | null>(() => session.changedFiles[0]?.path ?? null63 unmodified lines: null,[activeFile, editKey]);// Server-rendered, already-highlighted HTML for the active diff. The snapshot// is generated in dark mode, so only reuse it while the demo is in dark mode// (otherwise a freshly opened file would flash dark before re-highlighting).// It's also only safe when the file is unedited so the markup matches// `fileDiff`.const activePrerenderedHTML =themeType === 'dark' &&activePath != null &&editsRef.current.get(editKey) == null? prerenderedDiffs?.[activePath]: undefined;const breadcrumbSegments = activePath != null ? activePath.split('/') : [];18 unmodified lines}, [activePath]);return (<EditorProvider editor={editor}><div className="aui" data-theme-type={themeType} data-embedded="true"><div className="aui-body"><section className="aui-center"><header className="aui-center-header"><nav className="aui-breadcrumb" aria-label="File path">25 unmodified linesfileDiff={fileDiff}className="aui-surface"options={{theme,themeType,disableFileHeader: true,overflow: 'wrap',diffStyle: 'unified',}}50 unmodified lines</div><ChangesTreesession={session}activePath={activePath}themeType={themeType}onSelect={openFile}/></aside></div>4 unmodified lines16 unmodified lines171819202122232425 unmodified lines50515253545556575859606165 unmodified lines12712812913013113213313413513613713813914017 unmodified lines15815916016116216316416516616716816917017117217317417517617763 unmodified lines24124224324424524624724824925025125218 unmodified lines27127227327427527627727827925 unmodified lines30530630730830931031131231350 unmodified lines3643653663673683693703714 unmodified lines16 unmodified linesgetSessionGitStatus,getSessionPaths,} from './mockData';// The editor's stylesheet flattens every line number to one neutral colour// (`--diffs-editor-line-number-fg`) and is injected as an unlayered <style>,// so it overrides the library's per-line colouring (which lives in @layer// base). We adopt this extra, higher-specificity unlayered sheet into the25 unmodified lines// component, so it's created in an effect and torn down on session change.function ChangesTree({session,activePath,onSelect,}: {session: AuiSession;activePath: string | null;onSelect: (path: string) => void;}) {const containerRef = useRef<HTMLDivElement | null>(null);const treeRef = useRef<FileTree | null>(null);65 unmodified lines};}, [session]);// Inline color-scheme beats the tree's `:host { color-scheme: light dark }`,// pinning its light-dark() colours to the demo's dark mode.useEffect(() => {if (containerRef.current != null) {containerRef.current.style.colorScheme = 'dark';}}, [session]);// Keep the highlighted row matched to the active file.useEffect(() => {const tree = treeRef.current;17 unmodified linesreturn <div ref={containerRef} className="aui-tree" />;}export interface AgentUiProps {// Highlight themes the surrounding worker pool was initialized with. Defaults// to the shared homepage pool's themes.theme?: { dark: string; light: string };// Server-rendered diff HTML keyed by file path. When present the matching// FileDiff hydrates from this markup (already syntax-highlighted) instead of// waiting on the client worker, which also avoids an SSR/client mismatch.prerenderedDiffs?: Record<string, string>;}// The demo is always dark: the snapshot is prerendered dark and matching it// avoids theme flashing, so there is no light/dark toggle.export function AgentUi({ theme = DEFAULT_THEMES, prerenderedDiffs }: AgentUiProps) {const session = AUI_SESSIONS[0];const [activePath, setActivePath] = useState<string | null>(() => session.changedFiles[0]?.path ?? null63 unmodified lines: null,[activeFile, editKey]);// Server-rendered, already-highlighted HTML for the active diff. Only safe// when the file is unedited so the markup matches `fileDiff`.const activePrerenderedHTML =activePath != null && editsRef.current.get(editKey) == null? prerenderedDiffs?.[activePath]: undefined;const breadcrumbSegments = activePath != null ? activePath.split('/') : [];18 unmodified lines}, [activePath]);return (<EditorProvider editor={editor}><div className="aui" data-theme-type="dark" data-embedded="true"><div className="aui-body"><section className="aui-center"><header className="aui-center-header"><nav className="aui-breadcrumb" aria-label="File path">25 unmodified linesfileDiff={fileDiff}className="aui-surface"options={{theme,themeType: 'dark',disableFileHeader: true,overflow: 'wrap',diffStyle: 'unified',}}50 unmodified lines</div><ChangesTreesession={session}activePath={activePath}onSelect={openFile}/></aside></div>4 unmodified lines
Editor mode (experimental) makes any code surface—File or FileDiff—editable in place. Switch from Review (read-only) to Edit, then start typing in the code below and it updates as you edit. Select text to try the custom Selection Action widget.
1234567891011121314151617181920212223242526272829303132export interface DebounceOptions { waitMs: number; trailing?: boolean;}
export function debounce<Args extends unknown[]>( fn: (...args: Args) => void, options: DebounceOptions,) { let timer: ReturnType<typeof setTimeout> | undefined;
const debounced = (...args: Args) => { if (timer != null) { clearTimeout(timer); }
timer = setTimeout(() => { timer = undefined; if (options.trailing !== false) { fn(...args); } }, options.waitMs); };
debounced.cancel = () => { clearTimeout(timer); timer = undefined; };
return debounced;}
Select any text to reveal the gutter icon, then click it to open a custom widget rendered with renderSelectionAction(). Run a transform on the selection—here, wrap a string for translation or shout it in caps—or drive the same wrap from the toolbar, then reset the surface to its original source.
12345678910111213141516171819const greeting = 'Welcome back'const farewell = 'See you soon'const errorText = 'Something went wrong'
type Banner = { title: string; tone: 'info' | 'error' }
function renderBanner(name: string): Banner { const title = greeting + ', ' + name + '!' return { title, tone: 'info' }}
function renderError(): Banner { return { title: errorText, tone: 'error' }}
function renderFooter(year: number) { return farewell + ' · © ' + year}
Use editor.setMarkers() to inject inline context into your code for linter, formatting, and more. Includes support for severity-aware underlines and hover popups. Hover over markers with wavy underlines below to see an example.
1234567891011121314151617181920function calculateTotal(items, taxRate) { var total = 0 for (var i = 0; i < items.length; i++) { total += items[i].price }
let tax = total * taxRate console.log('subtotal', total)
if (total == 0) { return null }
return { subtotal: total, tax, grandTotal: total + tax, }}
Open the search panel with Cmd/Ctrl-F on any File or FileDiff to find and replace. The example below shows the search panel pre-filled—press Enter or use its arrows to jump between matches, and toggle case, whole-word, or regex as you go.
12345678910111213141516type User = { id: string; name: string; email: string;};
function formatUser(user: User) { const name = user.name.trim(); const email = user.email.toLowerCase(); return { id: user.id, name, email };}
export function getUsers(users: User[]) { return users.map(formatUser);}
Every edit lands on a structure-aware undo stack. The example below loads with a short refactor already applied across several commits—use the toolbar controls (or Cmd/Ctrl-Z and Shift to redo) to walk back and forth through each change.
12345678910111213141516171819function calculateCart(items) { var total = 0 for (var i = 0; i < items.length; i++) { total = total + items[i].price * items[i].qty }
var discount = 0 if (total > 100) { discount = total * 0.1 }
var shipping = 5 if (total > 50) { shipping = 0 }
return total - discount + shipping}
Edit mode ships with all the additional shortcuts your users will need out of the box. Use the example File below to try the shortcuts you see in the table. Editing the example File will not update the table.
123456789101112131415161718192021222324252627282930// The data behind the table on the right—this very page maps over it.// Editing here won't rebuild the table, but go ahead: the surface is live.// `keys` are alternatives (joined by /); `modifiers` are held together.// `mod` adds the platform key: Cmd on macOS, Ctrl everywhere else.export const shortcuts = [ // Editing { keys: ['Tab'], action: 'Indent line or selection' }, { keys: ['Tab'], action: 'Outdent line or selection', modifiers: ['Shift'] }, { keys: ['X'], action: 'Cut', mod: true }, { keys: ['C'], action: 'Copy', mod: true }, { keys: ['V'], action: 'Paste', mod: true }, // Selection & cursor { keys: ['←', '→', '↑', '↓'], action: 'Move the cursor' }, { keys: ['←', '→', '↑', '↓'], action: 'Extend the selection', modifiers: ['Shift'] }, { keys: ['←', '→'], action: 'Jump to line start / end', mod: true }, { keys: ['Home', 'End'], action: 'Jump to document start / end', mod: true }, { keys: ['A'], action: 'Select all', mod: true }, { keys: ['Esc'], action: 'Collapse to a single cursor' }, // History { keys: ['Z'], action: 'Undo', mod: true }, { keys: ['Z'], action: 'Redo', modifiers: ['Shift'], mod: true }, // Find { keys: ['F'], action: 'Open the search panel', mod: true }, { keys: ['D'], action: 'Find next match of the selection', mod: true }, { keys: ['Enter'], action: 'Next match (in search panel)' }, { keys: ['Esc'], action: 'Close the search panel' }, // Multiple cursors { keys: ['Click'], action: 'Add a cursor at the click', mod: true },];
| Key | Action |
|---|---|
| Editing | |
| Tab | Indent line or selection |
| ShiftTab | Outdent line or selection |
| CmdX | Cut |
| CmdC | Copy |
| CmdV | Paste |
| Selection & cursor | |
| ←→↑↓ | Move the cursor |
| Shift←→↑↓ | Extend the selection |
| Cmd←→ | Jump to line start / end |
| CmdHomeEnd | Jump to document start / end |
| CmdA | Select all |
| Esc | Collapse to a single cursor |
| History | |
| CmdZ | Undo |
| CmdShiftZ | Redo |
| Find | |
| CmdF | Open the search panel |
| CmdD | Find next match of the selection |
| Enter | Next match (in search panel) |
| Esc | Close the search panel |
| Multiple cursors | |
| CmdClick | Add a cursor at the click |
The demos above cover the headline features. Here's the rest of what edit mode gives you for free.
File, FileDiff, or MultiFileDiff; the new-file side of a diff re-tokenizes as you type.VirtualizedFile / VirtualizedFileDiff; off-screen lines render on demand.contentEditable with role="textbox"; autocorrect, spellcheck, and capitalization off.@pierre/diffs/editor entry point—import it only when editing begins.Collectively, our team brings over 150 years of expertise designing, building, and scaling the world's largest distributed systems at Cloudflare, Coinbase, Discord, GitHub, Reddit, Stripe, X, and others.