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:
ved015 2026-07-24 19:46:54 +05:30
parent c3f58b7139
commit 131f13d8c2
3 changed files with 185 additions and 17 deletions

View file

@ -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(),

View file

@ -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(

View file

@ -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",