Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions src/components/DiffCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,20 @@ export type DiffData = CanonicalDiffData;
export interface DiffCardProps {
/** The diff data to visualize. */
diff: DiffData;
/** Called when the user accepts the change. */
onAccept?: (diffId: string) => void;
/** Called when the user rejects the change. */
onReject?: (diffId: string) => void;
/**
* Called when the user accepts the change.
*
* Receives the pending-action id (`diff.actionId`, falling back to
* the deprecated `diff.diffId` for older payloads).
*/
onAccept?: (actionId: string) => void;
/**
* Called when the user rejects the change.
*
* Receives the pending-action id (`diff.actionId`, falling back to
* the deprecated `diff.diffId` for older payloads).
*/
onReject?: (actionId: string) => void;
/** Whether the accept/reject action is in progress. */
loading?: boolean;
/** Additional CSS class name. */
Expand All @@ -32,13 +42,14 @@ type DiffCardStatus = 'pending' | 'accepted' | 'rejected';
* ```tsx
* <DiffCard
* diff={{
* diffId: 'abc123',
* actionId: 'abc123',
* diffId: 'abc123', // kept for back-compat; same value as actionId
* diffType: 'edit',
* originalContent: 'Hello world',
* replacementContent: 'Hello universe',
* }}
* onAccept={(id) => apiFetch({ path: `/resolve/${id}`, method: 'POST', data: { action: 'accept' } })}
* onReject={(id) => apiFetch({ path: `/resolve/${id}`, method: 'POST', data: { action: 'reject' } })}
* onAccept={(id) => apiFetch({ path: '/actions/resolve', method: 'POST', data: { action_id: id, decision: 'accepted' } })}
* onReject={(id) => apiFetch({ path: '/actions/resolve', method: 'POST', data: { action_id: id, decision: 'rejected' } })}
* />
* ```
*/
Expand All @@ -58,14 +69,19 @@ export function DiffCard({
className,
].filter(Boolean).join(' ');

// Prefer `actionId` (canonical since v0.11). Fall back to `diffId`
// for payloads produced by older parsers or consumers that still
// construct `CanonicalDiffData` by hand without setting `actionId`.
const resolvedId = diff.actionId || diff.diffId;

const handleAccept = () => {
setStatus('accepted');
onAccept?.(diff.diffId);
onAccept?.(resolvedId);
};

const handleReject = () => {
setStatus('rejected');
onReject?.(diff.diffId);
onReject?.(resolvedId);
};

const diffHtml = renderDiff(diff);
Expand Down
48 changes: 39 additions & 9 deletions src/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ export interface CanonicalDiffEditorData {
}

export interface CanonicalDiffData {
/**
* Pending-action id the backend assigned when staging this preview.
*
* This is the canonical field as of v0.11 — it aligns with the
* unified "pending action" primitive on the server side (any tool
* invocation can be staged + previewed + resolved through the same
* /actions/resolve endpoint, not just content diffs).
*/
actionId: string;
/**
* Back-compat alias for `actionId`.
*
* Always populated with the same value as `actionId`. Kept so
* consumers that wired against the v0.7–v0.10 `diffId` field keep
* working through one more major version. New code should read
* `actionId`.
*
* @deprecated Use `actionId`.
*/
diffId: string;
diffType: CanonicalDiffType;
originalContent: string;
Expand Down Expand Up @@ -74,13 +93,23 @@ export function parseCanonicalDiff( value: unknown ): CanonicalDiffData | null {
const container = isRecord( value.data ) ? value.data : value;
const rawDiff = isRecord( container.diff ) ? container.diff : container;

const diffId = typeof rawDiff.diffId === 'string'
? rawDiff.diffId
: typeof rawDiff.diff_id === 'string'
? rawDiff.diff_id
: typeof container.diff_id === 'string'
? container.diff_id
: '';
// Resolve the pending-action id. Prefer the canonical `actionId`
// (server unified on pending-action vocabulary in mid-2026) and
// fall back to the historical `diffId` / `diff_id` shapes so
// older backends and stored payloads keep rendering.
const resolvedId = typeof rawDiff.actionId === 'string'
? rawDiff.actionId
: typeof rawDiff.action_id === 'string'
? rawDiff.action_id
: typeof container.action_id === 'string'
? container.action_id
: typeof rawDiff.diffId === 'string'
? rawDiff.diffId
: typeof rawDiff.diff_id === 'string'
? rawDiff.diff_id
: typeof container.diff_id === 'string'
? container.diff_id
: '';

const diffType = rawDiff.diffType === 'replace' || rawDiff.diffType === 'insert'
? rawDiff.diffType
Expand All @@ -102,7 +131,7 @@ export function parseCanonicalDiff( value: unknown ): CanonicalDiffData | null {
? rawDiff.replacement_content
: '';

if ( ! diffId && ! originalContent && ! replacementContent ) {
if ( ! resolvedId && ! originalContent && ! replacementContent ) {
return null;
}

Expand All @@ -129,7 +158,8 @@ export function parseCanonicalDiff( value: unknown ): CanonicalDiffData | null {
: undefined;

return {
diffId,
actionId: resolvedId,
diffId: resolvedId,
diffType,
originalContent,
replacementContent,
Expand Down