The memory pane opened with an untranslated provider description, a
provider picker, a scope toggle and API key fields, and pushed what the
agents actually remember to the bottom. Most people never connect a
semantic-recall provider, so that setup now sits behind a collapsed
Advanced disclosure (open while a provider is connected), and the
remembered documents come first.
- Drop the provider descriptions, the "Shared documents" heading and
the per-document revision label.
- Replace the Isolated/Shared toggle pair with one "Share between bots"
switch.
- Remove the unused standalone dialog mode of the memory pane.
- German: call the section "Gedächtnis" so it no longer reads like
reminders ("Erinnerungen").
Claude-Session: https://claude.ai/code/session_01LWzKXyE6uDmmmzuod4oXNQ
Co-authored-by: Claude <noreply@anthropic.com>
237 lines
7.6 KiB
TypeScript
237 lines
7.6 KiB
TypeScript
import { Trans, useLingui } from "@lingui/react/macro";
|
||
import type { SpaceMemoryConfig } from "@rakazo/contracts";
|
||
import {
|
||
Button,
|
||
Field,
|
||
FieldLabel,
|
||
NativeSelect,
|
||
NativeSelectOption,
|
||
Switch,
|
||
} from "@rakazo/ui-web";
|
||
import { useEffect, useId, useState } from "react";
|
||
import { rpc } from "../lib/rpc";
|
||
import { SpaceMemorySection } from "./KnowledgeSection";
|
||
import {
|
||
defaultMemoryProviderSettings,
|
||
MEMORY_PROVIDER_SETTINGS,
|
||
type MemoryProviderConnectionDraft,
|
||
memoryProviderSettings,
|
||
} from "./memory-providers/registry";
|
||
|
||
type MemoryScope = "isolated" | "shared";
|
||
|
||
function SharedScopeSwitch({
|
||
value,
|
||
disabled,
|
||
onChange,
|
||
}: {
|
||
value: MemoryScope;
|
||
disabled: boolean;
|
||
onChange: (scope: MemoryScope) => void;
|
||
}) {
|
||
const labelId = useId();
|
||
return (
|
||
<div className="flex items-center justify-between gap-4">
|
||
<span id={labelId} className="text-ui-body text-foreground">
|
||
<Trans>Share between bots</Trans>
|
||
</span>
|
||
<Switch
|
||
aria-labelledby={labelId}
|
||
checked={value === "shared"}
|
||
disabled={disabled}
|
||
onCheckedChange={(checked) => onChange(checked ? "shared" : "isolated")}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* What the agents remember first; the optional semantic-recall provider stays
|
||
* behind Advanced because most people never need it.
|
||
*/
|
||
export function MemorySettingsOverlay({
|
||
config,
|
||
onConfigChange,
|
||
onBusyChange,
|
||
}: {
|
||
config: SpaceMemoryConfig | null | undefined;
|
||
onConfigChange: (config: SpaceMemoryConfig | null) => void;
|
||
onBusyChange?: (busy: boolean) => void;
|
||
}) {
|
||
const { t } = useLingui();
|
||
const providerSelectId = useId();
|
||
const defaultRegistration = defaultMemoryProviderSettings();
|
||
const [selectedProvider, setSelectedProvider] = useState(
|
||
config?.provider ?? defaultRegistration.id,
|
||
);
|
||
const [defaultScope, setDefaultScope] = useState<MemoryScope>(
|
||
config?.defaultMemoryScope ?? "isolated",
|
||
);
|
||
const [pending, setPending] = useState<"connect" | "disconnect" | "scope" | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (config) {
|
||
setSelectedProvider(config.provider);
|
||
setDefaultScope(config.defaultMemoryScope);
|
||
return;
|
||
}
|
||
if (config === null && !memoryProviderSettings(selectedProvider)) {
|
||
setSelectedProvider(defaultRegistration.id);
|
||
}
|
||
}, [config, defaultRegistration.id, selectedProvider]);
|
||
|
||
const registration = memoryProviderSettings(config?.provider ?? selectedProvider);
|
||
const busy = pending !== null;
|
||
|
||
useEffect(() => {
|
||
return () => onBusyChange?.(false);
|
||
}, [onBusyChange]);
|
||
|
||
function markPending(next: "connect" | "disconnect" | "scope" | null) {
|
||
setPending(next);
|
||
onBusyChange?.(next !== null);
|
||
}
|
||
|
||
async function connect(draft: MemoryProviderConnectionDraft) {
|
||
if (!registration) return false;
|
||
setError(null);
|
||
markPending("connect");
|
||
try {
|
||
const next = await rpc.memory.connectProvider({
|
||
provider: registration.id,
|
||
...draft,
|
||
defaultMemoryScope: defaultScope,
|
||
});
|
||
onConfigChange(next);
|
||
return true;
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : t`Could not connect ${registration.name}`);
|
||
return false;
|
||
} finally {
|
||
markPending(null);
|
||
}
|
||
}
|
||
|
||
async function disconnect() {
|
||
setError(null);
|
||
markPending("disconnect");
|
||
try {
|
||
await rpc.memory.disconnectProvider();
|
||
onConfigChange(null);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : t`Could not disconnect memory provider`);
|
||
} finally {
|
||
markPending(null);
|
||
}
|
||
}
|
||
|
||
async function updateDefaultScope(scope: MemoryScope) {
|
||
if (scope === defaultScope) return;
|
||
setError(null);
|
||
markPending("scope");
|
||
try {
|
||
const next = await rpc.memory.setDefaultScope({ defaultMemoryScope: scope });
|
||
onConfigChange(next);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : t`Could not update the default memory scope`);
|
||
} finally {
|
||
markPending(null);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div data-testid="memory-settings" className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||
<div className="rk-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pb-6 pt-5 sm:px-8 sm:pb-8">
|
||
<SpaceMemorySection />
|
||
|
||
<details data-testid="memory-advanced" className="group mt-8" open={Boolean(config)}>
|
||
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 text-ui-body text-muted-foreground">
|
||
<span>
|
||
<Trans>Advanced</Trans>
|
||
</span>
|
||
<span aria-hidden="true" className="transition-transform group-open:rotate-90">
|
||
›
|
||
</span>
|
||
</summary>
|
||
|
||
<div className="mt-4">
|
||
{error ? <p className="mb-4 text-sm text-destructive">{error}</p> : null}
|
||
|
||
{config === undefined ? (
|
||
<p className="text-sm text-muted-foreground">
|
||
<Trans>Loading memory settings…</Trans>
|
||
</p>
|
||
) : config ? (
|
||
<div className="rounded-xl border border-border px-4 py-3">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<span className="min-w-0 truncate text-ui-body text-foreground">
|
||
{registration?.connectedLabel(config) ?? config.provider}
|
||
</span>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
disabled={busy}
|
||
onClick={() => void disconnect()}
|
||
>
|
||
{pending === "disconnect" ? (
|
||
<Trans>Disconnecting…</Trans>
|
||
) : (
|
||
<Trans>Disconnect</Trans>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
<div className="mt-3">
|
||
<SharedScopeSwitch
|
||
value={defaultScope}
|
||
disabled={busy}
|
||
onChange={(scope) => void updateDefaultScope(scope)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : registration ? (
|
||
<>
|
||
{MEMORY_PROVIDER_SETTINGS.length > 1 ? (
|
||
<Field className="mb-4">
|
||
<FieldLabel htmlFor={providerSelectId}>
|
||
<Trans>Provider</Trans>
|
||
</FieldLabel>
|
||
<NativeSelect
|
||
id={providerSelectId}
|
||
className="w-full"
|
||
value={selectedProvider}
|
||
disabled={busy}
|
||
onChange={(event) => setSelectedProvider(event.target.value)}
|
||
>
|
||
{MEMORY_PROVIDER_SETTINGS.map((entry) => (
|
||
<NativeSelectOption key={entry.id} value={entry.id}>
|
||
{entry.name}
|
||
</NativeSelectOption>
|
||
))}
|
||
</NativeSelect>
|
||
</Field>
|
||
) : null}
|
||
|
||
<div className="mb-4">
|
||
<SharedScopeSwitch
|
||
value={defaultScope}
|
||
disabled={busy}
|
||
onChange={setDefaultScope}
|
||
/>
|
||
</div>
|
||
|
||
<registration.SettingsForm busy={busy} onConnect={connect} />
|
||
</>
|
||
) : (
|
||
<p className="text-sm text-destructive">
|
||
<Trans>The selected memory provider is not available in this build.</Trans>
|
||
</p>
|
||
)}
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|