From c21b4be45c042650f99d8a9f2bf82c23e1170c4c Mon Sep 17 00:00:00 2001 From: ved015 Date: Fri, 5 Jun 2026 21:43:02 +0530 Subject: [PATCH] fix plugin memory attribution --- apps/web/components/dashboard-view.tsx | 59 ++++-- apps/web/lib/plugin-catalog.ts | 1 + apps/web/lib/plugin-document.ts | 278 +++++++++++++++++++++---- apps/web/lib/plugin-space.ts | 13 +- 4 files changed, 293 insertions(+), 58 deletions(-) diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx index 4194f827..7f378326 100644 --- a/apps/web/components/dashboard-view.tsx +++ b/apps/web/components/dashboard-view.tsx @@ -40,6 +40,7 @@ import { type Profession, } from "@/hooks/use-personalization" import { normalizePluginClientId } from "@/lib/plugin-catalog" +import { detectPluginSpace } from "@/lib/plugin-space" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] @@ -434,13 +435,19 @@ function getDocumentMetadataRecords( } function hasClaudeCodeContainer(document: DocumentWithMemories): boolean { + return getDocumentContainerTags(document).some((tag) => + tag.startsWith("claudecode_"), + ) +} + +function getDocumentContainerTags(document: DocumentWithMemories): string[] { const containerTags = (document as { containerTags?: string[] }).containerTags ?? [] - if (containerTags.some((tag) => tag.startsWith("claudecode_"))) return true + const memorySpaceTags = (document.memoryEntries ?? []) + .map((entry) => entry.spaceContainerTag) + .filter((tag): tag is string => !!tag) - return (document.memoryEntries ?? []).some((entry) => - entry.spaceContainerTag?.startsWith("claudecode_"), - ) + return [...containerTags, ...memorySpaceTags] } function getPluginClientFromDocument( @@ -462,6 +469,11 @@ function getPluginClientFromDocument( if (hasClaudeCodeContainer(document)) return "claude_code" + for (const tag of getDocumentContainerTags(document)) { + const plugin = detectPluginSpace(tag) + if (plugin) return normalizePluginClientId(plugin.pluginId) + } + const content = getDocumentText(document) const title = document.title ?? "" if ( @@ -689,17 +701,38 @@ function parseToolUsage( } // Attach latest document info to plugin items where available from MCP documents - for (const [, item] of toolMap) { - if (item.type === "Plugin" && !item.lastDocumentTitle) { - const pluginId = item.id.replace(/^plugin_/, "") - const docInfo = latestDocPerPlugin.get(pluginId) - if (docInfo) { - item.lastDocumentTitle = docInfo.title - item.lastDocumentId = docInfo.id - item.lastDocumentPreview = docInfo.preview - item.lastDocument = docInfo.document + for (const [pluginId, docInfo] of latestDocPerPlugin) { + const itemKey = `plugin_${pluginId}` + const existingItem = toolMap.get(itemKey) + if (existingItem?.type === "Plugin") { + existingItem.lastDocumentTitle = docInfo.title + existingItem.lastDocumentId = docInfo.id + existingItem.lastDocumentPreview = docInfo.preview + existingItem.lastDocument = docInfo.document + existingItem.hasBeenUsed = true + if ( + !existingItem.lastUsedAt || + docInfo.at.getTime() > existingItem.lastUsedAt.getTime() + ) { + existingItem.lastUsedAt = docInfo.at } + continue } + + const catalog = PLUGIN_DISPLAY_CATALOG[pluginId] + toolMap.set(itemKey, { + id: itemKey, + name: catalog?.name ?? pluginId, + type: "Plugin", + icon: catalog?.icon ?? null, + lastUsedAt: docInfo.at, + hasBeenUsed: true, + connectedAt: null, + lastDocumentTitle: docInfo.title, + lastDocumentId: docInfo.id, + lastDocumentPreview: docInfo.preview, + lastDocument: docInfo.document, + }) } // Sort by lastUsedAt (most recent first), then by hasBeenUsed diff --git a/apps/web/lib/plugin-catalog.ts b/apps/web/lib/plugin-catalog.ts index f94a4e92..fb5113f4 100644 --- a/apps/web/lib/plugin-catalog.ts +++ b/apps/web/lib/plugin-catalog.ts @@ -142,6 +142,7 @@ const SPACE_TO_CATALOG_ID: Record = { codex: "codex", opencode: "opencode", openclaw: "openclaw", + hermes: "hermes", } export function spacePluginIdToCatalogId(spacePluginId: string): string | null { diff --git a/apps/web/lib/plugin-document.ts b/apps/web/lib/plugin-document.ts index c5f79db3..a22d2395 100644 --- a/apps/web/lib/plugin-document.ts +++ b/apps/web/lib/plugin-document.ts @@ -1,6 +1,11 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import type { z } from "zod" -import { detectPluginSource, pluginIconByLabel } from "@/lib/plugin-space" +import { + detectPluginSource, + detectPluginSpace, + pluginIconByLabel, + type PluginSpaceInfo, +} from "@/lib/plugin-space" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] @@ -8,10 +13,19 @@ type DocumentWithMemories = DocumentsResponse["documents"][0] export type PluginDocumentKind = | "codex-session" | "codex-save" + | "plugin-session" + | "plugin-save" | "amp-thread" | "openclaw-session" | "claude-code-doc" +type PluginIdentity = { + pluginId: PluginSpaceInfo["pluginId"] + label: string + iconSrc: string | null + projectId?: string +} + export interface PluginArtifact { label: string value: string @@ -76,11 +90,108 @@ function formatClientName(value: string | null | undefined): string | null { if (lower === "claude code") return "Claude Code" if (lower === "opencode") return "OpenCode" if (lower === "openclaw") return "OpenClaw" + if (lower === "hermes") return "Hermes" if (lower === "amp") return "Amp" return normalized.replace(/\b\w/g, (match) => match.toUpperCase()) } +function pluginIdentityFromSource( + value: string | null | undefined, +): PluginIdentity | null { + if (!value) return null + + const normalized = value.trim().toLowerCase().replace(/_/g, "-") + if (!normalized) return null + + switch (normalized) { + case "claude-code": + case "claude-code-plugin": + return { + pluginId: "claude-code", + label: "Claude Code", + iconSrc: "/images/plugins/claude-code.svg", + } + case "codex": + return { + pluginId: "codex", + label: "Codex", + iconSrc: "/images/plugins/codex.png", + } + case "openclaw": + return { + pluginId: "openclaw", + label: "OpenClaw", + iconSrc: "/images/plugins/openclaw.svg", + } + case "hermes": + return { + pluginId: "hermes", + label: "Hermes", + iconSrc: "/images/plugins/hermes.svg", + } + case "opencode": + return { + pluginId: "opencode", + label: "OpenCode", + iconSrc: "/images/plugins/opencode.svg", + } + case "amp": + return { + pluginId: "amp", + label: "Amp", + iconSrc: null, + } + default: + return null + } +} + +function pluginIdentityFromSpace( + document: DocumentWithMemories, +): PluginIdentity | null { + const documentWithTags = document as DocumentWithMemories & { + containerTags?: unknown + } + const containerTags = Array.isArray(documentWithTags.containerTags) + ? documentWithTags.containerTags.filter( + (tag): tag is string => typeof tag === "string" && !!tag, + ) + : [] + const memorySpaceTags = Array.isArray(document.memoryEntries) + ? document.memoryEntries + .map((entry) => entry?.spaceContainerTag) + .filter((tag): tag is string => typeof tag === "string" && !!tag) + : [] + + for (const tag of [...containerTags, ...memorySpaceTags]) { + const plugin = detectPluginSpace(tag) + if (plugin) return plugin + } + + return null +} + +function getDocumentPluginIdentity( + document: DocumentWithMemories, + metadata: Record, +): PluginIdentity | null { + const sourceCandidates = [ + typeof document.source === "string" ? document.source : null, + typeof metadata.sm_source === "string" ? metadata.sm_source : null, + typeof metadata.sm_internal_mcp_client_name === "string" + ? metadata.sm_internal_mcp_client_name + : null, + ] + + for (const source of sourceCandidates) { + const plugin = pluginIdentityFromSource(source) + if (plugin) return plugin + } + + return pluginIdentityFromSpace(document) +} + function extractArtifacts(text: string): { cleanText: string artifacts: PluginArtifact[] @@ -180,7 +291,12 @@ function takePreview(text: string, maxLength = 180): string { return `${normalized.slice(0, maxLength - 1).trimEnd()}...` } -function parseSaveSections(content: string): ParsedPluginDocument | null { +function parseSaveSections( + content: string, + plugin: PluginIdentity | null, +): ParsedPluginDocument | null { + if (!plugin) return null + const match = content.match(/\[SAVE:([^\]]+)\]([\s\S]*?)\[\/SAVE\]/i) if (!match) return null @@ -238,12 +354,13 @@ function parseSaveSections(content: string): ParsedPluginDocument | null { "Saved project note" return { - kind: "codex-save", - pluginLabel: "Codex", + kind: plugin.pluginId === "codex" ? "codex-save" : "plugin-save", + pluginLabel: plugin.label, + pluginIconSrc: plugin.iconSrc ?? undefined, formatLabel: "Saved note", title: "Saved memory note", preview: takePreview( - sections[0]?.value ?? "Saved project knowledge from Codex", + sections[0]?.value ?? `Saved project knowledge from ${plugin.label}`, 140, ), summary: takePreview(summary, 220), @@ -259,9 +376,10 @@ function parseSaveSections(content: string): ParsedPluginDocument | null { function parseSessionTranscript( content: string, config: { - kind: "codex-session" | "amp-thread" + kind: "codex-session" | "amp-thread" | "plugin-session" headerLabel: "Session" | "Amp thread" pluginLabel: string + pluginIconSrc?: string | null formatLabel: string }, ): ParsedPluginDocument | null { @@ -289,6 +407,7 @@ function parseSessionTranscript( return { kind: config.kind, pluginLabel: config.pluginLabel, + pluginIconSrc: config.pluginIconSrc ?? undefined, formatLabel: config.formatLabel, title: `${config.pluginLabel} conversation`, preview: takePreview(previewSource, 140), @@ -302,7 +421,17 @@ function parseSessionTranscript( } } -function parseOpenClawTranscript(content: string): ParsedPluginDocument | null { +function parseRoleBlockTranscript( + content: string, + plugin: PluginIdentity | null, +): ParsedPluginDocument | null { + if ( + !plugin || + (plugin.pluginId !== "openclaw" && plugin.pluginId !== "hermes") + ) { + return null + } + const { messages, artifacts } = parseRoleBlockMessages(content) if (messages.length === 0) return null @@ -312,12 +441,14 @@ function parseOpenClawTranscript(content: string): ParsedPluginDocument | null { "Conversation" return { - kind: "openclaw-session", - pluginLabel: "OpenClaw", + kind: + plugin.pluginId === "openclaw" ? "openclaw-session" : "plugin-session", + pluginLabel: plugin.label, + pluginIconSrc: plugin.iconSrc ?? undefined, formatLabel: "Conversation", - title: "OpenClaw conversation", + title: `${plugin.label} conversation`, preview: takePreview(previewSource, 140), - summary: `${messages.length} message${messages.length === 1 ? "" : "s"} captured from OpenClaw.`, + summary: `${messages.length} message${messages.length === 1 ? "" : "s"} captured from ${plugin.label}.`, artifacts, messages, sections: [], @@ -325,6 +456,36 @@ function parseOpenClawTranscript(content: string): ParsedPluginDocument | null { } } +function parsePluginSpaceNote( + document: DocumentWithMemories, + content: string, + plugin: PluginIdentity | null, +): ParsedPluginDocument | null { + if (!plugin) return null + + const title = + (typeof document.title === "string" && document.title.trim()) || + `${plugin.label} memory` + const summary = + typeof document.summary === "string" ? document.summary.trim() : "" + const memoryText = firstMemoryEntryText(document) + const preview = takePreview(memoryText || summary || content, 220) + + return { + kind: plugin.pluginId === "codex" ? "codex-save" : "plugin-save", + pluginLabel: plugin.label, + pluginIconSrc: plugin.iconSrc ?? undefined, + formatLabel: "Note", + title, + preview, + summary: takePreview(summary || memoryText || content, 220), + artifacts: [], + messages: [], + sections: [], + rawContent: content, + } +} + function withIcon( parsed: ParsedPluginDocument | null, ): ParsedPluginDocument | null { @@ -417,7 +578,8 @@ export function claudeCodeTokenBadge( document: DocumentWithMemories, ): string | null { const meta = (document.metadata ?? {}) as Record - if (getDocumentPluginSource(document, meta) !== "claude-code-plugin") { + const plugin = getDocumentPluginIdentity(document, meta) + if (plugin?.pluginId !== "claude-code") { return null } const tokens = document.tokenCount @@ -431,10 +593,26 @@ function parseClaudeCodeByMetadata( ): ParsedPluginDocument | null { const docSource = getDocumentPluginSource(document, metadata) let source = detectPluginSource(metadata, docSource) + const plugin = getDocumentPluginIdentity(document, metadata) const rawContent = typeof document.content === "string" ? document.content : "" + if (!source && plugin?.pluginId === "claude-code") { + const project = + typeof metadata.project === "string" && metadata.project.trim() + ? metadata.project.trim() + : plugin.projectId + source = { + pluginId: "claude-code", + label: "Claude Code", + iconSrc: "/images/plugins/claude-code.svg", + projectName: project, + formatLabel: "Session", + type: "session_turn", + } + } + if (!source) { if (CLAUDE_CODE_CONTENT_RE.test(rawContent)) { const md = metadata ?? {} @@ -488,10 +666,7 @@ export function parsePluginDocument( if (!document) return null const metadata = (document.metadata ?? {}) as Record - - if (getDocumentPluginSource(document, metadata) === "claude-code-plugin") { - return withIcon(parseClaudeCodeByMetadata(document, metadata)) - } + const plugin = getDocumentPluginIdentity(document, metadata) const content = normalizeContent( typeof document.content === "string" ? document.content : "", @@ -504,39 +679,58 @@ export function parsePluginDocument( ) if (content) { - const codexSave = parseSaveSections(content) - if (codexSave) { + const saveDoc = parseSaveSections(content, plugin) + if (saveDoc) { if (clientName) { - codexSave.clientLabel = "Client" - codexSave.clientValue = clientName + saveDoc.clientLabel = "Client" + saveDoc.clientValue = clientName } - return withIcon(codexSave) + return withIcon(saveDoc) } - const codexSession = parseSessionTranscript(content, { - kind: "codex-session", - headerLabel: "Session", - pluginLabel: "Codex", - formatLabel: "Conversation", - }) - if (codexSession) { - if (clientName) { - codexSession.clientLabel = "Client" - codexSession.clientValue = clientName - } - return withIcon(codexSession) + if (plugin?.pluginId === "claude-code") { + return withIcon(parseClaudeCodeByMetadata(document, metadata)) } - const ampThread = parseSessionTranscript(content, { - kind: "amp-thread", - headerLabel: "Amp thread", - pluginLabel: "Amp", - formatLabel: "Conversation", - }) - if (ampThread) return withIcon(ampThread) + if (plugin?.pluginId === "codex") { + const codexSession = parseSessionTranscript(content, { + kind: "codex-session", + headerLabel: "Session", + pluginLabel: plugin.label, + pluginIconSrc: plugin.iconSrc, + formatLabel: "Conversation", + }) + if (codexSession) { + if (clientName) { + codexSession.clientLabel = "Client" + codexSession.clientValue = clientName + } + return withIcon(codexSession) + } + } - const openClawSession = parseOpenClawTranscript(content) - if (openClawSession) return withIcon(openClawSession) + if (plugin?.pluginId === "amp") { + const ampThread = parseSessionTranscript(content, { + kind: "amp-thread", + headerLabel: "Amp thread", + pluginLabel: plugin.label, + pluginIconSrc: plugin.iconSrc, + formatLabel: "Conversation", + }) + if (ampThread) return withIcon(ampThread) + } + + const roleBlockSession = parseRoleBlockTranscript(content, plugin) + if (roleBlockSession) return withIcon(roleBlockSession) + } + + const pluginSpaceNote = parsePluginSpaceNote(document, content, plugin) + if (pluginSpaceNote) { + if (clientName) { + pluginSpaceNote.clientLabel = "Client" + pluginSpaceNote.clientValue = clientName + } + return withIcon(pluginSpaceNote) } return withIcon(parseClaudeCodeByMetadata(document, metadata)) diff --git a/apps/web/lib/plugin-space.ts b/apps/web/lib/plugin-space.ts index b42a2fdc..a4e4d7df 100644 --- a/apps/web/lib/plugin-space.ts +++ b/apps/web/lib/plugin-space.ts @@ -1,7 +1,7 @@ import { normalizePluginClientId } from "@/lib/plugin-catalog" export type PluginSpaceInfo = { - pluginId: "claude-code" | "openclaw" | "opencode" | "codex" | "amp" + pluginId: "claude-code" | "openclaw" | "opencode" | "codex" | "amp" | "hermes" label: string iconSrc: string | null projectId?: string @@ -45,6 +45,12 @@ const PLUGINS: PluginDef[] = [ iconSrc: null, prefixes: ["amp"], }, + { + id: "hermes", + label: "Hermes", + iconSrc: "/images/plugins/hermes.svg", + prefixes: ["hermes"], + }, ] function parsePluginRest(rest: string): { projectId?: string } { @@ -65,6 +71,7 @@ const PLUGIN_ICON_BY_LABEL: Record = { OpenClaw: "/images/plugins/openclaw.svg", OpenCode: "/images/plugins/opencode.svg", Codex: "/images/plugins/codex.png", + Hermes: "/images/plugins/hermes.svg", } export function pluginIconByLabel( @@ -112,8 +119,8 @@ export function detectPluginSource( metadata && typeof metadata.sm_source === "string" ? metadata.sm_source : null - const source = documentSource ?? sourceFromMeta - if (source !== "claude-code-plugin") return null + const source = (documentSource ?? sourceFromMeta)?.trim().toLowerCase() + if (source !== "claude-code-plugin" && source !== "claude-code") return null const md = metadata ?? {} const project =