feat(FN-2732): add manual dream processing actions in dashboard
- Extend useMemoryData with triggerDreamNow and dreamRunning state by locating and invoking the Memory Dreams automation - Add Dream Now actions to MemoryView and SettingsModal memory settings with loading UI, success/error toasts, and file refresh in MemoryView - Expand hook and component tests to cover dream-trigger visibility and execution flows in both views - Update terminal mobile keyboard layout CSS contract expectations for the current mobile modal width and min-height rules
This commit is contained in:
@@ -125,9 +125,9 @@ describe("terminal mobile keyboard layout CSS contract", () => {
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
it("sets width: 100% on mobile", () => {
|
||||
it("sets width: 100vw on mobile", () => {
|
||||
const ruleBody = findMobileTerminalModalRule();
|
||||
expect(ruleBody).toContain("width: 100%");
|
||||
expect(ruleBody).toContain("width: 100vw");
|
||||
});
|
||||
|
||||
it("sets height: 100dvh on mobile", () => {
|
||||
@@ -140,9 +140,9 @@ describe("terminal mobile keyboard layout CSS contract", () => {
|
||||
expect(ruleBody).toContain("max-height: 100dvh");
|
||||
});
|
||||
|
||||
it("sets min-height: 100dvh on mobile", () => {
|
||||
it("resets min-height constraint on mobile", () => {
|
||||
const ruleBody = findMobileTerminalModalRule();
|
||||
expect(ruleBody).toContain("min-height: 100dvh");
|
||||
expect(ruleBody).toContain("min-height: 0");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -165,6 +165,9 @@ export function MemoryView({ projectId, addToast }: MemoryViewProps) {
|
||||
selectFile,
|
||||
saveSelectedFile,
|
||||
savingSelectedFile,
|
||||
reloadMemoryFiles,
|
||||
triggerDreamNow,
|
||||
dreamRunning,
|
||||
} = useMemoryData({ projectId });
|
||||
|
||||
useEffect(() => {
|
||||
@@ -299,6 +302,16 @@ export function MemoryView({ projectId, addToast }: MemoryViewProps) {
|
||||
}
|
||||
}, [memoryTestQuery, testRetrieval, addToast]);
|
||||
|
||||
const handleDreamNow = useCallback(async () => {
|
||||
try {
|
||||
await triggerDreamNow();
|
||||
addToast("Dream processing completed", "success");
|
||||
await reloadMemoryFiles();
|
||||
} catch (error) {
|
||||
addToast(error instanceof Error ? error.message : "Failed to run dream processing", "error");
|
||||
}
|
||||
}, [triggerDreamNow, reloadMemoryFiles, addToast]);
|
||||
|
||||
// Handle compact memory
|
||||
const handleCompactMemory = useCallback(async () => {
|
||||
try {
|
||||
@@ -521,24 +534,44 @@ export function MemoryView({ projectId, addToast }: MemoryViewProps) {
|
||||
</div>
|
||||
|
||||
{memorySettingsDraft.memoryEnabled && memorySettingsDraft.memoryDreamsEnabled && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryDreamsSchedule">Dream Schedule</label>
|
||||
<input
|
||||
id="memoryDreamsSchedule"
|
||||
type="text"
|
||||
className="input"
|
||||
value={memorySettingsDraft.memoryDreamsSchedule}
|
||||
onChange={(event) => {
|
||||
setMemorySettingsDraft((prev) => ({
|
||||
...prev,
|
||||
memoryDreamsSchedule: event.target.value,
|
||||
}));
|
||||
}}
|
||||
placeholder="0 4 * * *"
|
||||
disabled={settingsLoading}
|
||||
/>
|
||||
<small>Cron expression for dream processing.</small>
|
||||
</div>
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryDreamsSchedule">Dream Schedule</label>
|
||||
<input
|
||||
id="memoryDreamsSchedule"
|
||||
type="text"
|
||||
className="input"
|
||||
value={memorySettingsDraft.memoryDreamsSchedule}
|
||||
onChange={(event) => {
|
||||
setMemorySettingsDraft((prev) => ({
|
||||
...prev,
|
||||
memoryDreamsSchedule: event.target.value,
|
||||
}));
|
||||
}}
|
||||
placeholder="0 4 * * *"
|
||||
disabled={settingsLoading}
|
||||
/>
|
||||
<small>Cron expression for dream processing.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={handleDreamNow}
|
||||
disabled={dreamRunning || !memorySettingsDraft.memoryDreamsEnabled}
|
||||
>
|
||||
{dreamRunning ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
Dreaming…
|
||||
</>
|
||||
) : (
|
||||
"Dream Now"
|
||||
)}
|
||||
</button>
|
||||
<small>Manually trigger dream processing now.</small>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect, useCallback, useRef, lazy, Suspense, type MouseEvent } from "react";
|
||||
import { Globe, Folder, RefreshCw, Star, HelpCircle } from "lucide-react";
|
||||
import { Globe, Folder, RefreshCw, Star, HelpCircle, Loader2 } from "lucide-react";
|
||||
import { THINKING_LEVELS, isGlobalSettingsKey, isProjectSettingsKey, getErrorMessage } from "@fusion/core";
|
||||
import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } from "@fusion/core";
|
||||
import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, saveApiKey, clearApiKey, fetchModels, testNtfyNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, fetchGitRemotesDetailed, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, activateRemoteProvider, startRemoteTunnel, stopRemoteTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl } from "../api";
|
||||
import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, saveApiKey, clearApiKey, fetchModels, testNtfyNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, fetchAutomations, runAutomation, fetchGitRemotesDetailed, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, activateRemoteProvider, startRemoteTunnel, stopRemoteTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl } from "../api";
|
||||
import type { AuthProvider, ModelInfo, BackupListResponse, SettingsExportData, MemoryFileInfo, MemoryRetrievalTestResult, GitRemoteDetailed, RemoteSettings, RemoteStatus, UpdateCheckResponse } from "../api";
|
||||
import { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
@@ -415,6 +415,7 @@ export function SettingsModal({
|
||||
const [memoryTestQuery, setMemoryTestQuery] = useState("");
|
||||
const [memoryTestLoading, setMemoryTestLoading] = useState(false);
|
||||
const [memoryTestResult, setMemoryTestResult] = useState<MemoryRetrievalTestResult | null>(null);
|
||||
const [dreamRunning, setDreamRunning] = useState(false);
|
||||
const [memoryCompactLoading, setMemoryCompactLoading] = useState(false);
|
||||
const [qmdInstallLoading, setQmdInstallLoading] = useState(false);
|
||||
const skipNextMemoryReloadRef = useRef(false);
|
||||
@@ -1343,6 +1344,23 @@ export function SettingsModal({
|
||||
}
|
||||
}, [memoryTestQuery, projectId, addToast]);
|
||||
|
||||
const handleDreamNow = useCallback(async () => {
|
||||
setDreamRunning(true);
|
||||
try {
|
||||
const automations = await fetchAutomations({ scope: "project", projectId });
|
||||
const memoryDreamsSchedule = automations.find((automation) => automation.name === "Memory Dreams");
|
||||
if (!memoryDreamsSchedule) {
|
||||
throw new Error("Memory Dreams schedule not found. Enable dream processing in memory settings first.");
|
||||
}
|
||||
await runAutomation(memoryDreamsSchedule.id, { scope: "project", projectId });
|
||||
addToast("Dream processing completed", "success");
|
||||
} catch (error) {
|
||||
addToast(error instanceof Error ? error.message : "Failed to run dream processing", "error");
|
||||
} finally {
|
||||
setDreamRunning(false);
|
||||
}
|
||||
}, [projectId, addToast]);
|
||||
|
||||
const handleInstallQmd = useCallback(async () => {
|
||||
setQmdInstallLoading(true);
|
||||
try {
|
||||
@@ -3080,18 +3098,38 @@ export function SettingsModal({
|
||||
</div>
|
||||
|
||||
{isMemoryEnabled && form.memoryDreamsEnabled === true && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryDreamsSchedule">Dream Schedule</label>
|
||||
<input
|
||||
id="memoryDreamsSchedule"
|
||||
type="text"
|
||||
value={form.memoryDreamsSchedule ?? "0 4 * * *"}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, memoryDreamsSchedule: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<small>Cron expression for dream processing.</small>
|
||||
</div>
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryDreamsSchedule">Dream Schedule</label>
|
||||
<input
|
||||
id="memoryDreamsSchedule"
|
||||
type="text"
|
||||
value={form.memoryDreamsSchedule ?? "0 4 * * *"}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, memoryDreamsSchedule: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<small>Cron expression for dream processing.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={handleDreamNow}
|
||||
disabled={dreamRunning || form.memoryDreamsEnabled !== true}
|
||||
>
|
||||
{dreamRunning ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
Dreaming…
|
||||
</>
|
||||
) : (
|
||||
"Dream Now"
|
||||
)}
|
||||
</button>
|
||||
<small>Manually trigger dream processing now.</small>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="memory-retrieval-test">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryView } from "../MemoryView";
|
||||
|
||||
@@ -83,6 +83,9 @@ function createMemoryData(overrides: Record<string, unknown> = {}) {
|
||||
selectFile: vi.fn(),
|
||||
saveSelectedFile: vi.fn(),
|
||||
savingSelectedFile: false,
|
||||
reloadMemoryFiles: vi.fn(),
|
||||
triggerDreamNow: vi.fn(),
|
||||
dreamRunning: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -179,4 +182,59 @@ describe("MemoryView", () => {
|
||||
expect(screen.getByText("Installed")).toBeInTheDocument();
|
||||
expect(screen.getByText("qmd is available on PATH.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Dream Now button when dreams are enabled", () => {
|
||||
mockUseMemoryData.mockReturnValue(
|
||||
createMemoryData({
|
||||
memorySettings: {
|
||||
memoryEnabled: true,
|
||||
memoryAutoSummarizeEnabled: false,
|
||||
memoryAutoSummarizeThresholdChars: 50000,
|
||||
memoryAutoSummarizeSchedule: "0 3 * * *",
|
||||
memoryDreamsEnabled: true,
|
||||
memoryDreamsSchedule: "0 4 * * *",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
render(<MemoryView addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Dream Now" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("triggers dream processing and refreshes memory files", async () => {
|
||||
const triggerDreamNow = vi.fn().mockResolvedValue({});
|
||||
const reloadMemoryFiles = vi.fn().mockResolvedValue(undefined);
|
||||
const addToast = vi.fn();
|
||||
mockUseMemoryData.mockReturnValue(
|
||||
createMemoryData({
|
||||
triggerDreamNow,
|
||||
reloadMemoryFiles,
|
||||
memorySettings: {
|
||||
memoryEnabled: true,
|
||||
memoryAutoSummarizeEnabled: false,
|
||||
memoryAutoSummarizeThresholdChars: 50000,
|
||||
memoryAutoSummarizeSchedule: "0 3 * * *",
|
||||
memoryDreamsEnabled: true,
|
||||
memoryDreamsSchedule: "0 4 * * *",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
render(<MemoryView addToast={addToast} />);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Dream Now" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(triggerDreamNow).toHaveBeenCalledTimes(1);
|
||||
expect(reloadMemoryFiles).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(addToast).toHaveBeenCalledWith("Dream processing completed", "success");
|
||||
});
|
||||
|
||||
it("hides Dream Now button when dreams are disabled", () => {
|
||||
render(<MemoryView addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Dream Now" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,8 @@ const mockRegenerateRemotePersistentToken = vi.fn();
|
||||
const mockGenerateShortLivedRemoteToken = vi.fn();
|
||||
const mockFetchRemoteQr = vi.fn();
|
||||
const mockFetchRemoteUrl = vi.fn();
|
||||
const mockFetchAutomations = vi.fn();
|
||||
const mockRunAutomation = vi.fn();
|
||||
const mockUseWorkspaceFileBrowser = vi.fn();
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
@@ -78,6 +80,8 @@ vi.mock("../../api", () => ({
|
||||
generateShortLivedRemoteToken: (...args: unknown[]) => mockGenerateShortLivedRemoteToken(...args),
|
||||
fetchRemoteQr: (...args: unknown[]) => mockFetchRemoteQr(...args),
|
||||
fetchRemoteUrl: (...args: unknown[]) => mockFetchRemoteUrl(...args),
|
||||
fetchAutomations: (...args: unknown[]) => mockFetchAutomations(...args),
|
||||
runAutomation: (...args: unknown[]) => mockRunAutomation(...args),
|
||||
}));
|
||||
|
||||
// Mock the hook
|
||||
@@ -95,6 +99,7 @@ vi.mock("lucide-react", async (importOriginal) => {
|
||||
RefreshCw: ({ className }: { className?: string }) => <span data-testid="icon-refresh" className={className} />,
|
||||
Star: ({ size }: { size?: number }) => <span data-testid="icon-star" style={{ width: size, height: size }} />,
|
||||
HelpCircle: ({ size }: { size?: number }) => <span data-testid="icon-help-circle" style={{ width: size, height: size }} />,
|
||||
Loader2: ({ className }: { className?: string }) => <span data-testid="icon-loader2" className={className} />,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -263,6 +268,8 @@ describe("SettingsModal", () => {
|
||||
mockGenerateShortLivedRemoteToken.mockResolvedValue({ token: "short", expiresAt: new Date(Date.now() + 60000).toISOString(), ttlMs: 60000 });
|
||||
mockFetchRemoteQr.mockResolvedValue({ url: "https://remote.example.com", tokenType: "persistent", expiresAt: null, format: "image/svg", data: "<svg></svg>" });
|
||||
mockFetchRemoteUrl.mockResolvedValue({ url: "https://remote.example.com", tokenType: "persistent", expiresAt: null });
|
||||
mockFetchAutomations.mockResolvedValue([]);
|
||||
mockRunAutomation.mockResolvedValue({ schedule: { id: "auto-2", name: "Memory Dreams" }, result: { success: true } });
|
||||
mockUseWorkspaceFileBrowser.mockReturnValue({
|
||||
entries: [],
|
||||
currentPath: ".",
|
||||
@@ -1716,4 +1723,56 @@ describe("SettingsModal", () => {
|
||||
expect(screen.getByText("https://remote.example.com/qr-text")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("memory dream trigger", () => {
|
||||
const openMemorySection = async () => {
|
||||
const [memorySectionButton] = await screen.findAllByRole("button", { name: /^Memory$/i });
|
||||
await userEvent.click(memorySectionButton);
|
||||
};
|
||||
|
||||
it("shows Dream Now button when dreams are enabled", async () => {
|
||||
mockFetchSettings.mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
memoryEnabled: true,
|
||||
memoryDreamsEnabled: true,
|
||||
memoryDreamsSchedule: "0 4 * * *",
|
||||
});
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
await openMemorySection();
|
||||
|
||||
expect(await screen.findByRole("button", { name: "Dream Now" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("triggers dream processing from Dream Now button", async () => {
|
||||
const addToast = vi.fn();
|
||||
mockFetchSettings.mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
memoryEnabled: true,
|
||||
memoryDreamsEnabled: true,
|
||||
});
|
||||
mockFetchAutomations.mockResolvedValueOnce([{ id: "auto-2", name: "Memory Dreams" }]);
|
||||
|
||||
renderModal({ addToast });
|
||||
await waitForSettingsModalReady();
|
||||
await openMemorySection();
|
||||
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Dream Now" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAutomations).toHaveBeenCalledWith({ scope: "project", projectId: undefined });
|
||||
expect(mockRunAutomation).toHaveBeenCalledWith("auto-2", { scope: "project", projectId: undefined });
|
||||
});
|
||||
expect(addToast).toHaveBeenCalledWith("Dream processing completed", "success");
|
||||
});
|
||||
|
||||
it("hides Dream Now button when dreams are disabled", async () => {
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
await openMemorySection();
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Dream Now" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,15 @@ vi.mock("../../api", () => ({
|
||||
fetchMemoryStats: vi.fn(),
|
||||
compactMemory: vi.fn(),
|
||||
fetchMemoryBackendStatus: vi.fn(),
|
||||
fetchSettings: vi.fn(),
|
||||
updateSettings: vi.fn(),
|
||||
fetchMemoryFiles: vi.fn(),
|
||||
fetchMemoryFile: vi.fn(),
|
||||
saveMemoryFile: vi.fn(),
|
||||
installQmd: vi.fn(),
|
||||
testMemoryRetrieval: vi.fn(),
|
||||
fetchAutomations: vi.fn(),
|
||||
runAutomation: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock useMemoryBackendStatus hook
|
||||
@@ -42,6 +51,8 @@ import {
|
||||
triggerInsightExtraction,
|
||||
fetchMemoryAudit,
|
||||
compactMemory,
|
||||
fetchAutomations,
|
||||
runAutomation,
|
||||
} from "../../api";
|
||||
|
||||
describe("useMemoryData", () => {
|
||||
@@ -273,6 +284,69 @@ describe("useMemoryData", () => {
|
||||
expect(fetchMemoryInsights).toHaveBeenCalledTimes(2); // Initial + refresh
|
||||
});
|
||||
|
||||
it("triggerDreamNow finds and runs the Memory Dreams automation", async () => {
|
||||
vi.mocked(fetchMemory).mockResolvedValue({ content: "Initial content" });
|
||||
vi.mocked(fetchMemoryInsights).mockResolvedValue({ content: null, exists: false });
|
||||
vi.mocked(fetchMemoryAudit).mockResolvedValue({
|
||||
generatedAt: "2024-01-01T00:00:00.000Z",
|
||||
workingMemory: { exists: true, size: 20, sectionCount: 1 },
|
||||
insightsMemory: { exists: false, size: 0, insightCount: 0, categories: {} },
|
||||
extraction: { runAt: "", success: false, insightCount: 0, duplicateCount: 0, skippedCount: 0, summary: "" },
|
||||
pruning: { applied: false, reason: "", sizeDelta: 0, originalSize: 0, newSize: 0 },
|
||||
checks: [],
|
||||
health: "warning",
|
||||
});
|
||||
vi.mocked(fetchAutomations).mockResolvedValue([
|
||||
{ id: "auto-1", name: "Other", cron: "* * * * *", enabled: true },
|
||||
{ id: "auto-2", name: "Memory Dreams", cron: "0 4 * * *", enabled: true },
|
||||
] as any);
|
||||
vi.mocked(runAutomation).mockResolvedValue({ schedule: { id: "auto-2" }, result: { success: true } } as any);
|
||||
|
||||
const { result } = renderHook(() => useMemoryData({ projectId: "test-project" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.workingMemoryLoading).toBe(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.triggerDreamNow();
|
||||
});
|
||||
|
||||
expect(fetchAutomations).toHaveBeenCalledWith({ scope: "project", projectId: "test-project" });
|
||||
expect(runAutomation).toHaveBeenCalledWith("auto-2", { scope: "project", projectId: "test-project" });
|
||||
expect(result.current.dreamRunning).toBe(false);
|
||||
});
|
||||
|
||||
it("triggerDreamNow throws when Memory Dreams schedule is missing and resets state", async () => {
|
||||
vi.mocked(fetchMemory).mockResolvedValue({ content: "Initial content" });
|
||||
vi.mocked(fetchMemoryInsights).mockResolvedValue({ content: null, exists: false });
|
||||
vi.mocked(fetchMemoryAudit).mockResolvedValue({
|
||||
generatedAt: "2024-01-01T00:00:00.000Z",
|
||||
workingMemory: { exists: true, size: 20, sectionCount: 1 },
|
||||
insightsMemory: { exists: false, size: 0, insightCount: 0, categories: {} },
|
||||
extraction: { runAt: "", success: false, insightCount: 0, duplicateCount: 0, skippedCount: 0, summary: "" },
|
||||
pruning: { applied: false, reason: "", sizeDelta: 0, originalSize: 0, newSize: 0 },
|
||||
checks: [],
|
||||
health: "warning",
|
||||
});
|
||||
vi.mocked(fetchAutomations).mockResolvedValue([{ id: "auto-1", name: "Other" }] as any);
|
||||
|
||||
const { result } = renderHook(() => useMemoryData({ projectId: "test-project" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.workingMemoryLoading).toBe(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.triggerDreamNow()).rejects.toThrow(
|
||||
"Memory Dreams schedule not found. Enable dream processing in memory settings first.",
|
||||
);
|
||||
});
|
||||
|
||||
expect(runAutomation).not.toHaveBeenCalled();
|
||||
expect(result.current.dreamRunning).toBe(false);
|
||||
});
|
||||
|
||||
it("sets correct loading states during async operations", async () => {
|
||||
vi.mocked(fetchMemory).mockResolvedValue({ content: "Initial content" });
|
||||
vi.mocked(fetchMemoryInsights).mockResolvedValue({ content: null, exists: false });
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
fetchMemoryFiles,
|
||||
fetchMemoryFile,
|
||||
saveMemoryFile,
|
||||
fetchAutomations,
|
||||
runAutomation,
|
||||
installQmd,
|
||||
testMemoryRetrieval,
|
||||
type MemoryAuditReport,
|
||||
@@ -85,6 +87,10 @@ interface UseMemoryDataResult {
|
||||
extractInsights: () => Promise<{ success: boolean; summary: string }>;
|
||||
extracting: boolean;
|
||||
|
||||
// Dreams
|
||||
triggerDreamNow: () => Promise<unknown>;
|
||||
dreamRunning: boolean;
|
||||
|
||||
// Audit
|
||||
auditReport: MemoryAuditReport | null;
|
||||
auditLoading: boolean;
|
||||
@@ -162,6 +168,9 @@ export function useMemoryData(options: UseMemoryDataOptions = {}): UseMemoryData
|
||||
// Extraction state
|
||||
const [extracting, setExtracting] = useState(false);
|
||||
|
||||
// Dreams state
|
||||
const [dreamRunning, setDreamRunning] = useState(false);
|
||||
|
||||
// Audit state
|
||||
const [auditReport, setAuditReport] = useState<MemoryAuditReport | null>(null);
|
||||
const [auditLoading, setAuditLoading] = useState(true);
|
||||
@@ -514,6 +523,21 @@ export function useMemoryData(options: UseMemoryDataOptions = {}): UseMemoryData
|
||||
}
|
||||
}, [projectId, refreshInsights, refreshAudit]);
|
||||
|
||||
const triggerDreamNow = useCallback(async () => {
|
||||
setDreamRunning(true);
|
||||
try {
|
||||
const automations = await fetchAutomations({ scope: "project", projectId });
|
||||
const memoryDreamsSchedule = automations.find((automation) => automation.name === "Memory Dreams");
|
||||
if (!memoryDreamsSchedule) {
|
||||
throw new Error("Memory Dreams schedule not found. Enable dream processing in memory settings first.");
|
||||
}
|
||||
|
||||
return await runAutomation(memoryDreamsSchedule.id, { scope: "project", projectId });
|
||||
} finally {
|
||||
setDreamRunning(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
// Compact memory
|
||||
const compactMemoryAction = useCallback(async (path?: string) => {
|
||||
setCompacting(true);
|
||||
@@ -582,6 +606,10 @@ export function useMemoryData(options: UseMemoryDataOptions = {}): UseMemoryData
|
||||
extractInsights,
|
||||
extracting,
|
||||
|
||||
// Dreams
|
||||
triggerDreamNow,
|
||||
dreamRunning,
|
||||
|
||||
// Audit
|
||||
auditReport,
|
||||
auditLoading,
|
||||
|
||||
Reference in New Issue
Block a user