diff --git a/canvas/src/app/__tests__/orgs-page.test.tsx b/canvas/src/app/__tests__/orgs-page.test.tsx index db0adeb7..fb0a1f75 100644 --- a/canvas/src/app/__tests__/orgs-page.test.tsx +++ b/canvas/src/app/__tests__/orgs-page.test.tsx @@ -85,6 +85,11 @@ function setLocation(href: string) { } beforeEach(() => { + // Always reset to real timers first. If a previous polling test failed + // before its finally-block ran, fake timers would still be active and + // vi.useFakeTimers() in the polling tests would be a no-op — causing + // setTimeout(0) to hang and the test to time out. + vi.useRealTimers(); vi.clearAllMocks(); // Reset mock return values so each test starts fresh. // The mock functions (vi.fn) persist across tests; only their @@ -95,10 +100,9 @@ beforeEach(() => { }); afterEach(() => { - cleanup(); -}); - -afterEach(() => { + // Ensure fake timers are never left active after a test — even one that + // failed before reaching its own finally-block. + vi.useRealTimers(); cleanup(); }); diff --git a/canvas/src/components/ConsoleModal.tsx b/canvas/src/components/ConsoleModal.tsx index 55265837..1a43ca10 100644 --- a/canvas/src/components/ConsoleModal.tsx +++ b/canvas/src/components/ConsoleModal.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { api } from "@/lib/api"; +import { showToast } from "@/components/Toaster"; interface Props { workspaceId: string; @@ -133,7 +134,13 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
{output && ( + ))} +
+ + + {/* Search bar + namespace filter */} +
{/* Magnifying glass icon */} setSearchQuery(e.target.value)} placeholder="Semantic search…" - aria-label="Search memory entries" + aria-label="Search memories" className="w-full bg-zinc-900 border border-zinc-700/60 focus:border-blue-500/60 rounded-lg pl-8 pr-7 py-1.5 text-[11px] text-zinc-200 placeholder-zinc-600 focus:outline-none transition-colors" /> - {/* Clear button — only shown when there is a query */} {searchQuery && ( )}
+ + {/* Namespace filter */} +
+ + setActiveNamespace(e.target.value)} + placeholder="all namespaces" + aria-label="Filter by namespace" + className="flex-1 bg-zinc-900 border border-zinc-700/60 focus:border-blue-500/60 rounded px-2 py-1 text-[11px] text-zinc-200 placeholder-zinc-600 focus:outline-none transition-colors min-w-0" + /> +
{/* Toolbar */} @@ -290,13 +236,13 @@ export function MemoryInspectorPanel({ workspaceId }: Props) { {debouncedQuery ? `${entries.length} result${entries.length !== 1 ? "s" : ""}` : entries.length === 1 - ? "1 entry" - : `${entries.length} entries`} + ? "1 memory" + : `${entries.length} memories`} @@ -316,11 +262,9 @@ export function MemoryInspectorPanel({ workspaceId }: Props) { {/* Content */}
{loading ? ( - /* Skeleton rows — visible during search-transition re-fetches */ ) : entries.length === 0 ? ( debouncedQuery ? ( - /* Search-specific empty state */

@@ -341,56 +285,40 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {

) : ( - /* Default empty state */
-

No memory entries yet

+

No {activeScope} memories

- Memory entries will appear here when the workspace writes to its KV - store. + {activeScope === "LOCAL" + ? "This workspace has not written any local memories yet." + : activeScope === "TEAM" + ? "No team memories shared with this workspace yet." + : "No global memories exist yet."}

) ) : (
- {entries.map((entry) => { - const isExpanded = expandedKey === entry.key; - const isEditing = editingKey === entry.key; - return ( - { - const next = isExpanded ? null : entry.key; - setExpandedKey(next); - if (!next && isEditing) cancelEdit(); - }} - onEditValueChange={setEditValue} - onStartEdit={() => startEdit(entry)} - onSave={() => saveEdit(entry)} - onCancelEdit={cancelEdit} - onDelete={() => setPendingDeleteKey(entry.key)} - /> - ); - })} + {entries.map((entry) => ( + setPendingDeleteId(entry.id)} + /> + ))}
)}
{/* Delete confirmation dialog */} setPendingDeleteKey(null)} + onCancel={() => setPendingDeleteId(null)} /> ); @@ -400,155 +328,97 @@ export function MemoryInspectorPanel({ workspaceId }: Props) { interface MemoryEntryRowProps { entry: MemoryEntry; - isExpanded: boolean; - isEditing: boolean; - editValue: string; - editError: string | null; - saving: boolean; - onToggle: () => void; - onEditValueChange: (v: string) => void; - onStartEdit: () => void; - onSave: () => void; - onCancelEdit: () => void; onDelete: () => void; } -function MemoryEntryRow({ - entry, - isExpanded, - isEditing, - editValue, - editError, - saving, - onToggle, - onEditValueChange, - onStartEdit, - onSave, - onCancelEdit, - onDelete, -}: MemoryEntryRowProps) { - const bodyId = `mem-body-${sanitizeId(entry.key)}`; +function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) { + const [expanded, setExpanded] = useState(false); + const bodyId = `mem-body-${sanitizeId(entry.id)}`; + return (
- {/* Header row — click to expand/collapse */} + {/* Header row */} {/* Expanded body */} - {isExpanded && ( + {expanded && (
- {entry.expires_at && ( -

- Expires: {new Date(entry.expires_at).toLocaleString()} -

- )} - - {isEditing ? ( - /* Edit mode */ -
-