mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
Render agent conversations
Parse OpenCode and Claude Code transcripts into Codex-style conversation cards while retaining raw and plain-note fallbacks.
This commit is contained in:
parent
c3f58b7139
commit
131f13d8c2
3 changed files with 185 additions and 17 deletions
|
|
@ -130,12 +130,10 @@ export function PluginContent({ parsed }: { parsed: ParsedPluginDocument }) {
|
|||
const [mode, setMode] = useState<"structured" | "raw">("structured")
|
||||
const hasMessages = parsed.messages.length > 0
|
||||
|
||||
const hideHeader = parsed.kind === "claude-code-doc"
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{!hideHeader && <PluginHeader parsed={parsed} />}
|
||||
<div className={cn("px-4", hideHeader ? "pt-4" : "pt-3")}>
|
||||
<PluginHeader parsed={parsed} />
|
||||
<div className="px-4 pt-3">
|
||||
<div
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,123 @@ function makeCodexSessionDocument(content: string): PluginDocumentInput {
|
|||
}
|
||||
|
||||
describe("parsePluginDocument — session transcripts", () => {
|
||||
it("renders plain OpenCode documents in structured and raw views", () => {
|
||||
const content = "Vedant Mahajan works at Supermemory."
|
||||
const parsed = parsePluginDocument({
|
||||
id: "doc_opencode",
|
||||
title: content,
|
||||
content,
|
||||
source: "opencode",
|
||||
metadata: {
|
||||
sm_source: "opencode",
|
||||
sm_scope: "project",
|
||||
},
|
||||
containerTags: ["repo_supermemory"],
|
||||
memoryEntries: [],
|
||||
} as unknown as PluginDocumentInput)
|
||||
|
||||
expect(parsed?.pluginLabel).toBe("OpenCode")
|
||||
expect(parsed?.sections).toEqual([
|
||||
{ label: "Memory", value: content, tone: "default" },
|
||||
])
|
||||
expect(parsed?.rawContent).toBe(content)
|
||||
})
|
||||
|
||||
it("renders OpenCode transcripts as structured conversations", () => {
|
||||
const content = [
|
||||
"[Conversation ses_abc:1-2]",
|
||||
"1. [user] Test the renderer",
|
||||
"2. [assistant] First line of the response",
|
||||
"Second line of the response",
|
||||
].join("\n")
|
||||
const parsed = parsePluginDocument({
|
||||
id: "doc_opencode_conversation",
|
||||
title: "[Conversation ses_abc:1-2]",
|
||||
content,
|
||||
source: "opencode",
|
||||
metadata: {
|
||||
sm_source: "opencode",
|
||||
type: "conversation",
|
||||
conversationId: "ses_abc:1-2",
|
||||
},
|
||||
containerTags: ["repo_supermemory"],
|
||||
memoryEntries: [],
|
||||
} as unknown as PluginDocumentInput)
|
||||
|
||||
expect(parsed?.kind).toBe("plugin-session")
|
||||
expect(parsed?.title).toBe("OpenCode conversation")
|
||||
expect(parsed?.summary).toBe(
|
||||
"1 user message and 1 assistant message captured from OpenCode.",
|
||||
)
|
||||
expect(parsed?.identifierLabel).toBe("Conversation")
|
||||
expect(parsed?.identifierValue).toBe("ses_abc:1-2")
|
||||
expect(parsed?.messages).toEqual([
|
||||
{ id: "1-user", role: "user", text: "Test the renderer" },
|
||||
{
|
||||
id: "2-assistant",
|
||||
role: "assistant",
|
||||
text: "First line of the response\nSecond line of the response",
|
||||
},
|
||||
])
|
||||
expect(parsed?.sections).toEqual([])
|
||||
expect(parsed?.rawContent).toBe(content)
|
||||
})
|
||||
|
||||
it("renders Claude Code transcripts as conversations before embedded saves", () => {
|
||||
const content = [
|
||||
"<|turn_start|>2026-07-13T15:12:54.912Z",
|
||||
"<|start|>user<|message|>Remember that this project uses Vitest.<|end|>",
|
||||
"<|start|>assistant<|message|>I'll save that preference.<|end|>",
|
||||
"<|start|>assistant:tool_result<|message|>[SAVE:vedant:2026-07-13]",
|
||||
"Decision: Use Vitest for JavaScript tests.",
|
||||
"[/SAVE]<|end|>",
|
||||
"<|turn_end|>",
|
||||
].join("\n")
|
||||
const parsed = parsePluginDocument({
|
||||
id: "doc_claude_conversation",
|
||||
title: "Project Preference",
|
||||
content,
|
||||
source: "claude-code",
|
||||
metadata: {
|
||||
sm_source: "claude-code",
|
||||
type: "session_turn",
|
||||
project: "supermemory",
|
||||
},
|
||||
containerTags: ["repo_supermemory"],
|
||||
memoryEntries: [],
|
||||
} as unknown as PluginDocumentInput)
|
||||
|
||||
expect(parsed?.kind).toBe("claude-code-doc")
|
||||
expect(parsed?.formatLabel).toBe("Conversation")
|
||||
expect(parsed?.title).toBe("Claude Code conversation")
|
||||
expect(parsed?.summary).toBe(
|
||||
"1 user message and 1 assistant message captured from Claude Code.",
|
||||
)
|
||||
expect(parsed?.messages).toEqual([
|
||||
{
|
||||
id: "user-0",
|
||||
role: "user",
|
||||
text: "Remember that this project uses Vitest.",
|
||||
},
|
||||
{
|
||||
id: "assistant-1",
|
||||
role: "assistant",
|
||||
text: "I'll save that preference.",
|
||||
},
|
||||
{
|
||||
id: "tool-2",
|
||||
role: "tool",
|
||||
text: [
|
||||
"[SAVE:vedant:2026-07-13]",
|
||||
"Decision: Use Vitest for JavaScript tests.",
|
||||
"[/SAVE]",
|
||||
].join("\n"),
|
||||
},
|
||||
])
|
||||
expect(parsed?.sections).toEqual([])
|
||||
expect(parsed?.rawContent).toBe(content)
|
||||
})
|
||||
|
||||
it("keeps the Codex source badge inside a shared Agents container", () => {
|
||||
const parsed = parsePluginDocument({
|
||||
...makeCodexSessionDocument(
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ function parseSessionTranscript(
|
|||
content: string,
|
||||
config: {
|
||||
kind: "codex-session" | "amp-thread" | "plugin-session"
|
||||
headerLabel: "Session" | "Amp thread"
|
||||
headerLabel: "Session" | "Conversation" | "Amp thread"
|
||||
pluginLabel: string
|
||||
pluginIconSrc?: string | null
|
||||
formatLabel: string
|
||||
|
|
@ -469,6 +469,7 @@ function parsePluginSpaceNote(
|
|||
const summary = typeof document.summary === "string" ? document.summary : ""
|
||||
const memoryText = firstMemoryEntryText(document)
|
||||
const preview = takePreview(memoryText || summary || content, 220)
|
||||
const sectionText = content || memoryText || summary
|
||||
|
||||
return {
|
||||
kind: plugin.pluginId === "codex" ? "codex-save" : "plugin-save",
|
||||
|
|
@ -482,7 +483,9 @@ function parsePluginSpaceNote(
|
|||
summary: takePreview(summary || memoryText || content, 220),
|
||||
artifacts: [],
|
||||
messages: [],
|
||||
sections: [],
|
||||
sections: sectionText
|
||||
? [{ label: "Memory", value: sectionText, tone: "default" }]
|
||||
: [],
|
||||
rawContent: content,
|
||||
}
|
||||
}
|
||||
|
|
@ -634,28 +637,47 @@ function parseClaudeCodeByMetadata(
|
|||
|
||||
if (!source) return null
|
||||
|
||||
const summary = typeof document.summary === "string" ? document.summary : ""
|
||||
const title =
|
||||
(typeof document.title === "string" && document.title.trim()) ||
|
||||
(source.projectName
|
||||
? `${source.formatLabel} · ${source.projectName}`
|
||||
: source.formatLabel)
|
||||
|
||||
const documentSummary =
|
||||
typeof document.summary === "string" ? document.summary : ""
|
||||
const memoryText = firstMemoryEntryText(document)
|
||||
const cleanedTranscript = stripClaudeCodeTranscript(rawContent)
|
||||
const preview = takePreview(memoryText || cleanedTranscript || summary, 220)
|
||||
const messages = parseClaudeCodeTurns(rawContent)
|
||||
const isConversation = messages.length > 0
|
||||
const userCount = messages.filter((message) => message.role === "user").length
|
||||
const assistantCount = messages.filter(
|
||||
(message) => message.role === "assistant",
|
||||
).length
|
||||
const fallbackPreview = memoryText || cleanedTranscript || documentSummary
|
||||
const previewSource =
|
||||
messages.find((message) => message.role === "user")?.text ??
|
||||
messages[0]?.text ??
|
||||
fallbackPreview
|
||||
const preview = takePreview(previewSource, 220)
|
||||
const title = isConversation
|
||||
? `${source.label} conversation`
|
||||
: (typeof document.title === "string" && document.title.trim()) ||
|
||||
(source.projectName
|
||||
? `${source.formatLabel} · ${source.projectName}`
|
||||
: source.formatLabel)
|
||||
const summary = isConversation
|
||||
? `${userCount} user message${userCount === 1 ? "" : "s"} and ${assistantCount} assistant message${assistantCount === 1 ? "" : "s"} captured from ${source.label}.`
|
||||
: documentSummary
|
||||
const sectionText = rawContent || memoryText || documentSummary
|
||||
|
||||
const parsed: ParsedPluginDocument = {
|
||||
kind: "claude-code-doc",
|
||||
pluginLabel: source.label,
|
||||
pluginIconSrc: source.iconSrc,
|
||||
formatLabel: source.formatLabel,
|
||||
formatLabel: isConversation ? "Conversation" : source.formatLabel,
|
||||
title,
|
||||
preview,
|
||||
summary,
|
||||
artifacts: [],
|
||||
messages: parseClaudeCodeTurns(rawContent),
|
||||
sections: [],
|
||||
messages,
|
||||
sections:
|
||||
!isConversation && sectionText
|
||||
? [{ label: "Memory", value: sectionText, tone: "default" }]
|
||||
: [],
|
||||
rawContent,
|
||||
}
|
||||
return parsed
|
||||
|
|
@ -680,6 +702,20 @@ export function parsePluginDocument(
|
|||
)
|
||||
|
||||
if (content) {
|
||||
if (
|
||||
plugin?.pluginId === "claude-code" &&
|
||||
CLAUDE_CODE_CONTENT_RE.test(content)
|
||||
) {
|
||||
const claudeCodeSession = parseClaudeCodeByMetadata(document, metadata)
|
||||
if (claudeCodeSession) {
|
||||
if (clientName) {
|
||||
claudeCodeSession.clientLabel = "Client"
|
||||
claudeCodeSession.clientValue = clientName
|
||||
}
|
||||
return withIcon(claudeCodeSession)
|
||||
}
|
||||
}
|
||||
|
||||
const saveDoc = parseSaveSections(content, plugin)
|
||||
if (saveDoc) {
|
||||
if (clientName) {
|
||||
|
|
@ -710,6 +746,23 @@ export function parsePluginDocument(
|
|||
}
|
||||
}
|
||||
|
||||
if (plugin?.pluginId === "opencode") {
|
||||
const openCodeSession = parseSessionTranscript(content, {
|
||||
kind: "plugin-session",
|
||||
headerLabel: "Conversation",
|
||||
pluginLabel: plugin.label,
|
||||
pluginIconSrc: plugin.iconSrc,
|
||||
formatLabel: "Conversation",
|
||||
})
|
||||
if (openCodeSession) {
|
||||
if (clientName) {
|
||||
openCodeSession.clientLabel = "Client"
|
||||
openCodeSession.clientValue = clientName
|
||||
}
|
||||
return withIcon(openCodeSession)
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin?.pluginId === "amp") {
|
||||
const ampThread = parseSessionTranscript(content, {
|
||||
kind: "amp-thread",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue