This commit is contained in:
Abhay Singh 2026-08-04 22:46:44 -07:00 • committed by GitHub
commit 2f8c977f17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 125 additions and 18 deletions

View file

@ -17,6 +17,7 @@ import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
serializeMemoriesForDataset,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
@ -212,7 +213,9 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) {
memoryLength: memoryText.length,
})
iconElement.dataset.memoriesData = String(response.data)
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")

View file

@ -17,6 +17,7 @@ import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
serializeMemoriesForDataset,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
@ -459,7 +460,9 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
memoryLength: memoryText.length,
})
iconElement.dataset.memoriesData = String(response.data)
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")

View file

@ -17,6 +17,7 @@ import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
serializeMemoriesForDataset,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
@ -417,7 +418,9 @@ async function getRelatedMemoriesForGemini(actionSource: string) {
if (response?.success && response?.data && input) {
const memoryText = showMemorySuggestion("gemini", input, response.data)
iconElement.dataset.memoriesData = String(response.data)
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
iconElement.dataset.supermemories = memoryText
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")

View file

@ -0,0 +1,49 @@
import { describe, expect, it } from "bun:test"
import {
parseMemoriesFromDataset,
serializeMemoriesForDataset,
} from "./memory-suggestion"
describe("memory dataset serialization", () => {
it("round-trips a memory that contains a comma as a single item", () => {
const memories = ["Lives in Austin, Texas", "Prefers dark mode"]
const stored = serializeMemoriesForDataset(memories)
expect(parseMemoriesFromDataset(stored)).toEqual(memories)
})
it("round-trips a memory that contains a newline as a single item", () => {
const memories = ["Shipping address:\n123 Main St", "Likes coffee"]
const stored = serializeMemoriesForDataset(memories)
expect(parseMemoriesFromDataset(stored)).toEqual(memories)
})
it("trims and drops empty entries when serializing", () => {
const stored = serializeMemoriesForDataset([" keep ", "", " "])
expect(parseMemoriesFromDataset(stored)).toEqual(["keep"])
})
it("serializes an empty list to an empty string for truthiness checks", () => {
expect(serializeMemoriesForDataset([])).toBe("")
expect(serializeMemoriesForDataset(undefined)).toBe("")
})
it("returns an empty array for empty or missing input", () => {
expect(parseMemoriesFromDataset("")).toEqual([])
expect(parseMemoriesFromDataset(null)).toEqual([])
expect(parseMemoriesFromDataset(undefined)).toEqual([])
})
it("falls back to the legacy comma/newline split for non-JSON values", () => {
expect(parseMemoriesFromDataset("first,second\nthird")).toEqual([
"first",
"second",
"third",
])
})
it("wraps a single non-array value into one item", () => {
expect(
parseMemoriesFromDataset(serializeMemoriesForDataset("solo")),
).toEqual(["solo"])
})
})

View file

@ -12,6 +12,52 @@ export function buildSupermemoryText(memories: unknown): string {
return `\n\n${SUPERMEMORY_PREFIX} ${memoryText}`
}
function normalizeMemoryList(memories: unknown): string[] {
const list = Array.isArray(memories)
? memories
: memories == null
? []
: [memories]
return list
.map((memory) => (typeof memory === "string" ? memory : String(memory)))
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0)
}
/**
* Serialize the memory list for storage on a `data-*` attribute. Memories are
* free text that can contain commas and newlines, so they are stored as JSON
* rather than joined into a single string, otherwise a memory with a comma in
* it is split into fragments when the popup reads it back. Returns an empty
* string for an empty list so existing truthiness checks on the attribute
* (memories present vs not) keep working.
*/
export function serializeMemoriesForDataset(memories: unknown): string {
const list = normalizeMemoryList(memories)
return list.length > 0 ? JSON.stringify(list) : ""
}
/**
* Read back a memory list written by {@link serializeMemoriesForDataset}.
* Falls back to the legacy comma/newline split so any value written by older
* code (or a plain joined string) still renders.
*/
export function parseMemoriesFromDataset(
raw: string | null | undefined,
): string[] {
if (!raw) return []
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) return normalizeMemoryList(parsed)
} catch {
// Not JSON — fall through to the legacy delimiter split.
}
return raw
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
}
export function showMemorySuggestion(
platform: string,
input: SuggestionInput,
@ -305,10 +351,7 @@ export function showMarkerPopover(
color: rgba(255, 255, 255, 0.76);
`
memories
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
parseMemoriesFromDataset(memories)
.slice(0, 5)
.forEach((memory) => {
const item = document.createElement("div")

View file

@ -10,6 +10,10 @@ import {
autoCapturePromptsEnabled,
} from "../../utils/storage"
import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components"
import {
parseMemoriesFromDataset,
serializeMemoriesForDataset,
} from "./memory-suggestion"
let t3DebounceTimeout: NodeJS.Timeout | null = null
let t3RouteObserver: MutationObserver | null = null
@ -233,7 +237,9 @@ async function getRelatedMemoriesForT3(actionSource: string) {
if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`
iconElement.dataset.memoriesData = response.data
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
updateT3IconFeedback("Included Memories", iconElement)
} else {
@ -329,11 +335,9 @@ function updateT3IconFeedback(
overflow-y: auto;
`
const memoriesText = iconElement.dataset.memoriesData || ""
const individualMemories = memoriesText
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
const individualMemories = parseMemoriesFromDataset(
iconElement.dataset.memoriesData,
)
individualMemories.forEach((memory, index) => {
const memoryItem = document.createElement("div")
@ -421,15 +425,17 @@ function updateT3IconFeedback(
content.removeChild(memoryItem)
}
const currentMemories = (iconElement.dataset.memoriesData || "")
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
const currentMemories = parseMemoriesFromDataset(
iconElement.dataset.memoriesData,
)
currentMemories.splice(index, 1)
// Injected prompt keeps its existing joined-text form; the popup's
// own data is stored as JSON so comma-bearing memories stay intact.
const updatedMemories = currentMemories.join(" ,")
iconElement.dataset.memoriesData = updatedMemories
iconElement.dataset.memoriesData =
serializeMemoriesForDataset(currentMemories)
const textareaElement =
(document.querySelector("textarea") as HTMLTextAreaElement) ||