Skip to content
Open
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
13 changes: 8 additions & 5 deletions packages/memory-graph/src/components/legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,16 @@ function LineIcon({
)
}

function ClusterSwatches() {
const swatches = ["#58C7E8", "#E7BC52", "#74D680", "#D47B75", "#A789E8"]
function ClusterSwatches({ colors }: { colors: GraphThemeColors }) {
const swatches =
colors.clusterColors.length > 0
? colors.clusterColors.slice(0, 5)
: [colors.memStrokeDefault]
Comment on lines +85 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent fallback behavior when clusterColors is empty. The legend shows [colors.memStrokeDefault] (single color), but the graph data functions in use-graph-data.ts fall back to DEFAULT_CLUSTER_COLORS (full palette). This causes a mismatch where the legend could display one color while the actual graph uses the default multi-color palette.

// Fix: Use consistent fallback
const swatches =
	colors.clusterColors.length > 0
		? colors.clusterColors.slice(0, 5)
		: DEFAULT_CLUSTER_COLORS.slice(0, 5)

This ensures the legend accurately represents the colors actually used in the graph when clusterColors is explicitly set to an empty array via overrides.

Suggested change
const swatches =
colors.clusterColors.length > 0
? colors.clusterColors.slice(0, 5)
: [colors.memStrokeDefault]
const swatches =
colors.clusterColors.length > 0
? colors.clusterColors.slice(0, 5)
: DEFAULT_CLUSTER_COLORS.slice(0, 5)

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

return (
<div style={{ display: "flex", gap: 2, flexShrink: 0 }}>
{swatches.map((color) => (
{swatches.map((color, index) => (
<span
key={color}
key={`${color}-${index}`}
style={{
width: 7,
height: 12,
Expand Down Expand Up @@ -563,7 +566,7 @@ export const Legend = memo(function Legend({
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<span style={sectionLabelStyle}>Color</span>
<div style={statusRowStyle}>
<ClusterSwatches />
<ClusterSwatches colors={colors} />
<div
style={{
display: "flex",
Expand Down
14 changes: 14 additions & 0 deletions packages/memory-graph/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import type { GraphThemeColors } from "./types"

export const DEFAULT_CLUSTER_COLORS = [
"#58C7E8",
"#E7BC52",
"#74D680",
"#D47B75",
"#A789E8",
"#62C5A8",
"#74ABD8",
"#C78AC8",
"#D18A58",
"#8BCB6F",
]

export const MEMORY_BORDER_KEYS = {
forgotten: "memBorderForgotten",
expiring: "memBorderExpiring",
Expand Down Expand Up @@ -66,4 +79,5 @@ export const DEFAULT_COLORS: GraphThemeColors = {
popoverTextMuted: "#94a3b8",
controlBg: "#1a1f29",
controlBorder: "#2A2F36",
clusterColors: DEFAULT_CLUSTER_COLORS,
}
40 changes: 21 additions & 19 deletions packages/memory-graph/src/hooks/use-graph-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
GraphThemeColors,
MemoryNodeData,
} from "../types"
import { DEFAULT_CLUSTER_COLORS } from "../constants"
import { hashString } from "../utils/hash"

const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000
Expand All @@ -21,19 +22,6 @@ const APPEND_CANDIDATES_PER_RING = 18
const APPEND_MAX_RINGS = 8
const APPEND_SPATIAL_CELL_SIZE = APPEND_CLUSTER_RADIUS + APPEND_AREA_GAP + 120
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5))
const CLUSTER_COLORS = [
"#58C7E8",
"#E7BC52",
"#74D680",
"#D47B75",
"#A789E8",
"#62C5A8",
"#74ABD8",
"#C78AC8",
"#D18A58",
"#8BCB6F",
]

export interface ClusterAssignment {
key: string
color: string
Expand Down Expand Up @@ -107,12 +95,18 @@ function getMemoryRingCapacity(ring: number): number {
return Math.max(8, Math.floor((2 * Math.PI * radius) / MEMORY_ORBIT_SPACING))
}

export function getClusterColor(key: string): string {
return CLUSTER_COLORS[hashString(key) % CLUSTER_COLORS.length] as string
export function getClusterColor(
key: string,
clusterColors: readonly string[] = DEFAULT_CLUSTER_COLORS,
): string {
const palette =
clusterColors.length > 0 ? clusterColors : DEFAULT_CLUSTER_COLORS
return palette[hashString(key) % palette.length] as string
}

export function computeClusterAssignments(
documents: GraphApiDocument[],
clusterColors: readonly string[] = DEFAULT_CLUSTER_COLORS,
): Map<string, ClusterAssignment> {
const adjacency = new Map<string, Set<string>>()
const docByMemory = new Map<string, string>()
Expand Down Expand Up @@ -180,7 +174,7 @@ export function computeClusterAssignments(
: `relation:${firstDocId}:${firstId}`
const assignment = {
key,
color: getClusterColor(key),
color: getClusterColor(key, clusterColors),
size: component.length,
}

Expand All @@ -205,6 +199,7 @@ function getMemoryRelationTargets(mem: GraphApiMemory): Record<string, string> {
function getDocumentClusterAssignment(
doc: GraphApiDocument,
assignments: Map<string, ClusterAssignment>,
clusterColors: readonly string[] = DEFAULT_CLUSTER_COLORS,
): ClusterAssignment {
const counts = new Map<
string,
Expand All @@ -229,7 +224,7 @@ function getDocumentClusterAssignment(
return (
best?.assignment ?? {
key: `doc:${doc.id}`,
color: getClusterColor(`doc:${doc.id}`),
color: getClusterColor(`doc:${doc.id}`, clusterColors),
size: 1,
}
)
Expand Down Expand Up @@ -492,7 +487,10 @@ export function useGraphData(
? buildAppendSpatialGrid(appendPlacementNodes)
: null
let appendIndex = 0
const clusterAssignments = computeClusterAssignments(documents)
const clusterAssignments = computeClusterAssignments(
documents,
colors.clusterColors,
)

const result: GraphNode[] = []
// Spiral layout: documents form a compact spiral core, memories orbit
Expand All @@ -509,7 +507,11 @@ export function useGraphData(

for (let docIdx = 0; docIdx < docCount; docIdx++) {
const doc = documents[docIdx]
const docCluster = getDocumentClusterAssignment(doc, clusterAssignments)
const docCluster = getDocumentClusterAssignment(
doc,
clusterAssignments,
colors.clusterColors,
)
const angle = docIdx * goldenAngle
const radius = spiralScale * Math.sqrt((docIdx + 1) / docCount)
const initialX = cx + Math.cos(angle) * radius
Expand Down
21 changes: 19 additions & 2 deletions packages/memory-graph/src/hooks/use-graph-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ function readCssVar(name: string, fallback: string): string {
return val || fallback
}

function readCssColorList(name: string, fallback: string[]): string[] {
if (typeof document === "undefined") return fallback
const val = getComputedStyle(document.documentElement)
.getPropertyValue(name)
.trim()
if (!val) return fallback
const colors = val
.split(",")
.map((color) => color.trim())
.filter(Boolean)
return colors.length > 0 ? colors : fallback
}

function resolveColors(): GraphThemeColors {
return {
bg: readCssVar("--graph-bg", DEFAULT_COLORS.bg),
Expand Down Expand Up @@ -71,6 +84,10 @@ function resolveColors(): GraphThemeColors {
"--graph-control-border",
DEFAULT_COLORS.controlBorder,
),
clusterColors: readCssColorList(
"--graph-cluster-colors",
DEFAULT_COLORS.clusterColors,
),
}
}

Expand Down Expand Up @@ -107,13 +124,13 @@ export function useGraphTheme(
const overrideKey = overrides
? Object.entries(overrides)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}:${v}`)
.map(([k, v]) => `${k}:${Array.isArray(v) ? v.join("|") : v}`)
.join(",")
: ""

// biome-ignore lint/correctness/useExhaustiveDependencies: overrideKey tracks overrides by value
const merged = useMemo(
() => (overrides ? { ...colors, ...overrides } : colors),
// biome-ignore lint/correctness/useExhaustiveDependencies: overrideKey tracks overrides by value
[colors, overrideKey],
)

Expand Down
1 change: 1 addition & 0 deletions packages/memory-graph/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface GraphThemeColors {
popoverTextMuted: string
controlBg: string
controlBorder: string
clusterColors: string[]
}

export interface GraphCanvasProps {
Expand Down