slidev-addon-agent 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -0
  3. package/agent/constants.ts +119 -0
  4. package/agent/deck-context.ts +67 -0
  5. package/agent/index.ts +201 -0
  6. package/agent/middleware.ts +163 -0
  7. package/agent/skills/slidev/README.md +61 -0
  8. package/agent/skills/slidev/SKILL.md +189 -0
  9. package/agent/skills/slidev/references/animation-click-marker.md +37 -0
  10. package/agent/skills/slidev/references/animation-drawing.md +68 -0
  11. package/agent/skills/slidev/references/animation-rough-marker.md +53 -0
  12. package/agent/skills/slidev/references/api-slide-hooks.md +37 -0
  13. package/agent/skills/slidev/references/build-og-image.md +36 -0
  14. package/agent/skills/slidev/references/build-pdf.md +40 -0
  15. package/agent/skills/slidev/references/build-remote-assets.md +34 -0
  16. package/agent/skills/slidev/references/build-seo-meta.md +43 -0
  17. package/agent/skills/slidev/references/code-groups.md +64 -0
  18. package/agent/skills/slidev/references/code-import-snippet.md +55 -0
  19. package/agent/skills/slidev/references/code-line-highlighting.md +50 -0
  20. package/agent/skills/slidev/references/code-line-numbers.md +46 -0
  21. package/agent/skills/slidev/references/code-magic-move.md +57 -0
  22. package/agent/skills/slidev/references/code-max-height.md +37 -0
  23. package/agent/skills/slidev/references/code-twoslash.md +42 -0
  24. package/agent/skills/slidev/references/core-animations.md +196 -0
  25. package/agent/skills/slidev/references/core-cli.md +140 -0
  26. package/agent/skills/slidev/references/core-components.md +197 -0
  27. package/agent/skills/slidev/references/core-exporting.md +148 -0
  28. package/agent/skills/slidev/references/core-frontmatter.md +195 -0
  29. package/agent/skills/slidev/references/core-global-context.md +155 -0
  30. package/agent/skills/slidev/references/core-headmatter.md +188 -0
  31. package/agent/skills/slidev/references/core-hosting.md +152 -0
  32. package/agent/skills/slidev/references/core-layouts.md +286 -0
  33. package/agent/skills/slidev/references/core-syntax.md +155 -0
  34. package/agent/skills/slidev/references/diagram-latex.md +55 -0
  35. package/agent/skills/slidev/references/diagram-mermaid.md +44 -0
  36. package/agent/skills/slidev/references/diagram-plantuml.md +45 -0
  37. package/agent/skills/slidev/references/editor-monaco-run.md +44 -0
  38. package/agent/skills/slidev/references/editor-monaco-write.md +24 -0
  39. package/agent/skills/slidev/references/editor-monaco.md +50 -0
  40. package/agent/skills/slidev/references/editor-prettier.md +40 -0
  41. package/agent/skills/slidev/references/editor-side.md +23 -0
  42. package/agent/skills/slidev/references/editor-vscode.md +55 -0
  43. package/agent/skills/slidev/references/layout-canvas-size.md +25 -0
  44. package/agent/skills/slidev/references/layout-draggable.md +57 -0
  45. package/agent/skills/slidev/references/layout-global-layers.md +50 -0
  46. package/agent/skills/slidev/references/layout-slots.md +75 -0
  47. package/agent/skills/slidev/references/layout-transform.md +33 -0
  48. package/agent/skills/slidev/references/layout-zoom.md +39 -0
  49. package/agent/skills/slidev/references/presenter-notes-ruby.md +35 -0
  50. package/agent/skills/slidev/references/presenter-recording.md +30 -0
  51. package/agent/skills/slidev/references/presenter-remote.md +40 -0
  52. package/agent/skills/slidev/references/presenter-timer.md +34 -0
  53. package/agent/skills/slidev/references/style-direction.md +34 -0
  54. package/agent/skills/slidev/references/style-icons.md +46 -0
  55. package/agent/skills/slidev/references/style-scoped.md +50 -0
  56. package/agent/skills/slidev/references/syntax-block-frontmatter.md +39 -0
  57. package/agent/skills/slidev/references/syntax-frontmatter-merging.md +49 -0
  58. package/agent/skills/slidev/references/syntax-importing-slides.md +60 -0
  59. package/agent/skills/slidev/references/syntax-mdc.md +51 -0
  60. package/agent/skills/slidev/references/tool-eject-theme.md +27 -0
  61. package/agent/tools/export-tool.ts +216 -0
  62. package/agent/tools/review-tool.ts +136 -0
  63. package/app/index.ts +124 -0
  64. package/components/MessageItem.vue +231 -0
  65. package/components/SlidevAgentNavButton.vue +48 -0
  66. package/components/SlidevAgentSidebar.vue +766 -0
  67. package/components/SubagentCard.vue +184 -0
  68. package/components/TypingDots.vue +62 -0
  69. package/dist/agent/constants.js +117 -0
  70. package/dist/agent/deck-context.js +47 -0
  71. package/dist/agent/index.js +167 -0
  72. package/dist/agent/middleware.js +134 -0
  73. package/dist/agent/slide-preview-tool.js +257 -0
  74. package/dist/agent/tools/export-tool.js +167 -0
  75. package/dist/agent/tools/review-tool.js +111 -0
  76. package/dist/app/index.js +101 -0
  77. package/dist/bin/slidev-agent.js +155 -0
  78. package/dist/lib/bridge.js +151 -0
  79. package/dist/lib/env.js +17 -0
  80. package/dist/lib/headless-tools.js +10 -0
  81. package/dist/lib/langgraph-init.js +59 -0
  82. package/dist/lib/review-tool.js +98 -0
  83. package/lib/bridge.ts +212 -0
  84. package/lib/config.ts +79 -0
  85. package/lib/env.ts +38 -0
  86. package/lib/headless-tool-impl.ts +26 -0
  87. package/lib/headless-tools.ts +11 -0
  88. package/lib/langgraph-init.ts +79 -0
  89. package/lib/messages.ts +169 -0
  90. package/lib/render-chat-markdown.ts +19 -0
  91. package/lib/sidebar.ts +573 -0
  92. package/lib/state.ts +44 -0
  93. package/package.json +65 -0
  94. package/public/deepagents.svg +12 -0
