-
-
+
+
+ {title ?
{title} : null}
{message}
- {action && onAction && (
-
-
+ {action && actionHref ? (
+
+
+
+ {action}
+
+
+ ) : null}
+ {action && onAction && !actionHref ? (
+
+
{action}
- )}
+ ) : null}
);
}
diff --git a/ui/src/components/InheritanceChain.tsx b/ui/src/components/InheritanceChain.tsx
new file mode 100644
index 00000000000..099559acc60
--- /dev/null
+++ b/ui/src/components/InheritanceChain.tsx
@@ -0,0 +1,53 @@
+import { ChevronRight } from "lucide-react";
+import { Link } from "@/lib/router";
+import { cn } from "@/lib/utils";
+
+export type InheritanceChainItem = {
+ label: string;
+ detail?: string | null;
+ href?: string | null;
+ active?: boolean;
+ unavailable?: boolean;
+};
+
+export function InheritanceChain({ items }: { items: InheritanceChainItem[] }) {
+ return (
+
+ {items.map((item, index) => (
+
+ {index > 0 ? : null}
+
+
+ ))}
+
+ );
+}
+
+function InheritanceChainStep({ item }: { item: InheritanceChainItem }) {
+ const className = cn(
+ "inline-flex min-h-8 max-w-full flex-col justify-center rounded-md border px-2.5 py-1 text-left text-xs transition-colors",
+ item.active
+ ? "border-primary/50 bg-primary/10 text-primary"
+ : item.unavailable
+ ? "border-dashed border-border bg-muted/30 text-muted-foreground"
+ : "border-border bg-background text-foreground",
+ item.href && !item.unavailable ? "hover:bg-accent/60" : null,
+ );
+
+ const content = (
+ <>
+
{item.label}
+ {item.detail ?
{item.detail} : null}
+ >
+ );
+
+ if (item.href && !item.unavailable) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return
{content} ;
+}
diff --git a/ui/src/components/MemoryOverrideCard.tsx b/ui/src/components/MemoryOverrideCard.tsx
new file mode 100644
index 00000000000..9f7b93fe579
--- /dev/null
+++ b/ui/src/components/MemoryOverrideCard.tsx
@@ -0,0 +1,265 @@
+import { useEffect, useMemo, useState } from "react";
+import type { MemoryBinding, MemoryResolvedBinding } from "@paperclipai/shared";
+import { AlertCircle, Database } from "lucide-react";
+import { EmptyState } from "./EmptyState";
+import { InheritanceChain } from "./InheritanceChain";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Skeleton } from "@/components/ui/skeleton";
+
+const INHERIT_VALUE = "__inherit__";
+
+type MemoryOverrideTargetType = "agent" | "project";
+
+interface MemoryOverrideCardProps {
+ targetType: MemoryOverrideTargetType;
+ resolvedBinding: MemoryResolvedBinding | null | undefined;
+ bindings: MemoryBinding[];
+ source: MemoryResolvedBinding["source"] | null | undefined;
+ onSave: (bindingId: string | null) => void;
+ loading?: boolean;
+ error?: Error | null;
+ saving?: boolean;
+ saveError?: Error | null;
+ onRetry?: () => void;
+ companySettingsHref?: string;
+ agentOverrideCount?: number;
+}
+
+function targetLabel(targetType: MemoryOverrideTargetType) {
+ return targetType === "agent" ? "agent" : "project";
+}
+
+function titleLabel(targetType: MemoryOverrideTargetType) {
+ return targetType === "agent" ? "Agent memory" : "Project memory";
+}
+
+function overrideLabel(targetType: MemoryOverrideTargetType) {
+ return targetType === "agent" ? "Agent override" : "Project override";
+}
+
+function inheritLabel(targetType: MemoryOverrideTargetType) {
+ return targetType === "agent" ? "Inherit project/company default" : "Inherit company default";
+}
+
+function sourceLabel(source: MemoryResolvedBinding["source"] | null | undefined) {
+ if (source === "agent_override") return "Agent override";
+ if (source === "project_override") return "Project override";
+ if (source === "company_default") return "Company default";
+ if (source === "binding_key") return "Direct binding key";
+ return "Unconfigured";
+}
+
+function describeBinding(binding: MemoryBinding | null | undefined, targetType: MemoryOverrideTargetType) {
+ if (!binding) return `No memory binding resolves for this ${targetLabel(targetType)} yet.`;
+ return `${binding.name ?? binding.key} (${binding.providerKey})`;
+}
+
+function selectedBindingLabel(binding: MemoryBinding | undefined, targetType: MemoryOverrideTargetType) {
+ if (!binding) return inheritLabel(targetType);
+ return `${binding.name ?? binding.key} (${binding.providerKey})`;
+}
+
+function bindingPreviewLabel(binding: MemoryBinding | null | undefined, fallback: string) {
+ return binding ? `${binding.providerKey}:${binding.key}` : fallback;
+}
+
+function agentOverrideSummary(count: number | undefined) {
+ if (typeof count !== "number") return null;
+ return `${count} agent override${count === 1 ? "" : "s"} configured.`;
+}
+
+function bindingSelectionFromResolved(
+ resolvedBinding: MemoryResolvedBinding | null | undefined,
+ targetType: MemoryOverrideTargetType,
+) {
+ if (resolvedBinding?.targetType === targetType && resolvedBinding.binding) {
+ return resolvedBinding.binding.id;
+ }
+ return INHERIT_VALUE;
+}
+
+export function MemoryOverrideCard({
+ targetType,
+ resolvedBinding,
+ bindings,
+ source,
+ onSave,
+ loading = false,
+ error = null,
+ saving = false,
+ saveError = null,
+ onRetry,
+ companySettingsHref = "/company/settings#memory",
+ agentOverrideCount,
+}: MemoryOverrideCardProps) {
+ const [selectedBindingId, setSelectedBindingId] = useState(INHERIT_VALUE);
+
+ const currentSelection = useMemo(
+ () => bindingSelectionFromResolved(resolvedBinding, targetType),
+ [resolvedBinding, targetType],
+ );
+
+ useEffect(() => {
+ setSelectedBindingId(currentSelection);
+ }, [currentSelection]);
+
+ const selectedBinding = bindings.find((binding) => binding.id === selectedBindingId);
+ const hasUnsavedChange = selectedBindingId !== currentSelection;
+ const chainSummary = agentOverrideSummary(agentOverrideCount);
+ const nextBinding = selectedBindingId === INHERIT_VALUE ? null : selectedBinding;
+ const inheritanceItems = [
+ {
+ label: "Company default",
+ detail: source === "company_default" ? resolvedBinding?.binding?.providerKey : null,
+ href: companySettingsHref,
+ active: source === "company_default",
+ },
+ {
+ label: "Project override",
+ detail: targetType === "agent" ? "Issue/project scope" : "This project",
+ active: source === "project_override",
+ },
+ {
+ label: targetType === "project" ? "Agent overrides" : "Agent override",
+ detail: targetType === "project" ? chainSummary : "This agent",
+ active: source === "agent_override",
+ unavailable: targetType === "project" && !agentOverrideCount,
+ },
+ ];
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{titleLabel(targetType)}
+
{describeBinding(resolvedBinding?.binding, targetType)}
+
Source: {sourceLabel(source)}
+
+
+
+
+
+
+
+ {bindings.length === 0 ? (
+
+ ) : (
+
+
+
+ {overrideLabel(targetType)}
+
+
+
+
+
+ {inheritLabel(targetType)}
+ {bindings.map((binding) => (
+
+
+
+ ))}
+
+
+
+
onSave(selectedBindingId === INHERIT_VALUE ? null : selectedBindingId)}
+ >
+ {saving ? "Saving..." : "Save override"}
+
+
+
+ {hasUnsavedChange ? (
+
+ New runs will use
+
+ {bindingPreviewLabel(nextBinding, selectedBindingLabel(selectedBinding, targetType))}
+
+ instead of
+
+ {bindingPreviewLabel(resolvedBinding?.binding, "no binding")}
+
+ Switching will not affect existing records.
+
+ ) : null}
+
+ {saveError ? (
+
{saveError.message}
+ ) : null}
+
+ )}
+
+
+
+ );
+}
+
+function BindingSelectLabel({ binding }: { binding: MemoryBinding }) {
+ return (
+
+
+ {binding.name ?? binding.key}
+ {binding.providerKey}
+
+
+ {binding.enabled ? "Enabled" : "Disabled"}
+
+
+ );
+}
diff --git a/ui/src/components/MemoryRecordRow.tsx b/ui/src/components/MemoryRecordRow.tsx
new file mode 100644
index 00000000000..bf62c3bdc50
--- /dev/null
+++ b/ui/src/components/MemoryRecordRow.tsx
@@ -0,0 +1,175 @@
+import type { Agent, Issue, MemoryBinding, MemoryRecord, Project } from "@paperclipai/shared";
+import type { KeyboardEvent } from "react";
+import { Link } from "@/lib/router";
+import { agentUrl, cn, issueUrl, projectUrl, relativeTime } from "../lib/utils";
+import { Checkbox } from "@/components/ui/checkbox";
+import { StatusBadge } from "./StatusBadge";
+
+export function labelizeMemoryValue(value: string) {
+ return value.replace(/_/g, " ");
+}
+
+export function summarizeMemoryRecord(record: MemoryRecord, maxLength = 360) {
+ const text = (record.summary ?? record.content).replace(/\s+/g, " ").trim();
+ return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
+}
+
+export function getMemoryRecordTitle(record: MemoryRecord) {
+ return record.title ?? record.citation?.sourceTitle ?? labelizeMemoryValue(record.source?.kind ?? "memory");
+}
+
+export function memoryRecordSourceLabel(record: MemoryRecord, issuesById?: ReadonlyMap
) {
+ const issueId = record.source?.issueId ?? record.scope.issueId ?? null;
+ const issue = issueId ? issuesById?.get(issueId) : null;
+ if (issue) return issue.identifier ?? issue.title;
+ if (record.source?.runId ?? record.scope.runId) return `run ${(record.source?.runId ?? record.scope.runId)!.slice(0, 8)}`;
+ if (record.source?.externalRef) return record.source.externalRef;
+ return labelizeMemoryValue(record.source?.kind ?? record.scopeType);
+}
+
+export function memoryRecordSourceHref(record: MemoryRecord, issuesById?: ReadonlyMap) {
+ const issueId = record.source?.issueId ?? record.scope.issueId ?? null;
+ const issue = issueId ? issuesById?.get(issueId) : null;
+ if (issue) {
+ const base = issueUrl(issue);
+ if (record.source?.commentId) return `${base}#comment-${record.source.commentId}`;
+ if (record.source?.documentKey) return `${base}#document-${record.source.documentKey}`;
+ return base;
+ }
+ const runId = record.source?.runId ?? record.scope.runId ?? null;
+ const agentId = record.scope.agentId ?? null;
+ if (runId && agentId) return `/agents/${agentId}/runs/${runId}`;
+ return null;
+}
+
+export function memoryRecordReviewStatus(record: MemoryRecord) {
+ return record.revokedAt || record.retentionState === "revoked" ? "revoked" : record.reviewState;
+}
+
+export function MemoryRecordRow({
+ record,
+ binding,
+ issuesById,
+ agentsById,
+ projectsById,
+ onSelect,
+ selected = false,
+ onSelectedChange,
+ variant = "feed",
+}: {
+ record: MemoryRecord;
+ binding?: MemoryBinding | null;
+ issuesById?: ReadonlyMap;
+ agentsById?: ReadonlyMap;
+ projectsById?: ReadonlyMap;
+ onSelect?: () => void;
+ selected?: boolean;
+ onSelectedChange?: (checked: boolean) => void;
+ variant?: "feed" | "compact";
+}) {
+ const sourceUrl = memoryRecordSourceHref(record, issuesById);
+ const agent = record.scope.agentId ? agentsById?.get(record.scope.agentId) : null;
+ const project = record.scope.projectId ? projectsById?.get(record.scope.projectId) : null;
+ const bindingLabel = binding?.name ?? binding?.key ?? record.providerKey;
+ const summaryLength = variant === "compact" ? 280 : 360;
+ const handleRowKeyDown = (event: KeyboardEvent) => {
+ if (!onSelect || event.currentTarget !== event.target) return;
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ onSelect();
+ }
+ };
+
+ const content = (
+ <>
+ {onSelectedChange ? (
+ event.stopPropagation()}
+ onKeyDown={(event) => event.stopPropagation()}
+ >
+ onSelectedChange(checked === true)}
+ aria-label={`Select ${getMemoryRecordTitle(record)}`}
+ />
+
+ ) : null}
+
+
+
+ {getMemoryRecordTitle(record)}
+
+ {bindingLabel}
+
+
+
{summarizeMemoryRecord(record, summaryLength)}
+
+
+
Source
+
{memoryRecordSourceLabel(record, issuesById)}
+
{labelizeMemoryValue(record.source?.kind ?? record.scopeType)}
+ {record.citation?.label || record.citation?.sourceTitle ? (
+
{record.citation.label ?? record.citation.sourceTitle}
+ ) : null}
+ {sourceUrl ? (
+ onSelect ? (
+
event.stopPropagation()}
+ >
+ Open source
+
+ ) : (
+
Open source
+ )
+ ) : null}
+
+
+
Context
+ {project ? (
+ onSelect ? (
+
{project.name}
+ ) : (
+
+ {project.name}
+
+ )
+ ) : null}
+ {agent ? (
+ onSelect ? (
+
{agent.name}
+ ) : (
+
+ {agent.name}
+
+ )
+ ) : null}
+
{relativeTime(record.createdAt)}
+
{record.sensitivityLabel}
+
+ >
+ );
+
+ const className = cn(
+ "grid w-full gap-3 px-4 py-4 text-left",
+ onSelectedChange
+ ? variant === "compact"
+ ? "md:grid-cols-[32px_1fr_190px_140px]"
+ : "md:grid-cols-[32px_1fr_220px_160px]"
+ : variant === "compact"
+ ? "md:grid-cols-[1fr_190px_140px]"
+ : "md:grid-cols-[1fr_220px_160px]",
+ onSelect ? "transition-colors hover:bg-accent/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" : null,
+ );
+
+ if (onSelect) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return {content}
;
+}
diff --git a/ui/src/components/ProjectMemorySettings.test.tsx b/ui/src/components/ProjectMemorySettings.test.tsx
index 6c428dc06fa..a394188c6fb 100644
--- a/ui/src/components/ProjectMemorySettings.test.tsx
+++ b/ui/src/components/ProjectMemorySettings.test.tsx
@@ -1,6 +1,7 @@
// @vitest-environment jsdom
import { act } from "react";
+import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { MemoryBinding, MemoryResolvedBinding } from "@paperclipai/shared";
@@ -10,6 +11,7 @@ import { ProjectMemorySettings } from "./ProjectMemorySettings";
const memoryApiMocks = vi.hoisted(() => ({
listBindings: vi.fn(),
+ listTargets: vi.fn(),
getProjectBinding: vi.fn(),
setProjectBinding: vi.fn(),
}));
@@ -18,8 +20,17 @@ vi.mock("../api/memory", () => ({
memoryApi: memoryApiMocks,
}));
+vi.mock("@/lib/router", () => ({
+ Link: ({ to, children, ...props }: { to: string; children: ReactNode }) => (
+
+ {children}
+
+ ),
+}));
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+Element.prototype.scrollIntoView = vi.fn();
function tick() {
return new Promise((resolve) => window.setTimeout(resolve, 0));
@@ -59,8 +70,10 @@ describe("ProjectMemorySettings", () => {
container = document.createElement("div");
document.body.appendChild(container);
memoryApiMocks.listBindings.mockReset();
+ memoryApiMocks.listTargets.mockReset();
memoryApiMocks.getProjectBinding.mockReset();
memoryApiMocks.setProjectBinding.mockReset();
+ memoryApiMocks.listTargets.mockResolvedValue([]);
});
afterEach(() => {
@@ -71,6 +84,17 @@ describe("ProjectMemorySettings", () => {
it("shows inherited company memory and saves a project override", async () => {
const binding = createBinding();
memoryApiMocks.listBindings.mockResolvedValue([binding]);
+ memoryApiMocks.listTargets.mockResolvedValue([
+ {
+ id: "target-agent",
+ companyId: "company-1",
+ bindingId: binding.id,
+ targetType: "agent",
+ targetId: "agent-1",
+ createdAt: new Date("2026-04-20T00:00:00.000Z"),
+ updatedAt: new Date("2026-04-20T00:00:00.000Z"),
+ },
+ ]);
memoryApiMocks.getProjectBinding.mockResolvedValue(createResolvedBinding());
memoryApiMocks.setProjectBinding.mockResolvedValue({
id: "target-project",
@@ -94,7 +118,7 @@ describe("ProjectMemorySettings", () => {
root.render(
-
+
,
);
@@ -107,15 +131,34 @@ describe("ProjectMemorySettings", () => {
expect(container.textContent).toContain("QMD default (qmd_memory)");
expect(container.textContent).toContain("Source: Company default");
+ expect(container.textContent).toContain("Company default");
+ expect(container.textContent).toContain("Project override");
+ expect(container.textContent).toContain("Agent overrides");
+ expect(container.textContent).toContain("1 agent override configured.");
+ expect(container.querySelector('a[href="/PAP/memories?projectId=project-1"]')?.textContent).toContain(
+ "See memories for this project",
+ );
- const select = container.querySelector("select");
- expect(select).not.toBeNull();
+ const selectTrigger = container.querySelector('[data-slot="select-trigger"]');
+ expect(selectTrigger).not.toBeNull();
await act(async () => {
- select!.value = binding.id;
- select!.dispatchEvent(new Event("change", { bubbles: true }));
+ selectTrigger!.dispatchEvent(
+ new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
+ );
+ await tick();
+ const bindingItem = Array.from(document.body.querySelectorAll('[data-slot="select-item"]')).find((item) =>
+ item.textContent?.includes("QMD default")
+ );
+ expect(bindingItem).toBeTruthy();
+ bindingItem!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
+ bindingItem!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ await tick();
});
+ expect(container.textContent).toContain("New runs will use");
+ expect(container.textContent).toContain("Switching will not affect existing records.");
+
const saveButton = Array.from(container.querySelectorAll("button")).find((button) =>
button.textContent?.includes("Save override")
);
@@ -132,4 +175,87 @@ describe("ProjectMemorySettings", () => {
root.unmount();
});
});
+
+ it("shows a company settings CTA when no bindings exist", async () => {
+ memoryApiMocks.listBindings.mockResolvedValue([]);
+ memoryApiMocks.getProjectBinding.mockResolvedValue(
+ createResolvedBinding({
+ targetType: null,
+ targetId: null,
+ binding: null,
+ source: "unconfigured",
+ }),
+ );
+
+ const root = createRoot(container);
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ await act(async () => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+
+ await act(async () => {
+ await tick();
+ await tick();
+ });
+
+ expect(container.textContent).toContain("Create a memory binding before setting an override.");
+ const companySettingsLinks = Array.from(container.querySelectorAll('a[href="/PAP/company/settings#memory"]'));
+ expect(companySettingsLinks.some((link) => link.textContent?.includes("Create a binding in Company Settings")))
+ .toBe(true);
+ expect(memoryApiMocks.setProjectBinding).not.toHaveBeenCalled();
+
+ await act(async () => {
+ root.unmount();
+ });
+ });
+
+ it("shows an error state with retry", async () => {
+ memoryApiMocks.listBindings.mockRejectedValue(new Error("Bindings unavailable"));
+ memoryApiMocks.getProjectBinding.mockResolvedValue(createResolvedBinding());
+
+ const root = createRoot(container);
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ await act(async () => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+
+ await act(async () => {
+ await tick();
+ await tick();
+ });
+
+ expect(container.textContent).toContain("Bindings unavailable");
+ const retryButton = Array.from(container.querySelectorAll("button")).find((button) =>
+ button.textContent?.includes("Retry")
+ );
+ expect(retryButton).toBeTruthy();
+
+ await act(async () => {
+ root.unmount();
+ });
+ });
});
diff --git a/ui/src/components/ProjectMemorySettings.tsx b/ui/src/components/ProjectMemorySettings.tsx
index 4ec5037f0d0..90abd52214f 100644
--- a/ui/src/components/ProjectMemorySettings.tsx
+++ b/ui/src/components/ProjectMemorySettings.tsx
@@ -1,29 +1,26 @@
-import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { Database } from "lucide-react";
import { memoryApi } from "../api/memory";
+import { MemoryOverrideCard } from "./MemoryOverrideCard";
import { useToast } from "../context/ToastContext";
import { queryKeys } from "../lib/queryKeys";
import { Button } from "@/components/ui/button";
-function sourceLabel(source: string | null | undefined) {
- if (source === "project_override") return "Project override";
- if (source === "company_default") return "Company default";
- if (source === "agent_override") return "Agent override";
- if (source === "binding_key") return "Direct binding key";
- return "Unconfigured";
+function companyPath(companyPrefix: string | null | undefined, path: string) {
+ if (!companyPrefix) return path;
+ return `/${companyPrefix}${path}`;
}
export function ProjectMemorySettings({
companyId,
projectId,
+ companyPrefix,
}: {
companyId: string;
projectId: string;
+ companyPrefix?: string | null;
}) {
const { pushToast } = useToast();
const queryClient = useQueryClient();
- const [selectedBindingId, setSelectedBindingId] = useState("__inherit__");
const bindingsQuery = useQuery({
queryKey: queryKeys.memory.bindings(companyId),
@@ -35,31 +32,18 @@ export function ProjectMemorySettings({
queryFn: () => memoryApi.getProjectBinding(projectId),
});
- useEffect(() => {
- const resolved = resolvedBindingQuery.data;
- if (!resolved) return;
- if (resolved.targetType === "project" && resolved.binding) {
- setSelectedBindingId(resolved.binding.id);
- return;
- }
- setSelectedBindingId("__inherit__");
- }, [resolvedBindingQuery.data]);
-
- const currentSelection = useMemo(() => {
- const resolved = resolvedBindingQuery.data;
- if (!resolved) return "__inherit__";
- if (resolved.targetType === "project" && resolved.binding) return resolved.binding.id;
- return "__inherit__";
- }, [resolvedBindingQuery.data]);
+ const targetsQuery = useQuery({
+ queryKey: queryKeys.memory.targets(companyId),
+ queryFn: () => memoryApi.listTargets(companyId),
+ });
const saveOverride = useMutation({
- mutationFn: () =>
- memoryApi.setProjectBinding(projectId, selectedBindingId === "__inherit__" ? null : selectedBindingId),
- onSuccess: async () => {
+ mutationFn: (bindingId: string | null) => memoryApi.setProjectBinding(projectId, bindingId),
+ onSuccess: async (_target, bindingId) => {
await queryClient.invalidateQueries({ queryKey: queryKeys.memory.all });
pushToast({
title: "Project memory binding updated",
- body: selectedBindingId === "__inherit__"
+ body: bindingId === null
? "This project now inherits the company default binding."
: "The project override is active for subsequent memory operations.",
tone: "success",
@@ -74,71 +58,35 @@ export function ProjectMemorySettings({
},
});
- const loading = bindingsQuery.isLoading || resolvedBindingQuery.isLoading;
- const error = bindingsQuery.error ?? resolvedBindingQuery.error ?? null;
- const resolvedBinding = resolvedBindingQuery.data?.binding ?? null;
+ const loading = bindingsQuery.isLoading || resolvedBindingQuery.isLoading || targetsQuery.isLoading;
+ const error = bindingsQuery.error ?? resolvedBindingQuery.error ?? targetsQuery.error ?? null;
+ const agentOverrideCount = (targetsQuery.data ?? []).filter((target) => target.targetType === "agent").length;
+ const companySettingsHref = companyPath(companyPrefix, "/company/settings#memory");
+ const projectMemoriesHref = companyPath(companyPrefix, `/memories?projectId=${encodeURIComponent(projectId)}`);
return (
-
-
-
-
-
-
-
-
Project memory
- {loading ? (
-
Loading memory state...
- ) : error ? (
-
{error.message}
- ) : (
- <>
-
- {resolvedBinding ? `${resolvedBinding.name ?? resolvedBinding.key} (${resolvedBinding.providerKey})` : "No memory binding resolves for this project yet."}
-
-
- Source: {sourceLabel(resolvedBindingQuery.data?.source ?? null)}
-
- >
- )}
-
-
-
-
- Project override
- setSelectedBindingId(event.target.value)}
- disabled={loading || Boolean(error)}
- className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none"
- >
- Inherit company default
- {(bindingsQuery.data ?? []).map((binding) => (
-
- {binding.name ?? binding.key}
-
- ))}
-
-
-
saveOverride.mutate()}
- >
- {saveOverride.isPending ? "Saving..." : "Save override"}
-
-
-
- {saveOverride.isError ? (
-
- {saveOverride.error instanceof Error ? saveOverride.error.message : "Failed to update project memory binding"}
-
- ) : null}
-
-
-
+
{
+ void bindingsQuery.refetch();
+ void resolvedBindingQuery.refetch();
+ void targetsQuery.refetch();
+ }}
+ onSave={(bindingId) => saveOverride.mutate(bindingId)}
+ />
+
+ See memories for this project
+
);
}
-
diff --git a/ui/src/components/StatusBadge.tsx b/ui/src/components/StatusBadge.tsx
index 2d31e0db1c9..075d37d8a0d 100644
--- a/ui/src/components/StatusBadge.tsx
+++ b/ui/src/components/StatusBadge.tsx
@@ -1,12 +1,21 @@
import { cn } from "../lib/utils";
import { statusBadge, statusBadgeDefault } from "../lib/status-colors";
+const statusBadgeVisualStatus: Record = {
+ pending: "idle",
+ accepted: "approved",
+ rejected: "rejected",
+ revoked: "archived",
+};
+
export function StatusBadge({ status }: { status: string }) {
+ const visualStatus = statusBadgeVisualStatus[status] ?? status;
+
return (
{status.replace("_", " ")}
diff --git a/ui/src/pages/DesignGuide.tsx b/ui/src/pages/DesignGuide.tsx
index 9d4ab867f4d..635f1e200d2 100644
--- a/ui/src/pages/DesignGuide.tsx
+++ b/ui/src/pages/DesignGuide.tsx
@@ -123,6 +123,7 @@ import { FilterBar, type FilterValue } from "@/components/FilterBar";
import { InlineEditor } from "@/components/InlineEditor";
import { PageSkeleton } from "@/components/PageSkeleton";
import { Identity } from "@/components/Identity";
+import { InheritanceChain } from "@/components/InheritanceChain";
/* ------------------------------------------------------------------ */
/* Section wrapper */
@@ -468,6 +469,21 @@ export function DesignGuide() {
+ {/* ============================================================ */}
+ {/* INHERITANCE CHAIN */}
+ {/* ============================================================ */}
+
+
{/* ============================================================ */}
{/* FORM ELEMENTS */}
{/* ============================================================ */}
diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx
index 45ff582080f..61a014d7746 100644
--- a/ui/src/pages/ProjectDetail.tsx
+++ b/ui/src/pages/ProjectDetail.tsx
@@ -700,7 +700,11 @@ export function ProjectDetail() {
)}
{activeTab === "memory" && resolvedCompanyId ? (
-
+
) : null}
{activeTab === "budget" && resolvedCompanyId ? (