diff --git a/ui/src/components/CompanyMemorySettings.tsx b/ui/src/components/CompanyMemorySettings.tsx
index ddf2bdc8b2e..c45cfc6b754 100644
--- a/ui/src/components/CompanyMemorySettings.tsx
+++ b/ui/src/components/CompanyMemorySettings.tsx
@@ -1,19 +1,44 @@
-import { useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type {
+ Agent,
MemoryBinding,
- MemoryListRecordsQuery,
+ MemoryBindingTarget,
MemoryProviderDescriptor,
- MemoryRecord,
+ Project,
} from "@paperclipai/shared";
-import { MEMORY_RETENTION_STATES, MEMORY_SCOPE_TYPES, MEMORY_SENSITIVITY_LABELS } from "@paperclipai/shared";
+import { Bot, FolderKanban, GitBranch, Plus, Star } from "lucide-react";
+import { agentsApi } from "../api/agents";
import { memoryApi } from "../api/memory";
+import { projectsApi } from "../api/projects";
+import { EmptyState } from "../components/EmptyState";
+import { StatusBadge } from "../components/StatusBadge";
import { useToast } from "../context/ToastContext";
import { queryKeys } from "../lib/queryKeys";
+import { Link, useBeforeUnload } from "@/lib/router";
import { getSuggestedMemoryConfig, prettyMemoryConfig, validateMemoryProviderConfig } from "../lib/memory-config-schema";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { MemoryProviderConfigForm } from "./MemoryProviderConfigForm";
const DEFAULT_LOCAL_BASIC_CONFIG = {
@@ -24,21 +49,7 @@ const DEFAULT_LOCAL_BASIC_CONFIG = {
maxHydrateSnippets: 5,
};
-function describeBindingConfig(binding: MemoryBinding) {
- if (binding.providerKey !== "local_basic") {
- const keys = Object.keys(binding.config ?? {});
- return keys.length > 0 ? `${keys.length} config field${keys.length === 1 ? "" : "s"}` : "Default config";
- }
-
- const config = { ...DEFAULT_LOCAL_BASIC_CONFIG, ...binding.config };
- return [
- config.enablePreRunHydrate ? "pre-run hydrate on" : "pre-run hydrate off",
- config.enablePostRunCapture ? "post-run capture on" : "post-run capture off",
- config.enableIssueDocumentCapture ? "issue docs on" : "issue docs off",
- config.enableIssueCommentCapture ? "comments on" : "comments off",
- `top ${String(config.maxHydrateSnippets)} snippets`,
- ].join(" • ");
-}
+const BINDING_KEY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
function providerLabel(provider: MemoryProviderDescriptor | undefined, binding: MemoryBinding) {
return provider?.displayName ?? binding.providerKey;
@@ -48,25 +59,145 @@ function providerDescription(provider: MemoryProviderDescriptor | undefined) {
return provider?.description ?? "Memory provider";
}
-function summarizeRecord(record: MemoryRecord) {
- const text = (record.summary ?? record.content).replace(/\s+/g, " ").trim();
- return text.length > 220 ? `${text.slice(0, 217)}...` : text;
+function shortId(id: string) {
+ return id.slice(0, 8);
}
-function recordScopeLabel(record: MemoryRecord) {
- return `${record.scopeType}${record.scopeId ? `:${record.scopeId.slice(0, 8)}` : ""}`;
+function formatConfigValue(value: unknown) {
+ if (typeof value === "boolean") return value ? "on" : "off";
+ if (value === null || value === undefined || value === "") return "unset";
+ if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`;
+ if (typeof value === "object") return "configured";
+ return String(value);
+}
+
+function bindingConfigSummary(binding: MemoryBinding) {
+ if (binding.providerKey === "local_basic") {
+ const config = { ...DEFAULT_LOCAL_BASIC_CONFIG, ...binding.config };
+ return [
+ { label: "Pre-run", value: formatConfigValue(config.enablePreRunHydrate) },
+ { label: "Post-run", value: formatConfigValue(config.enablePostRunCapture) },
+ { label: "Top N", value: formatConfigValue(config.maxHydrateSnippets) },
+ { label: "Issue docs", value: formatConfigValue(config.enableIssueDocumentCapture) },
+ ];
+ }
+
+ const entries = Object.entries(binding.config ?? {});
+ if (entries.length === 0) return [{ label: "Config", value: "default" }];
+ return entries.slice(0, 4).map(([label, value]) => ({
+ label,
+ value: formatConfigValue(value),
+ }));
+}
+
+function bindingKeyError(key: string) {
+ const trimmed = key.trim();
+ if (!trimmed) return "Binding key is required.";
+ if (!BINDING_KEY_PATTERN.test(trimmed)) {
+ return "Use kebab-case: lowercase letters, numbers, and single hyphens.";
+ }
+ return null;
+}
+
+function ProviderSelect({
+ value,
+ providers,
+ provider,
+ disabled = false,
+ onValueChange,
+}: {
+ value: string;
+ providers: MemoryProviderDescriptor[];
+ provider?: MemoryProviderDescriptor;
+ disabled?: boolean;
+ onValueChange?: (value: string) => void;
+}) {
+ const availableProviders = providers.some((candidate) => candidate.key === value)
+ ? providers
+ : [
+ ...providers,
+ {
+ key: value,
+ displayName: value,
+ description: "Memory provider",
+ kind: "builtin",
+ pluginId: null,
+ capabilities: {
+ browse: false,
+ correction: false,
+ asyncIngestion: false,
+ providerManagedExtraction: false,
+ },
+ configSchema: null,
+ configMetadata: null,
+ } satisfies MemoryProviderDescriptor,
+ ];
+
+ return (
+
+
+
+ {providerDescription(provider)}
+
+
+ );
+}
+
+function BindingRowsSkeleton() {
+ return (
+
+ {Array.from({ length: 3 }, (_, index) => (
+
+ ))}
+
+ );
+}
+
+function ConfigSummary({ binding }: { binding: MemoryBinding }) {
+ return (
+
+ {bindingConfigSummary(binding).map((item) => (
+
+
- {item.label}
+ - {item.value}
+
+ ))}
+
+ );
}
function MemoryBindingCard({
binding,
isDefault,
overrideCount,
+ providers,
provider,
onSetDefault,
}: {
binding: MemoryBinding;
isDefault: boolean;
overrideCount: number;
+ providers: MemoryProviderDescriptor[];
provider?: MemoryProviderDescriptor;
onSetDefault: (bindingId: string) => void;
}) {
@@ -89,6 +220,14 @@ function MemoryBindingCard({
|| enabled !== binding.enabled
|| prettyMemoryConfig(config) !== prettyMemoryConfig(binding.config ?? {});
+ useBeforeUnload(
+ useCallback((event) => {
+ if (!dirty) return;
+ event.preventDefault();
+ event.returnValue = "";
+ }, [dirty]),
+ );
+
const updateBinding = useMutation({
mutationFn: async () => {
const validation = validateMemoryProviderConfig(provider, config);
@@ -119,30 +258,19 @@ function MemoryBindingCard({
return (
-
+
-
{binding.name ?? binding.key}
-
- {providerLabel(provider, binding)}
-
-
- {binding.enabled ? "Enabled" : "Disabled"}
-
+ {binding.name ?? binding.key}
+
{isDefault && (
-
+
+
Company default
-
+
)}
Key: {binding.key}
-
{providerDescription(provider)}
-
{describeBindingConfig(binding)}
+
{overrideCount > 0 ? `${overrideCount} override${overrideCount === 1 ? "" : "s"}` : "No project or agent overrides"}
@@ -153,31 +281,45 @@ function MemoryBindingCard({
disabled={isDefault}
onClick={() => onSetDefault(binding.id)}
>
- {isDefault ? "Default" : "Set as default"}
+ {isDefault ? "Default" : "Make default"}
-
-
+
+
+
+
+ setEnabled(checked === true)}
+ />
+
+
+
-
Provider config
+
Provider config
) : null}
-
+
+ {dirty ?
Unsaved changes will prompt before leaving.
: null}