(props: MemoryFileEditorProps)
| 24 | * silently overwrites the other writer's changes). |
| 25 | */ |
| 26 | export function MemoryFileEditor(props: MemoryFileEditorProps) { |
| 27 | const { api } = useAPI(); |
| 28 | const [loaded, setLoaded] = useState<{ content: string; sha256: string } | null>(null); |
| 29 | const [draft, setDraft] = useState(""); |
| 30 | const [loadError, setLoadError] = useState<string | null>(null); |
| 31 | const [saveError, setSaveError] = useState<MemorySaveError | null>(null); |
| 32 | const [saving, setSaving] = useState(false); |
| 33 | const [reloadTick, setReloadTick] = useState(0); |
| 34 | // Read the live DOM value at save time (same pattern as |
| 35 | // WorkspaceHeartbeatModal): controlled-textarea onChange does not fire in |
| 36 | // happy-dom, and the DOM is authoritative for what the user typed anyway. |
| 37 | const textareaRef = useRef<HTMLTextAreaElement | null>(null); |
| 38 | |
| 39 | useEffect(() => { |
| 40 | if (!api) return; |
| 41 | const controller = new AbortController(); |
| 42 | api.memory |
| 43 | .read({ workspaceId: props.workspaceId, path: props.path }, { signal: controller.signal }) |
| 44 | .then((result) => { |
| 45 | if (controller.signal.aborted) return; |
| 46 | if (result.success) { |
| 47 | setLoaded(result.data); |
| 48 | setDraft(result.data.content); |
| 49 | setLoadError(null); |
| 50 | } else { |
| 51 | setLoadError(result.error); |
| 52 | } |
| 53 | }) |
| 54 | .catch((err: unknown) => { |
| 55 | if (isAbortError(err) || controller.signal.aborted) return; |
| 56 | setLoadError(getErrorMessage(err)); |
| 57 | }); |
| 58 | return () => controller.abort(); |
| 59 | }, [api, props.workspaceId, props.path, reloadTick]); |
| 60 | |
| 61 | const handleSave = async () => { |
| 62 | if (!api || loaded === null || saving) return; |
| 63 | const content = textareaRef.current?.value ?? draft; |
| 64 | setSaving(true); |
| 65 | try { |
| 66 | const result = await api.memory.save({ |
| 67 | workspaceId: props.workspaceId, |
| 68 | path: props.path, |
| 69 | content, |
| 70 | expectedSha256: loaded.sha256, |
| 71 | }); |
| 72 | if (result.success) { |
| 73 | setLoaded({ content, sha256: result.data.sha256 }); |
| 74 | setDraft(content); |
| 75 | setSaveError(null); |
| 76 | } else { |
| 77 | setSaveError(result.error); |
| 78 | } |
| 79 | } catch (err) { |
| 80 | setSaveError({ kind: "error", message: getErrorMessage(err) }); |
| 81 | } finally { |
| 82 | setSaving(false); |
| 83 | } |
nothing calls this directly
no test coverage detected