package/lib/bridge.ts ADDED
@@ -0,0 +1,212 @@
1
+ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+
4
+ import { env } from "./env.js"
5
+
6
+ const GENERATED_ROOT = ".slidev-agent/generated"
7
+ const MANIFEST_PATH = ".slidev-agent/manifest.json"
8
+
9
+ type BridgeConfig = {
10
+ apiUrl?: string
11
+ deckId?: string
12
+ namespace?: string
13
+ entry: string
14
+ routePrefix: string
15
+ cwd: string
16
+ }
17
+
18
+ type DeckFile = {
19
+ path: string
20
+ content: string
21
+ }
22
+
23
+ type NormalizedPayload = {
24
+ entry: string
25
+ files: DeckFile[]
26
+ }
27
+
28
+ type BridgeManifest = {
29
+ entry: string
30
+ generatedRoot: string
31
+ files: string[]
32
+ }
33
+
34
+ function resolveBridgeConfig(cwd: string): BridgeConfig {
35
+ return {
36
+ entry: env(process.env, "SLIDEV_AGENT_ENTRY", "slides.md"),
37
+ routePrefix: env(process.env, "SLIDEV_AGENT_ROUTE_PREFIX", "/slidev-agent"),
38
+ cwd,
39
+ }
40
+ }
41
+
42
+ function isRemoteBridgeEnabled(config: BridgeConfig) {
43
+ return Boolean(config.apiUrl && config.deckId)
44
+ }
45
+
46
+ function normalizeRoutePrefix(routePrefix: string) {
47
+ if (!routePrefix.startsWith("/"))
48
+ return `/${routePrefix}`
49
+
50
+ return routePrefix.replace(/\/$/, "")
51
+ }
52
+
53
+ function createDeckUrl(config: BridgeConfig) {
54
+ if (!config.apiUrl || !config.deckId)
55
+ return undefined
56
+
57
+ const prefix = normalizeRoutePrefix(config.routePrefix)
58
+ const url = new URL(`${prefix}/decks/${encodeURIComponent(config.deckId)}`, config.apiUrl)
59
+
60
+ if (config.namespace)
61
+ url.searchParams.set("namespace", config.namespace)
62
+
63
+ return url
64
+ }
65
+
66
+ function normalizePayload(payload: any, fallbackEntry: string): NormalizedPayload {
67
+ const entry = typeof payload.entry === "string" && payload.entry
68
+ ? payload.entry
69
+ : fallbackEntry
70
+
71
+ if (Array.isArray(payload.files)) {
72
+ return {
73
+ entry,
74
+ files: payload.files.map((file: any) => ({
75
+ path: file.path,
76
+ content: file.content,
77
+ })),
78
+ }
79
+ }
80
+
81
+ if (payload.slides && typeof payload.slides === "object") {
82
+ return {
83
+ entry,
84
+ files: Object.entries(payload.slides).map(([filePath, content]) => ({
85
+ path: filePath,
86
+ content: String(content),
87
+ })),
88
+ }
89
+ }
90
+
91
+ throw new Error("Unsupported response shape. Expected `files` or `slides` in the bridge response.")
92
+ }
93
+
94
+ async function ensureParentDirectory(targetFile: string) {
95
+ await mkdir(path.dirname(targetFile), { recursive: true })
96
+ }
97
+
98
+ async function writeManifest(cwd: string, manifest: BridgeManifest) {
99
+ const manifestFile = path.join(cwd, MANIFEST_PATH)
100
+ await ensureParentDirectory(manifestFile)
101
+ await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
102
+ }
103
+
104
+ async function readManifest(cwd: string): Promise<BridgeManifest> {
105
+ const manifestFile = path.join(cwd, MANIFEST_PATH)
106
+ const content = await readFile(manifestFile, "utf8")
107
+ return JSON.parse(content) as BridgeManifest
108
+ }
109
+
110
+ export async function pullRemoteSlides(cwd = process.cwd()) {
111
+ const config = resolveBridgeConfig(cwd)
112
+ if (!isRemoteBridgeEnabled(config)) {
113
+ return {
114
+ mode: "local" as const,
115
+ entry: path.join(cwd, config.entry),
116
+ manifest: null,
117
+ }
118
+ }
119
+
120
+ const deckUrl = createDeckUrl(config)
121
+ if (!deckUrl) {
122
+ throw new Error("Remote sync is not configured. Set SLIDEV_AGENT_API_URL and SLIDEV_AGENT_DECK_ID.")
123
+ }
124
+
125
+ const response = await fetch(deckUrl, {
126
+ headers: {
127
+ accept: "application/json",
128
+ },
129
+ })
130
+
131
+ if (!response.ok)
132
+ throw new Error(`Bridge pull failed with ${response.status} ${response.statusText}`)
133
+
134
+ const payload = normalizePayload(await response.json(), config.entry)
135
+ const generatedRoot = path.join(cwd, GENERATED_ROOT)
136
+
137
+ await rm(generatedRoot, { recursive: true, force: true })
138
+ await mkdir(generatedRoot, { recursive: true })
139
+
140
+ for (const file of payload.files) {
141
+ const outputFile = path.join(generatedRoot, file.path)
142
+ await ensureParentDirectory(outputFile)
143
+ await writeFile(outputFile, file.content, "utf8")
144
+ }
145
+
146
+ const manifest: BridgeManifest = {
147
+ entry: payload.entry,
148
+ generatedRoot: GENERATED_ROOT,
149
+ files: payload.files.map(file => file.path),
150
+ }
151
+
152
+ await writeManifest(cwd, manifest)
153
+
154
+ return {
155
+ mode: "remote" as const,
156
+ entry: path.join(generatedRoot, payload.entry),
157
+ manifest,
158
+ }
159
+ }
160
+
161
+ export async function pushRemoteSlides(cwd = process.cwd()) {
162
+ const config = resolveBridgeConfig(cwd)
163
+ if (!isRemoteBridgeEnabled(config))
164
+ throw new Error("Remote sync is not configured. Set SLIDEV_AGENT_API_URL and SLIDEV_AGENT_DECK_ID.")
165
+
166
+ const manifest = await readManifest(cwd)
167
+ const generatedRoot = path.join(cwd, manifest.generatedRoot)
168
+
169
+ const files: DeckFile[] = []
170
+ for (const filePath of manifest.files) {
171
+ const absoluteFile = path.join(generatedRoot, filePath)
172
+ files.push({
173
+ path: filePath,
174
+ content: await readFile(absoluteFile, "utf8"),
175
+ })
176
+ }
177
+
178
+ const deckUrl = createDeckUrl(config)
179
+ if (!deckUrl) {
180
+ throw new Error("Remote sync is not configured. Set SLIDEV_AGENT_API_URL and SLIDEV_AGENT_DECK_ID.")
181
+ }
182
+
183
+ const response = await fetch(deckUrl, {
184
+ method: "PUT",
185
+ headers: {
186
+ "content-type": "application/json",
187
+ accept: "application/json",
188
+ },
189
+ body: JSON.stringify({
190
+ entry: manifest.entry,
191
+ files,
192
+ }),
193
+ })
194
+
195
+ if (!response.ok)
196
+ throw new Error(`Bridge push failed with ${response.status} ${response.statusText}`)
197
+ }
198
+
199
+ export async function resolveSlideEntry(cwd = process.cwd()) {
200
+ const config = resolveBridgeConfig(cwd)
201
+ if (isRemoteBridgeEnabled(config))
202
+ return pullRemoteSlides(cwd)
203
+
204
+ const entryFile = path.join(cwd, config.entry)
205
+ await stat(entryFile)
206
+
207
+ return {
208
+ mode: "local" as const,
209
+ entry: entryFile,
210
+ manifest: null,
211
+ }
212
+ }
package/lib/config.ts ADDED
@@ -0,0 +1,79 @@
1
+ export interface SlidevAgentRuntimeConfig {
2
+ apiUrl: string
3
+ assistantId: string
4
+ deckId: string
5
+ threadId: string | null
6
+ inputPlaceholder: string
7
+ enabled: boolean
8
+ }
9
+
10
+ import { env } from "./env"
11
+
12
+ interface SlidevAgentInjectedConfig {
13
+ apiUrl?: string
14
+ assistantId?: string
15
+ }
16
+
17
+ function readInjectedConfig(): SlidevAgentInjectedConfig {
18
+ if (typeof window === "undefined")
19
+ return {}
20
+
21
+ const injectedConfig = Reflect.get(window, "__SLIDEV_AGENT_CONFIG__")
22
+ if (!injectedConfig || typeof injectedConfig !== "object")
23
+ return {}
24
+
25
+ return injectedConfig as SlidevAgentInjectedConfig
26
+ }
27
+
28
+ function getThreadStorageKey(assistantId: string, deckId: string) {
29
+ return `slidev-agent:thread:${assistantId || "default"}:${deckId || "default"}`
30
+ }
31
+
32
+ function readStoredThreadId(assistantId: string, deckId: string) {
33
+ if (typeof window === "undefined")
34
+ return null
35
+
36
+ const storageKey = getThreadStorageKey(assistantId, deckId)
37
+ const current = window.sessionStorage.getItem(storageKey)
38
+ const legacyCurrent = window.localStorage.getItem(storageKey)
39
+
40
+ if (legacyCurrent) {
41
+ window.localStorage.removeItem(storageKey)
42
+ }
43
+
44
+ return current?.trim() || null
45
+ }
46
+
47
+ export function persistSlidevAgentThreadId(assistantId: string, deckId: string, threadId: string) {
48
+ if (typeof window === "undefined" || !threadId.trim())
49
+ return
50
+
51
+ window.sessionStorage.setItem(getThreadStorageKey(assistantId, deckId), threadId)
52
+ }
53
+
54
+ export function clearSlidevAgentThreadId(assistantId: string, deckId: string) {
55
+ if (typeof window === "undefined")
56
+ return
57
+
58
+ const storageKey = getThreadStorageKey(assistantId, deckId)
59
+ window.sessionStorage.removeItem(storageKey)
60
+ window.localStorage.removeItem(storageKey)
61
+ }
62
+
63
+ export function resolveSlidevAgentRuntimeConfig(): SlidevAgentRuntimeConfig {
64
+ const injectedConfig = readInjectedConfig()
65
+ const apiUrl = env(import.meta.env, "VITE_LANGGRAPH_API_URL") || injectedConfig.apiUrl || "http://localhost:2024"
66
+ const assistantId = env(import.meta.env, "VITE_LANGGRAPH_ASSISTANT_ID") || injectedConfig.assistantId || ""
67
+ const deckId = env(import.meta.env, "VITE_SLIDEV_AGENT_DECK_ID") || "default-deck"
68
+ const inputPlaceholder = env(import.meta.env, "VITE_SLIDEV_AGENT_PLACEHOLDER") || "Ask the agent to create or revise slides..."
69
+ const threadId = env(import.meta.env, "VITE_LANGGRAPH_THREAD_ID") || readStoredThreadId(assistantId, deckId)
70
+
71
+ return {
72
+ apiUrl,
73
+ assistantId,
74
+ deckId,
75
+ threadId,
76
+ inputPlaceholder,
77
+ enabled: Boolean(apiUrl && assistantId),
78
+ }
79
+ }
package/lib/env.ts ADDED
@@ -0,0 +1,38 @@
1
+ export function env(
2
+ source: Record<string, unknown>,
3
+ name: string,
4
+ ): string | undefined
5
+ export function env<Fallback extends string>(
6
+ source: Record<string, unknown>,
7
+ name: string,
8
+ fallback: Fallback,
9
+ ): string | Fallback
10
+ export function env(
11
+ source: Record<string, unknown>,
12
+ name: string,
13
+ fallback?: string,
14
+ ): string | undefined {
15
+ const value = source[name]
16
+ return typeof value === "string" && value.trim() ? value.trim() : fallback
17
+ }
18
+
19
+ type ImportMetaEnv = ImportMeta & {
20
+ env?: Record<string, string | undefined>
21
+ }
22
+
23
+ const globalEnv = "process" in globalThis
24
+ ? globalThis.process.env
25
+ : (import.meta as ImportMetaEnv).env ?? {}
26
+ const anthropicEnv = env(globalEnv, "ANTHROPIC_API_KEY")
27
+ const googleEnv = env(globalEnv, "GOOGLE_API_KEY")
28
+ const openaiEnv = env(globalEnv, "OPENAI_API_KEY")
29
+
30
+ export const model = env(globalEnv, "SLIDEV_AGENT_MODEL") ?? (
31
+ anthropicEnv
32
+ ? "anthropic:claude-sonnet-4-6"
33
+ : googleEnv
34
+ ? "google:gemini-2.5-flash"
35
+ : openaiEnv
36
+ ? "openai:gpt-5.4"
37
+ : undefined
38
+ )
@@ -0,0 +1,26 @@
1
+ import { nextTick, unref } from "vue"
2
+
3
+ import { slidevGoToSlide as slidevGoToSlideDefinition } from "./headless-tools"
4
+
5
+ type SlidevNavAdapter = {
6
+ go: (page: number) => void | Promise<void>
7
+ currentPage: unknown
8
+ }
9
+
10
+ export function createSlidevHeadlessTools(nav: SlidevNavAdapter) {
11
+ const slidevGoToSlide = slidevGoToSlideDefinition.implement(async ({ page, reason }) => {
12
+ await Promise.resolve(nav.go(page))
13
+ await nextTick()
14
+
15
+ const currentPage = Number(unref(nav.currentPage)) || page
16
+
17
+ return {
18
+ success: currentPage === page,
19
+ page,
20
+ currentPage,
21
+ reason: reason || undefined,
22
+ }
23
+ })
24
+
25
+ return [slidevGoToSlide]
26
+ }
@@ -0,0 +1,11 @@
1
+ import { tool } from "langchain"
2
+ import { z } from "zod"
3
+
4
+ export const slidevGoToSlide = tool({
5
+ name: "slidev_go_to_slide",
6
+ description: "Navigate the active Slidev presentation in the user's browser to a specific 1-based slide number. Use this after creating a new slide when you know its final index.",
7
+ schema: z.object({
8
+ page: z.number().int().positive().describe("The 1-based Slidev page number to open."),
9
+ reason: z.string().optional().describe("Optional short explanation for the navigation."),
10
+ }),
11
+ })
@@ -0,0 +1,79 @@
1
+ import { existsSync } from "node:fs"
2
+ import { writeFileSync } from "node:fs"
3
+ import { createRequire } from "node:module"
4
+ import path from "node:path"
5
+
6
+ export interface LanggraphJsonShape {
7
+ node_version: string
8
+ dependencies: string[]
9
+ graphs: Record<string, string>
10
+ http: { app: string }
11
+ env: string
12
+ }
13
+
14
+ function posixRelativeToCwd(cwd: string, absoluteFile: string): string {
15
+ const rel = path.relative(cwd, absoluteFile).replace(/\\/g, "/")
16
+ if (!rel)
17
+ return "."
18
+ return rel.startsWith(".") ? rel : `./${rel}`
19
+ }
20
+
21
+ /**
22
+ * Resolves installed `slidev-addon-agent` entry files and returns graph spec strings
23
+ * for langgraph.json (path + export name).
24
+ */
25
+ export function resolveSlidevAddonGraphSpecs(cwd: string): { agent: string; app: string } {
26
+ const packageJsonPath = path.join(cwd, "package.json")
27
+ if (!existsSync(packageJsonPath)) {
28
+ throw new Error(
29
+ `No package.json in ${cwd}. Run slidev-agent dev from your Slidev project root (where package.json lives).`,
30
+ )
31
+ }
32
+
33
+ const require = createRequire(packageJsonPath)
34
+ let pkgRoot: string
35
+ try {
36
+ pkgRoot = path.dirname(require.resolve("slidev-addon-agent"))
37
+ }
38
+ catch {
39
+ throw new Error(
40
+ "Could not resolve \"slidev-addon-agent\". Install it in this project, e.g. pnpm add slidev-addon-agent",
41
+ )
42
+ }
43
+
44
+ const agentFile = path.join(pkgRoot, "agent", "index.ts")
45
+ const appFile = path.join(pkgRoot, "app", "index.ts")
46
+
47
+ return {
48
+ agent: `${posixRelativeToCwd(cwd, agentFile)}:agent`,
49
+ app: `${posixRelativeToCwd(cwd, appFile)}:app`,
50
+ }
51
+ }
52
+
53
+ export function buildLanggraphJson(cwd: string): LanggraphJsonShape {
54
+ const { agent, app } = resolveSlidevAddonGraphSpecs(cwd)
55
+ return {
56
+ node_version: "20",
57
+ dependencies: ["."],
58
+ graphs: {
59
+ agent,
60
+ },
61
+ http: {
62
+ app,
63
+ },
64
+ env: ".env",
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Writes `langgraph.json` when it is missing (first `slidev-agent dev`).
70
+ * Does not overwrite an existing file.
71
+ */
72
+ export function writeLanggraphJsonIfMissing(cwd: string): void {
73
+ const outPath = path.join(cwd, "langgraph.json")
74
+ if (existsSync(outPath))
75
+ return
76
+
77
+ const config = buildLanggraphJson(cwd)
78
+ writeFileSync(outPath, `${JSON.stringify(config, null, 2)}\n`, "utf8")
79
+ }
@@ -0,0 +1,169 @@
1
+ function stringifyContentPart(part: unknown): string {
2
+ if (typeof part === "string")
3
+ return part
4
+
5
+ if (part && typeof part === "object") {
6
+ const maybeText = Reflect.get(part, "text")
7
+ if (typeof maybeText === "string")
8
+ return maybeText
9
+
10
+ const maybeType = Reflect.get(part, "type")
11
+ if (maybeType === "tool_call" || maybeType === "tool_result")
12
+ return ""
13
+ }
14
+
15
+ return ""
16
+ }
17
+
18
+ type MessageToolCall = {
19
+ id: string
20
+ name: string
21
+ args?: unknown
22
+ }
23
+
24
+ function parseToolCallLike(value: unknown): MessageToolCall | null {
25
+ if (!value || typeof value !== "object")
26
+ return null
27
+
28
+ const directId = Reflect.get(value, "id")
29
+ const nestedFunction = Reflect.get(value, "function")
30
+ const nestedId = Reflect.get(value, "tool_call_id")
31
+ const directName = Reflect.get(value, "name")
32
+ const nestedName = nestedFunction && typeof nestedFunction === "object"
33
+ ? Reflect.get(nestedFunction, "name")
34
+ : undefined
35
+ const directArgs = Reflect.get(value, "args")
36
+ const nestedArgs = nestedFunction && typeof nestedFunction === "object"
37
+ ? Reflect.get(nestedFunction, "arguments")
38
+ : undefined
39
+
40
+ const id = typeof directId === "string"
41
+ ? directId
42
+ : typeof nestedId === "string"
43
+ ? nestedId
44
+ : ""
45
+ const name = typeof directName === "string"
46
+ ? directName
47
+ : typeof nestedName === "string"
48
+ ? nestedName
49
+ : ""
50
+ const args = directArgs ?? nestedArgs
51
+
52
+ if (!id && !name)
53
+ return null
54
+
55
+ return { id, name, args }
56
+ }
57
+
58
+ export function getMessageRole(message: unknown): string {
59
+ if (message && typeof message === "object") {
60
+ const getType = Reflect.get(message, "getType")
61
+ if (typeof getType === "function")
62
+ return String(getType.call(message))
63
+
64
+ const type = Reflect.get(message, "type")
65
+ if (typeof type === "string")
66
+ return type
67
+ }
68
+
69
+ return "assistant"
70
+ }
71
+
72
+ export function getMessageContent(message: unknown): string {
73
+ if (!message || typeof message !== "object")
74
+ return ""
75
+
76
+ const content = Reflect.get(message, "content")
77
+ if (typeof content === "string")
78
+ return content
79
+
80
+ if (Array.isArray(content))
81
+ return content.map(stringifyContentPart).filter(Boolean).join("\n\n")
82
+
83
+ return ""
84
+ }
85
+
86
+ export function getMessageToolCalls(message: unknown): MessageToolCall[] {
87
+ if (!message || typeof message !== "object")
88
+ return []
89
+
90
+ const toolCalls: MessageToolCall[] = []
91
+ const seen = new Set<string>()
92
+ const content = Reflect.get(message, "content")
93
+
94
+ if (Array.isArray(content)) {
95
+ content.forEach((part) => {
96
+ if (!part || typeof part !== "object" || Reflect.get(part, "type") !== "tool_call")
97
+ return
98
+
99
+ const toolCall = parseToolCallLike(part)
100
+ if (!toolCall)
101
+ return
102
+
103
+ const key = toolCall.id || `${toolCall.name}:${JSON.stringify(toolCall.args ?? "")}`
104
+ if (seen.has(key))
105
+ return
106
+
107
+ seen.add(key)
108
+ toolCalls.push(toolCall)
109
+ })
110
+ }
111
+
112
+ const kwargs = Reflect.get(message, "additional_kwargs")
113
+ const nestedToolCalls = kwargs && typeof kwargs === "object"
114
+ ? Reflect.get(kwargs, "tool_calls")
115
+ : undefined
116
+
117
+ if (Array.isArray(nestedToolCalls)) {
118
+ nestedToolCalls.forEach((entry) => {
119
+ const toolCall = parseToolCallLike(entry)
120
+ if (!toolCall)
121
+ return
122
+
123
+ const key = toolCall.id || `${toolCall.name}:${JSON.stringify(toolCall.args ?? "")}`
124
+ if (seen.has(key))
125
+ return
126
+
127
+ seen.add(key)
128
+ toolCalls.push(toolCall)
129
+ })
130
+ }
131
+
132
+ return toolCalls
133
+ }
134
+
135
+ export function getMessageToolName(message: unknown): string {
136
+ if (!message || typeof message !== "object")
137
+ return ""
138
+
139
+ const directName = Reflect.get(message, "name")
140
+ if (typeof directName === "string")
141
+ return directName
142
+
143
+ const kwargs = Reflect.get(message, "additional_kwargs")
144
+ if (kwargs && typeof kwargs === "object") {
145
+ const nestedName = Reflect.get(kwargs, "name")
146
+ if (typeof nestedName === "string")
147
+ return nestedName
148
+ }
149
+
150
+ return ""
151
+ }
152
+
153
+ export function getMessageToolCallId(message: unknown): string {
154
+ if (!message || typeof message !== "object")
155
+ return ""
156
+
157
+ const directId = Reflect.get(message, "tool_call_id")
158
+ if (typeof directId === "string")
159
+ return directId
160
+
161
+ const kwargs = Reflect.get(message, "additional_kwargs")
162
+ if (kwargs && typeof kwargs === "object") {
163
+ const nestedId = Reflect.get(kwargs, "tool_call_id")
164
+ if (typeof nestedId === "string")
165
+ return nestedId
166
+ }
167
+
168
+ return ""
169
+ }
@@ -0,0 +1,19 @@
1
+ import DOMPurify from "dompurify"
2
+ import { marked } from "marked"
3
+
4
+ marked.setOptions({
5
+ gfm: true,
6
+ breaks: true,
7
+ })
8
+
9
+ DOMPurify.addHook("afterSanitizeAttributes", (node) => {
10
+ if (node.tagName === "A") {
11
+ node.setAttribute("target", "_blank")
12
+ node.setAttribute("rel", "noopener noreferrer")
13
+ }
14
+ })
15
+
16
+ export function renderChatMarkdown(source: string): string {
17
+ const html = marked.parse(source, { async: false }) as string
18
+ return DOMPurify.sanitize(html)
19
+ }