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:
Fusion
2026-04-28 01:58:49 -07:00
committed by gsxdsm
parent 789afc30b0
commit 0b8ffa1528
7 changed files with 327 additions and 37 deletions

View File

@@ -125,9 +125,9 @@ describe("terminal mobile keyboard layout CSS contract", () => {
return match?.[1] ?? ""; return match?.[1] ?? "";
} }
it("sets width: 100% on mobile", () => { it("sets width: 100vw on mobile", () => {
const ruleBody = findMobileTerminalModalRule(); const ruleBody = findMobileTerminalModalRule();
expect(ruleBody).toContain("width: 100%"); expect(ruleBody).toContain("width: 100vw");
}); });
it("sets height: 100dvh on mobile", () => { it("sets height: 100dvh on mobile", () => {
@@ -140,9 +140,9 @@ describe("terminal mobile keyboard layout CSS contract", () => {
expect(ruleBody).toContain("max-height: 100dvh"); expect(ruleBody).toContain("max-height: 100dvh");
}); });
it("sets min-height: 100dvh on mobile", () => { it("resets min-height constraint on mobile", () => {
const ruleBody = findMobileTerminalModalRule(); const ruleBody = findMobileTerminalModalRule();
expect(ruleBody).toContain("min-height: 100dvh"); expect(ruleBody).toContain("min-height: 0");
}); });
}); });

View File

