diff --git a/apps/web/components/document-modal/content/plugin-content.tsx b/apps/web/components/document-modal/content/plugin-content.tsx index 5b4fd6d0..3315024a 100644 --- a/apps/web/components/document-modal/content/plugin-content.tsx +++ b/apps/web/components/document-modal/content/plugin-content.tsx @@ -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 (
- {!hideHeader && } -
+ +
{ + 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( diff --git a/apps/web/lib/plugin-document.ts b/apps/web/lib/plugin-document.ts index 1606311f..718e5804 100644 --- a/apps/web/lib/plugin-document.ts +++ b/apps/web/lib/plugin-document.ts @@ -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",