Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 143 additions & 128 deletions dashboard/src/v2/components/memory/MemoryFilters.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @vitest-environment jsdom */
import { h } from "preact";
import { render, fireEvent } from "@testing-library/preact";
import { render, fireEvent, within } from "@testing-library/preact";
import * as matchers from "@testing-library/jest-dom/matchers";
import { expect, test, describe, afterEach, vi } from "vitest";
import { MemoryFilters } from "../MemoryFilters.js";
Expand Down Expand Up @@ -39,6 +39,10 @@ describe("MemoryFilters Accessibility", () => {

expect(shortTermTab).toHaveAttribute("aria-selected", "true");
expect(longTermTab).toHaveAttribute("aria-selected", "false");
expect(within(shortTermTab).getByText("7 memories")).toBeInTheDocument();
expect(within(longTermTab).getByText("10 memories")).toBeInTheDocument();
expect(getByRole("group", { name: "Memory scope filters" })).toBeInTheDocument();
expect(getByRole("group", { name: "Memory actions" })).toBeInTheDocument();
});

test("selects have proper aria labels", () => {
Expand Down Expand Up @@ -88,6 +92,7 @@ describe("MemoryFilters Accessibility", () => {
await fireEvent.change(getByRole("combobox", { name: "Filter memory by Agent Preset" }), { target: { value: "agent2" } });
expect(selectedAgentPresetIdSignal.value).toBe("agent2");
expect(getByText("Agent filter set to Agent 2.")).toBeInTheDocument();
expect(getByText("Short Term: showing 7 memories of 17 memories · Sprint 2 · Agent 2")).toBeInTheDocument();
});

test("tab keyboard navigation works", async () => {
Expand Down Expand Up @@ -142,8 +147,9 @@ describe("MemoryFilters Accessibility", () => {

const toggleBtn = getByRole("button", { name: "Hide embedding model catalog" });
expect(toggleBtn).toHaveAttribute("aria-pressed", "true");
expect(toggleBtn).toHaveAccessibleDescription("Active: test");
expect(getByText("Shown")).toBeInTheDocument();
expect(getByText("1 active")).toBeInTheDocument();
expect(getByText("Active: test")).toBeInTheDocument();
});

test("Danger mode toggle uses aria-pressed and persistent state copy", async () => {
Expand Down Expand Up @@ -200,7 +206,7 @@ describe("MemoryFilters Accessibility", () => {
/>
);

expect(getByText("Short Term: showing 0 of 3 memories · No sprint selected · All Agents")).toBeInTheDocument();
expect(getByText("Short Term: showing 0 memories of 3 memories · No sprint selected · All Agents")).toBeInTheDocument();
expect(getByRole("combobox", { name: "Filter memory by Sprint" })).toBeDisabled();
expect(getByText("Sprint filter disabled because this project has no sprints with memory.")).toBeInTheDocument();
expect(getByRole("combobox", { name: "Filter memory by Agent Preset" })).toBeDisabled();
Expand Down
22 changes: 10 additions & 12 deletions docs-web/user/dashboard/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,22 @@ Each memory has a **category**:

Categories drive default rendering and can be used as filters in search.

## Filters and tabs
## Header summary, filters, and actions

The page splits into:
The Memory header is the main place to choose what the graph, sidebar, and inspector are showing. It is grouped into separate rows so the current state stays readable on desktop and wraps cleanly on narrow screens:

- **Short-term** tab — memories scoped to a specific sprint.
- **Long-term** tab — project-wide memories.
- **Search** — vector similarity search across both tiers (cosine similarity, configurable `minSimilarity`).
- **Tier summary tabs** — **Short Term** and **Long Term** show their memory counts directly in the tab cards.
- **Current scope line** — shows copy such as `Short Term: showing 7 memories of 17 memories · Sprint 2 · All Agents` or `Long Term: showing 1 memory of 1 memory · Project-wide · All Agents`.
- **Scope filters** — Short-term memory shows the sprint selector and both tiers show the agent preset selector. Disabled selectors remain visible with reason copy when there are no matching sprints or agent presets.
- **Actions** — **Add Memory**, **Model Catalog**, and **Danger Delete** are separated from the selectors. Model Catalog shows whether it is shown or hidden plus active-model status. Danger Delete always shows Off/Armed state plus persistent explanatory copy.

Filter bar:
The sidebar search field filters the current visible tier, sprint, and agent slice by memory text/category. Programmatic semantic search still uses vector similarity across requested scopes (cosine similarity, configurable `minSimilarity`).

- **Category** (multi-select).
- **Sprint** (short-term only).
- **Agent preset** (only memories scoped to that preset).
- **Strength range** — filter by promotion strength score (0.0 – 1.0+).
Danger Delete semantics are unchanged: graph and inspector single-memory deletes are immediate only while Danger Delete is armed, while sidebar card deletion uses its separate arm/cancel guard.

## Creating a memory

Click **+ New memory**. Provide:
Click **Add Memory**. Provide:

- **Content** — the memory body (markdown supported).
- **Category** (default `context`).
Expand All @@ -55,7 +53,7 @@ The memory is embedded immediately using the active embedding model.

## Editing & deleting

Each memory card has **Edit** (content, category, strength) and **Delete**. Deletion requires confirmation.
Sidebar memory cards use their own guarded delete flow. Graph and inspector single-memory deletion is immediate only while Danger Delete is armed.

## Promotion (short-term → long-term)

Expand Down
4 changes: 3 additions & 1 deletion docs/dashboard/design-system-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ The Memory UI relies on specific hex colors that match existing app accents:
- **Sidebar & Details:** The memory sidebar starts collapsed as a narrow rail so the canvas stays available by default. When expanded, it becomes a mobile drawer with an internal scrolling list and a desktop side rail.
- **Inspector Dock:** The inspector is a bottom sheet on mobile and a fixed right dock on desktop. Its scroll area is internal, the close button is always labeled, and the panel never blocks the ability to dismiss it.
- **Overlay Safety:** Zoom controls, legend, and node count live inside the canvas wrapper with safe offsets that move away from the inspector on desktop and stay reachable on mobile.
- **Filters & Search:** Filter controls wrap into multiple rows with `flex-wrap`, `min-w-0`, and `max-w-full` so tier tabs, sprint selectors, agent selectors, model catalog, add memory, and danger actions never force horizontal scrolling.
- **Header Filters & Actions:** The Memory header control surface groups tier summary tabs, current-scope copy, scope selectors, and actions into distinct rows. Tier tabs show Short Term and Long Term counts directly; the compact scope line uses the pattern `Short Term: showing 7 memories of 17 memories · Sprint 2 · All Agents` or `Long Term: showing 1 memory of 1 memory · Project-wide · All Agents`.
- **Filters & Search:** Filter controls wrap into multiple rows with `flex-wrap`, `min-w-0`, stable flex bases, and `max-w-full` so tier tabs, sprint selectors, agent selectors, model catalog, add memory, and danger actions never force horizontal scrolling.
- **Header Actions:** Add Memory, Model Catalog, and Danger Delete are visually separated from selectors. Model Catalog keeps `aria-pressed`, shown/hidden text, and active-model status visible. Danger Delete keeps `aria-pressed`, Off/Armed button text, and persistent explanatory copy visible in both states.
- **Sidebar Search:** The expanded memory sidebar includes a visible search field above the list. Search filters are debounced and optional; clearing the field must restore the full alive memory list for the current tier/filter context.
- **Truncation:** Metadata limits string lengths gracefully using `truncate`, `break-words`, and compact badges for connected-memory details.

Expand Down
4 changes: 2 additions & 2 deletions docs/dashboard/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ The Memory settings panel also manages one project-scoped scheduler entry for lo
- Selection is pruned automatically when search, tier, sprint, agent, or sidebar state changes make a memory invisible, which keeps batch actions scoped to the current visible slice of memory.
- Improved memory list accessibility and reduced motion fallbacks in `MemoryList.tsx`, utilizing `useInteractionTokens` to respect OS-level reduced motion preferences.
- Updated the memory map camera so wheel, button, and click focus interactions preserve readable navigation on dense graphs. Wheel zoom uses smoother proportional movement, and graph labels keep stable on-screen sizing during zoom so text remains readable while node positions scale.
- `MemoryFilters.tsx` implements proper tab semantics, count text, roving keyboard focus, selected sprint/agent feedback, and model-catalog pressed-state copy. Lobotomize (delete) mode remains a single toggle but now has persistent danger-delete copy plus stronger pressed affordance so immediate single-memory deletion is visibly armed and reversible before use.
- Tier, sprint, agent, model catalog, and danger delete controls now expose the current selected or pressed state with visible status text and polite announcements. Disabled sprint and agent filters stay visible with reason copy instead of disappearing when no options are available.
- `MemoryFilters.tsx` implements proper tab semantics, count text, roving keyboard focus, selected sprint/agent feedback, and model-catalog pressed-state copy. The header now presents Short Term and Long Term as count summary cards, follows with a compact current-scope line such as `Short Term: showing 7 memories of 17 memories · Sprint 2 · All Agents`, and separates selectors from Add Memory, Model Catalog, and Danger Delete actions.
- Tier, sprint, agent, model catalog, and danger delete controls now expose the current selected or pressed state with visible status text and polite announcements. Disabled sprint and agent filters stay visible with reason copy instead of disappearing when no options are available, Model Catalog shows the active-model status, and the grouped header layout uses `min-w-0`, wrapping, and stable flex bases to avoid horizontal overflow.
- The memory list uses the shared `listReveal`, `listReorder`, and `expansionCollapse` motion tokens for search/filter transitions. Reduced-motion users receive immediate list updates while visible result counts and live regions continue to communicate what changed.
- Graph and list selection state is mirrored in text: selected cards show an `Open` badge, the graph area includes a visible selection status, and the inspector announces when a selected memory is open. This keeps critical selection feedback available without depending on canvas animation alone.
- During background refresh or failed refreshes, the sidebar keeps the last useful memory result list visible when available, marks the region busy or stale with visible copy, and exposes retry or next-action controls instead of replacing the list with a blank panel. Stale content is only reused for the same committed search query, so a new no-match search shows the no-match recovery state rather than old matches.
Expand Down
5 changes: 5 additions & 0 deletions tests/dashboard/v2/memory-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ describe("MemoryPage destructive mode", () => {
await waitFor(() => {
expect(screen.getByText("1 nodes")).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByText("Long Term: showing 1 memory of 1 memory · Project-wide · All Agents")).toBeInTheDocument();
});
expect(screen.getByRole("group", { name: "Memory scope filters" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Show embedding model catalog" })).toHaveAccessibleDescription("No active model");

await userEvent.click(screen.getByRole("button", { name: "Enable danger delete mode" }));

Expand Down