@@ -165,6 +165,9 @@ export function MemoryView({ projectId, addToast }: MemoryViewProps) {
selectFile, selectFile,
saveSelectedFile, saveSelectedFile,
savingSelectedFile, savingSelectedFile,
reloadMemoryFiles,
triggerDreamNow,
dreamRunning,
} = useMemoryData({ projectId }); } = useMemoryData({ projectId });
useEffect(() => { useEffect(() => {
@@ -299,6 +302,16 @@ export function MemoryView({ projectId, addToast }: MemoryViewProps) {
} }
}, [memoryTestQuery, testRetrieval, addToast]); }, [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 // Handle compact memory
const handleCompactMemory = useCallback(async () => { const handleCompactMemory = useCallback(async () => {
try { try {
@@ -521,24 +534,44 @@ export function MemoryView({ projectId, addToast }: MemoryViewProps) {
</div> </div>
{memorySettingsDraft.memoryEnabled && memorySettingsDraft.memoryDreamsEnabled && ( {memorySettingsDraft.memoryEnabled && memorySettingsDraft.memoryDreamsEnabled && (
<div className="form-group"> <>
<label htmlFor="memoryDreamsSchedule">Dream Schedule</label> <div className="form-group">
<input <label htmlFor="memoryDreamsSchedule">Dream Schedule</label>
id="memoryDreamsSchedule" <input
type="text" id="memoryDreamsSchedule"
className="input" type="text"
value={memorySettingsDraft.memoryDreamsSchedule} className="input"
onChange={(event) => { value={memorySettingsDraft.memoryDreamsSchedule}
setMemorySettingsDraft((prev) => ({ onChange={(event) => {
...prev, setMemorySettingsDraft((prev) => ({
memoryDreamsSchedule: event.target.value, ...prev,
})); memoryDreamsSchedule: event.target.value,
}} }));
placeholder="0 4 * * *" }}
disabled={settingsLoading} placeholder="0 4 * * *"
/> disabled={settingsLoading}
<small>Cron expression for dream processing.</small> />
</div> <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> </div>

View File

@@ -1,8 +1,8 @@
import { useState, useEffect, useCallback, useRef, lazy, Suspense, type MouseEvent } from "react"; 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 { THINKING_LEVELS, isGlobalSettingsKey, isProjectSettingsKey, getErrorMessage } from "@fusion/core";
import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } 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 type { AuthProvider, ModelInfo, BackupListResponse, SettingsExportData, MemoryFileInfo, MemoryRetrievalTestResult, GitRemoteDetailed, RemoteSettings, RemoteStatus, UpdateCheckResponse } from "../api";
import { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus"; import { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus";
import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
@@ -415,6 +415,7 @@ export function SettingsModal({
const [memoryTestQuery, setMemoryTestQuery] = useState(""); const [memoryTestQuery, setMemoryTestQuery] = useState("");
const [memoryTestLoading, setMemoryTestLoading] = useState(false); const [memoryTestLoading, setMemoryTestLoading] = useState(false);
const [memoryTestResult, setMemoryTestResult] = useState<MemoryRetrievalTestResult | null>(null); const [memoryTestResult, setMemoryTestResult] = useState<MemoryRetrievalTestResult | null>(null);
const [dreamRunning, setDreamRunning] = useState(false);
const [memoryCompactLoading, setMemoryCompactLoading] = useState(false); const [memoryCompactLoading, setMemoryCompactLoading] = useState(false);
const [qmdInstallLoading, setQmdInstallLoading] = useState(false); const [qmdInstallLoading, setQmdInstallLoading] = useState(false);
const skipNextMemoryReloadRef = useRef(false); const skipNextMemoryReloadRef = useRef(false);
@@ -1343,6 +1344,23 @@ export function SettingsModal({
} }
}, [memoryTestQuery, projectId, addToast]); }, [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 () => { const handleInstallQmd = useCallback(async () => {
setQmdInstallLoading(true); setQmdInstallLoading(true);
try { try {
@@ -3080,18 +3098,38 @@ export function SettingsModal({
</div> </div>
{isMemoryEnabled && form.memoryDreamsEnabled === true && ( {isMemoryEnabled && form.memoryDreamsEnabled === true && (
<div className="form-group"> <>
<label htmlFor="memoryDreamsSchedule">Dream Schedule</label> <div className="form-group">
<input <label htmlFor="memoryDreamsSchedule">Dream Schedule</label>
id="memoryDreamsSchedule" <input
type="text" id="memoryDreamsSchedule"
value={form.memoryDreamsSchedule ?? "0 4 * * *"} type="text"
onChange={(e) => value={form.memoryDreamsSchedule ?? "0 4 * * *"}
setForm((f) => ({ ...f, memoryDreamsSchedule: e.target.value })) onChange={(e) =>
} setForm((f) => ({ ...f, memoryDreamsSchedule: e.target.value }))
/> }
<small>Cron expression for dream processing.</small> />
</div> <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"> <div className="memory-retrieval-test">

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; 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 userEvent from "@testing-library/user-event";
import { MemoryView } from "../MemoryView"; import { MemoryView } from "../MemoryView";
@@ -83,6 +83,9 @@ function createMemoryData(overrides: Record<string, unknown> = {}) {
selectFile: vi.fn(), selectFile: vi.fn(),
saveSelectedFile: vi.fn(), saveSelectedFile: vi.fn(),
savingSelectedFile: false, savingSelectedFile: false,
reloadMemoryFiles: vi.fn(),
triggerDreamNow: vi.fn(),
dreamRunning: false,
...overrides, ...overrides,
}; };
} }
@@ -179,4 +182,59 @@ describe("MemoryView", () => {
expect(screen.getByText("Installed")).toBeInTheDocument(); expect(screen.getByText("Installed")).toBeInTheDocument();
expect(screen.getByText("qmd is available on PATH.")).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();
});
}); });

View File

@@ -40,6 +40,8 @@ const mockRegenerateRemotePersistentToken = vi.fn();
const mockGenerateShortLivedRemoteToken = vi.fn(); const mockGenerateShortLivedRemoteToken = vi.fn();
const mockFetchRemoteQr = vi.fn(); const mockFetchRemoteQr = vi.fn();
const mockFetchRemoteUrl = vi.fn(); const mockFetchRemoteUrl = vi.fn();
const mockFetchAutomations = vi.fn();
const mockRunAutomation = vi.fn();
const mockUseWorkspaceFileBrowser = vi.fn(); const mockUseWorkspaceFileBrowser = vi.fn();
vi.mock("../../api", () => ({ vi.mock("../../api", () => ({
@@ -78,6 +80,8 @@ vi.mock("../../api", () => ({
generateShortLivedRemoteToken: (...args: unknown[]) => mockGenerateShortLivedRemoteToken(...args), generateShortLivedRemoteToken: (...args: unknown[]) => mockGenerateShortLivedRemoteToken(...args),
fetchRemoteQr: (...args: unknown[]) => mockFetchRemoteQr(...args), fetchRemoteQr: (...args: unknown[]) => mockFetchRemoteQr(...args),
fetchRemoteUrl: (...args: unknown[]) => mockFetchRemoteUrl(...args), fetchRemoteUrl: (...args: unknown[]) => mockFetchRemoteUrl(...args),
fetchAutomations: (...args: unknown[]) => mockFetchAutomations(...args),
runAutomation: (...args: unknown[]) => mockRunAutomation(...args),
})); }));
// Mock the hook // Mock the hook
@@ -95,6 +99,7 @@ vi.mock("lucide-react", async (importOriginal) => {
RefreshCw: ({ className }: { className?: string }) => <span data-testid="icon-refresh" className={className} />, 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 }} />, 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 }} />, 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 }); 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>" }); 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 }); 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({ mockUseWorkspaceFileBrowser.mockReturnValue({
entries: [], entries: [],
currentPath: ".", currentPath: ".",
@@ -1716,4 +1723,56 @@ describe("SettingsModal", () => {
expect(screen.getByText("https://remote.example.com/qr-text")).toBeInTheDocument(); 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();
});
});
}); });

View File

@@ -13,6 +13,15 @@ vi.mock("../../api", () => ({
fetchMemoryStats: vi.fn(), fetchMemoryStats: vi.fn(),
compactMemory: vi.fn(), compactMemory: vi.fn(),
fetchMemoryBackendStatus: 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 // Mock useMemoryBackendStatus hook
@@ -42,6 +51,8 @@ import {
triggerInsightExtraction, triggerInsightExtraction,
fetchMemoryAudit, fetchMemoryAudit,
compactMemory, compactMemory,
fetchAutomations,
runAutomation,
} from "../../api"; } from "../../api";
describe("useMemoryData", () => { describe("useMemoryData", () => {
@@ -273,6 +284,69 @@ describe("useMemoryData", () => {
expect(fetchMemoryInsights).toHaveBeenCalledTimes(2); // Initial + refresh 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 () => { it("sets correct loading states during async operations", async () => {
vi.mocked(fetchMemory).mockResolvedValue({ content: "Initial content" }); vi.mocked(fetchMemory).mockResolvedValue({ content: "Initial content" });
vi.mocked(fetchMemoryInsights).mockResolvedValue({ content: null, exists: false }); vi.mocked(fetchMemoryInsights).mockResolvedValue({ content: null, exists: false });

View File

@@ -13,6 +13,8 @@ import {
fetchMemoryFiles, fetchMemoryFiles,
fetchMemoryFile, fetchMemoryFile,
saveMemoryFile, saveMemoryFile,
fetchAutomations,
runAutomation,
installQmd, installQmd,
testMemoryRetrieval, testMemoryRetrieval,
type MemoryAuditReport, type MemoryAuditReport,
@@ -85,6 +87,10 @@ interface UseMemoryDataResult {
extractInsights: () => Promise<{ success: boolean; summary: string }>; extractInsights: () => Promise<{ success: boolean; summary: string }>;
extracting: boolean; extracting: boolean;
// Dreams
triggerDreamNow: () => Promise<unknown>;
dreamRunning: boolean;
// Audit // Audit
auditReport: MemoryAuditReport | null; auditReport: MemoryAuditReport | null;
auditLoading: boolean; auditLoading: boolean;
@@ -162,6 +168,9 @@ export function useMemoryData(options: UseMemoryDataOptions = {}): UseMemoryData
// Extraction state // Extraction state
const [extracting, setExtracting] = useState(false); const [extracting, setExtracting] = useState(false);
// Dreams state
const [dreamRunning, setDreamRunning] = useState(false);
// Audit state // Audit state
const [auditReport, setAuditReport] = useState<MemoryAuditReport | null>(null); const [auditReport, setAuditReport] = useState<MemoryAuditReport | null>(null);
const [auditLoading, setAuditLoading] = useState(true); const [auditLoading, setAuditLoading] = useState(true);
@@ -514,6 +523,21 @@ export function useMemoryData(options: UseMemoryDataOptions = {}): UseMemoryData
} }
}, [projectId, refreshInsights, refreshAudit]); }, [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 // Compact memory
const compactMemoryAction = useCallback(async (path?: string) => { const compactMemoryAction = useCallback(async (path?: string) => {
setCompacting(true); setCompacting(true);
@@ -582,6 +606,10 @@ export function useMemoryData(options: UseMemoryDataOptions = {}): UseMemoryData
extractInsights, extractInsights,
extracting, extracting,
// Dreams
triggerDreamNow,
dreamRunning,
// Audit // Audit
auditReport, auditReport,
auditLoading, auditLoading,