mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
Fix nested agent config history diffs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
53bf68638d
commit
6aa50ea1fc
3 files changed
+56
-10
No files matched your search
@@ -4,7 +4,7 @@ import { flushSync } from "react-dom";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AgentConfigRevision } from "@paperclipai/shared";
|
||||
import { AgentConfigHistory } from "./AgentConfigHistory";
|
||||
import { AgentConfigHistory, revisionDiff } from "./AgentConfigHistory";
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
@@ -44,4 +44,34 @@ describe("AgentConfigHistory", () => {
|
||||
flushSync(() => confirmButton?.click());
|
||||
expect(onRestore).toHaveBeenCalledWith("revision-1");
|
||||
});
|
||||
|
||||
it("expands legacy whole-object changes into field-level rows", () => {
|
||||
const legacyRevision = {
|
||||
...revision,
|
||||
changedKeys: ["adapterConfig"],
|
||||
beforeConfig: { adapterConfig: { effort: "high", graceSec: 15, headers: { version: "1" } } },
|
||||
afterConfig: { adapterConfig: { effort: "high", graceSec: 42, headers: { version: "2" } } },
|
||||
} satisfies AgentConfigRevision;
|
||||
|
||||
expect(revisionDiff(legacyRevision)).toEqual([
|
||||
{ key: "adapterConfig.graceSec", before: 15, after: 42 },
|
||||
{ key: "adapterConfig.headers.version", before: "1", after: "2" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses the same human-readable labels as the review changes popover", () => {
|
||||
const labelRevision = {
|
||||
...revision,
|
||||
changedKeys: ["adapterConfig.graceSec"],
|
||||
beforeConfig: { adapterConfig: { graceSec: 15 } },
|
||||
afterConfig: { adapterConfig: { graceSec: 42 } },
|
||||
} satisfies AgentConfigRevision;
|
||||
flushSync(() => root.render(<AgentConfigHistory revisions={[labelRevision]} onRestore={vi.fn()} restoring={false} />));
|
||||
|
||||
const viewDiffButton = container.querySelector<HTMLButtonElement>('button[aria-label="View revision diff"]');
|
||||
flushSync(() => viewDiffButton?.click());
|
||||
|
||||
expect(document.body.textContent).toContain("Grace Sec");
|
||||
expect(document.body.textContent).toContain("Grace Sec1542");
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { formatDate } from "../lib/utils";
|
||||
import { formatAgentConfigValue } from "../lib/agent-config-changeset";
|
||||
import { formatAgentConfigValue, labelForAgentConfigKey } from "../lib/agent-config-changeset";
|
||||
|
||||
function valueAt(snapshot: Record<string, unknown>, key: string): unknown {
|
||||
if (Object.prototype.hasOwnProperty.call(snapshot, key)) return snapshot[key];
|
||||
@@ -29,11 +29,27 @@ function valueAt(snapshot: Record<string, unknown>, key: string): unknown {
|
||||
}
|
||||
|
||||
export function revisionDiff(revision: AgentConfigRevision) {
|
||||
return revision.changedKeys.map((key) => ({
|
||||
key,
|
||||
before: valueAt(revision.beforeConfig, key),
|
||||
after: valueAt(revision.afterConfig, key),
|
||||
}));
|
||||
return revision.changedKeys.flatMap((key) => expandRevisionDiff(key, valueAt(revision.beforeConfig, key), valueAt(revision.afterConfig, key)));
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function expandRevisionDiff(key: string, before: unknown, after: unknown): Array<{ key: string; before: unknown; after: unknown }> {
|
||||
if (!isRecord(before) && !isRecord(after)) return [{ key, before, after }];
|
||||
if (before !== undefined && !isRecord(before)) return [{ key, before, after }];
|
||||
if (after !== undefined && !isRecord(after)) return [{ key, before, after }];
|
||||
|
||||
const beforeRecord = isRecord(before) ? before : {};
|
||||
const afterRecord = isRecord(after) ? after : {};
|
||||
const fields = [...new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)])].sort();
|
||||
return fields.flatMap((field) => {
|
||||
const fieldBefore = beforeRecord[field];
|
||||
const fieldAfter = afterRecord[field];
|
||||
if (JSON.stringify(fieldBefore) === JSON.stringify(fieldAfter)) return [];
|
||||
return expandRevisionDiff(key + "." + field, fieldBefore, fieldAfter);
|
||||
});
|
||||
}
|
||||
|
||||
function RevisionDiffRows({ revision }: { revision: AgentConfigRevision }) {
|
||||
@@ -41,7 +57,7 @@ function RevisionDiffRows({ revision }: { revision: AgentConfigRevision }) {
|
||||
<div className="divide-y divide-border">
|
||||
{revisionDiff(revision).map((entry) => (
|
||||
<div key={entry.key} className="space-y-2 py-3">
|
||||
<div className="font-mono text-xs text-muted-foreground">{entry.key}</div>
|
||||
<div className="text-xs font-medium text-muted-foreground">{labelForAgentConfigKey(entry.key)}</div>
|
||||
<div className="grid gap-2 text-xs sm:grid-cols-2">
|
||||
<div className="min-w-0 rounded-md bg-muted/40 p-2 font-mono break-words">{formatAgentConfigValue(entry.before)}</div>
|
||||
<div className="min-w-0 rounded-md bg-muted/40 p-2 font-mono break-words">{formatAgentConfigValue(entry.after)}</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export type AgentConfigChange = {
|
||||
|
||||
type OverlayGroup = "identity" | "adapterConfig" | "heartbeat" | "runtime";
|
||||
|
||||
function labelForKey(key: string): string {
|
||||
export function labelForAgentConfigKey(key: string): string {
|
||||
return key.replace(/^.*\./, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (value) => value.toUpperCase());
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function buildAgentConfigChanges(agent: Agent, overlay: AgentConfigOverla
|
||||
: group === "runtime"
|
||||
? field
|
||||
: `adapterConfig.${field}`;
|
||||
changes.push({ key, label: labelForKey(key), section: sectionForKey(key), before: originalValue(agent, group, field), after });
|
||||
changes.push({ key, label: labelForAgentConfigKey(key), section: sectionForKey(key), before: originalValue(agent, group, field), after });
|
||||
}
|
||||
};
|
||||
addGroup("identity");
|
||||
|
||||
Reference in new issue
Block a user