diff --git a/cmd/agentsview/main.go b/cmd/agentsview/main.go index a534694dfc..9225184504 100644 --- a/cmd/agentsview/main.go +++ b/cmd/agentsview/main.go @@ -1336,6 +1336,7 @@ func openDB(cfg config.Config) (*db.DB, error) { return nil, err } database.SetToolResultImages(cfg.ToolResultImages) + database.SetAssetsDir(filepath.Join(cfg.DataDir, "assets")) if cfg.InstallationID != "" { unowned, err := database.EnsureInstallationIdentity(context.Background(), cfg.InstallationID) if err != nil { diff --git a/cmd/agentsview/sync_worker.go b/cmd/agentsview/sync_worker.go index 438dcdd4a8..da0d5f5942 100644 --- a/cmd/agentsview/sync_worker.go +++ b/cmd/agentsview/sync_worker.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "path/filepath" "github.com/spf13/cobra" "go.kenn.io/agentsview/internal/config" @@ -387,6 +388,7 @@ func workerEngineConfig(cfg config.Config) sync.EngineConfig { Machine: cfg.InstallationID, BlockedResultCategories: cfg.ResultContentBlockedCategories, ToolResultImages: cfg.ToolResultImages, + AssetsDir: filepath.Join(cfg.DataDir, "assets"), ArchiveContent: cfg.ArchiveContent, } } diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 9bff4aa9dc..5c553d4028 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -22,7 +22,7 @@ } ], "security": { - "csp": "default-src 'self' http://127.0.0.1:* http://localhost:*; script-src 'self' http://127.0.0.1:* http://localhost:*; connect-src 'self' http: https: ws: wss:; img-src 'self' data: http://127.0.0.1:* http://localhost:*; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com http://127.0.0.1:* http://localhost:*; font-src 'self' data: https://fonts.gstatic.com http://127.0.0.1:* http://localhost:*; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" + "csp": "default-src 'self' http://127.0.0.1:* http://localhost:*; script-src 'self' http://127.0.0.1:* http://localhost:*; connect-src 'self' http: https: ws: wss:; img-src 'self' data: blob: http://127.0.0.1:* http://localhost:*; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com http://127.0.0.1:* http://localhost:*; font-src 'self' data: https://fonts.gstatic.com http://127.0.0.1:* http://localhost:*; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" } }, "bundle": { diff --git a/docs/commands.md b/docs/commands.md index f8ac594dea..b1b1d7590f 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1638,3 +1638,7 @@ profile or pass them inline: ```bash AGENTSVIEW_DATA_DIR=/tmp/av-test agentsview serve ``` + +### Ingest-time image offload + +Set `tool_result_images = "offload"` to offload supported tool-result images on future ingestion. Run `agentsview db migrate --images` to retry retained inline images after an asset-write failure. The raw `agentsview session export` command continues to stream provider source bytes. See [image storage](/docs/data/#ingest-time-image-offload) for backup and remote-backend limits. diff --git a/docs/configuration.md b/docs/configuration.md index 248fafad56..c3198f3a6b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -195,7 +195,7 @@ zoom_level = 120 | `cursor_admin_user_id` | Optional default Cursor Admin usage filter by member user ID | | `github_token` | Optional saved GitHub token for Gist publishing | | `result_content_blocked_categories` | Tool categories whose result content is not stored (default: `["Read", "Glob"]`). Changes apply to new ingestion and full rebuilds; see [storage maintenance](/docs/data/#storage-maintenance) for existing source-backed sessions. | -| `tool_result_images` | Retain supported inline tool-result image blocks with `"keep"` (default), or store readable `agentsview_image` placeholders with `"drop"`. The setting affects future ingestion and full resyncs; run `db strip --images` for existing rows; also configurable under **Settings > Archive content**, and changes require a daemon restart. | +| `tool_result_images` | Retain supported inline tool-result image blocks with `"keep"` (default), or store readable `agentsview_image` placeholders with `"drop"`, or move supported images to the local asset store with `"offload"`. The setting affects future ingestion and full resyncs; run `db migrate --images` for existing rows; also configurable under **Settings > Archive content**, and changes require a daemon restart. | | `archive_content` | How much of each session the archive stores: `"full"` (default), `"transcripts"`, or `"usage"`; changes require a daemon restart — see [Archive content](#archive-content) | | `host` | Interface the server binds to (default `127.0.0.1`); non-loopback values require `require_auth = true` | | `require_auth` | Require bearer-token authentication for API access | @@ -1660,3 +1660,7 @@ Disable the CLI/web UI update check with any of: The desktop app's auto-updater is controlled separately via `AGENTSVIEW_DESKTOP_AUTOUPDATE=0`. + +### Ingest-time image offload + +`tool_result_images = "offload"` stores supported PNG, JPEG, WebP, and GIF tool-result images in `{dataDir}/assets/` before SQLite publishes their references. Restart the daemon to apply the setting. Failed writes keep inline content, and archives that omit tool content write no assets. See [image storage](/docs/data/#ingest-time-image-offload) for retries, backups, and remote-backend limits. diff --git a/docs/data.md b/docs/data.md index eeb62b80d5..f0b65dce42 100644 --- a/docs/data.md +++ b/docs/data.md @@ -135,3 +135,11 @@ reclaimed bytes separately; its result must not be combined with savings from filtering or a future compression implementation. See [`agentsview db compact`](/docs/commands/#agentsview-db-compact) for the staging space model and interrupted-compaction recovery. + +### Ingest-time image offload + +Set `tool_result_images = "offload"` to move supported inline tool-result PNG, JPEG, WebP, and GIF payloads into `{dataDir}/assets/` during ingestion. Restart the daemon after changing the setting. Each asset write completes before SQLite can commit its `agentsview_image` placeholder and `image_ref`. Unsupported media and malformed data remain inline. Archives that omit tool content write no image assets. `keep` retains inline content; `drop` retains the existing readable placeholder without an asset. + +If an asset write fails, ingestion and copied-session resync keep the original inline content. Retry retained inline payloads with `agentsview db migrate --images` after restoring access to the asset directory. Complete unreferenced objects from a failed database transaction remain available for reuse; the store does not automatically remove them. Back up the asset directory with the archive and copy both to another local serving host. + +PostgreSQL and CockroachDB preserve the placeholder and reference text but cannot resolve the local asset. DuckDB, artifact exports, and the normalized Markdown server session export at `/api/v1/sessions/{id}/md` carry the stored content. The HTML export keeps its existing contract. The raw `agentsview session export` command streams provider source bytes, so its output retains the original inline payloads. diff --git a/docs/session-api.md b/docs/session-api.md index bc8836564b..eefa648d6d 100644 --- a/docs/session-api.md +++ b/docs/session-api.md @@ -1105,3 +1105,7 @@ When [PostgreSQL sync](/docs/pg-sync/) is enabled, the `secret_findings` table, the session-level `secret_leak_count`, and the `--has-secret` filter all mirror to the shared database. Substring and regex content search work the same way against `pg serve`, with the same masking and `--reveal` constraints. + +### Ingest-time image offload + +The normalized Markdown endpoint `/api/v1/sessions/{id}/md` carries stored tool-result content, including `agentsview_image` placeholders and `image_ref` asset references. The HTML export keeps its existing contract. A serving host needs the matching `{dataDir}/assets` directory. PostgreSQL and CockroachDB retain the reference text but cannot resolve local assets. The raw `agentsview session export` command streams provider source bytes and retains their original inline payloads. diff --git a/frontend/e2e/message-content.spec.ts b/frontend/e2e/message-content.spec.ts index 07ce9622b9..2c4a06fe37 100644 --- a/frontend/e2e/message-content.spec.ts +++ b/frontend/e2e/message-content.spec.ts @@ -250,7 +250,9 @@ test.describe("Mixed content rendering", () => { test.describe("retained tool images", () => { test.describe.configure({ timeout: 60_000 }); - test("renders retained, image-only, migrated, and fallback tool images", async ({ page }, testInfo) => { + test("renders retained, image-only, migrated, and fallback tool images", async ({ + page, + }, testInfo) => { const fixturePath = fileURLToPath( new URL("../src/lib/utils/__fixtures__/retained-tool-image-1735.json", import.meta.url), ); @@ -365,12 +367,15 @@ test.describe("retained tool images", () => { } await route.abort(); }); - await page.route("**/api/v1/assets/first", (route) => - route.fulfill({ body: assetBytes, contentType: "image/png" }), - ); - await page.route("**/api/v1/assets/nested/second", (route) => - route.fulfill({ body: assetBytes, contentType: "image/png" }), - ); + await page.addInitScript(() => { + localStorage.setItem("agentsview-auth-token", "test-asset-token"); + }); + for (const assetPath of ["first", "nested%2Fsecond"]) { + await page.route(`**/api/v1/assets/${assetPath}`, async (route) => { + expect(route.request().headers()["authorization"]).toBe("Bearer test-asset-token"); + await route.fulfill({ body: assetBytes, contentType: "image/png" }); + }); + } await page.setViewportSize({ width: 1280, height: 900 }); await page.goto(`/sessions/${sessionId}`); @@ -421,11 +426,15 @@ test.describe("retained tool images", () => { const retainedImage = retainedFormatted.locator("img"); await expect(retainedImage).toHaveCount(1); await expect(retainedImage).toHaveAttribute("src", retainedImageURL!); - await expect.poll(() => retainedImage.evaluate((img: HTMLImageElement) => ({ - complete: img.complete, - naturalWidth: img.naturalWidth, - naturalHeight: img.naturalHeight, - }))).toEqual({ complete: true, naturalWidth: 600, naturalHeight: 600 }); + await expect + .poll(() => + retainedImage.evaluate((img: HTMLImageElement) => ({ + complete: img.complete, + naturalWidth: img.naturalWidth, + naturalHeight: img.naturalHeight, + })), + ) + .toEqual({ complete: true, naturalWidth: 600, naturalHeight: 600 }); const retainedText = await retainedFormatted.textContent(); expect(retainedText).not.toContain("input_image"); expect(retainedText).not.toContain(retainedImageURL!); @@ -447,7 +456,9 @@ test.describe("retained tool images", () => { expect(measurement.imageWidth).toBeLessThanOrEqual(measurement.clientWidth + 2); console.log(`retained layout width=${width}px ${JSON.stringify(measurement)}`); if (width === 1280) { - await retainedFormatted.screenshot({ path: testInfo.outputPath("agentsview-1735-after.png") }); + await retainedFormatted.screenshot({ + path: testInfo.outputPath("agentsview-1735-after.png"), + }); } else { await retainedFormatted.screenshot({ path: testInfo.outputPath( @@ -467,32 +478,36 @@ test.describe("retained tool images", () => { const imageOnlyImage = imageOnly.formatted.locator("img"); await expect(imageOnlyImage).toHaveCount(1); await expect(imageOnlyImage).toHaveAttribute("src", smallPNG); - await expect.poll(() => imageOnlyImage.evaluate((img: HTMLImageElement) => ({ - complete: img.complete, - naturalWidth: img.naturalWidth, - naturalHeight: img.naturalHeight, - }))).toEqual({ complete: true, naturalWidth: 1, naturalHeight: 1 }); + await expect + .poll(() => + imageOnlyImage.evaluate((img: HTMLImageElement) => ({ + complete: img.complete, + naturalWidth: img.naturalWidth, + naturalHeight: img.naturalHeight, + })), + ) + .toEqual({ complete: true, naturalWidth: 1, naturalHeight: 1 }); expect(await imageOnly.formatted.textContent()).not.toContain("input_image"); const migrated = await openFormatted(migratedBlock); const migratedImages = migrated.formatted.locator("img"); await expect(migratedImages).toHaveCount(2); - const migratedAssetBase = "/api/v1/assets"; - await expect(migratedImages.nth(0)).toHaveAttribute("src", `${migratedAssetBase}/first`); - await expect(migratedImages.nth(1)).toHaveAttribute( - "src", - `${migratedAssetBase}/nested/second`, - ); - await expect.poll(() => migratedImages.evaluateAll((images) => - images.map((image) => ({ - complete: (image as HTMLImageElement).complete, - naturalWidth: (image as HTMLImageElement).naturalWidth, - naturalHeight: (image as HTMLImageElement).naturalHeight, - })), - )).toEqual([ - { complete: true, naturalWidth: 1, naturalHeight: 1 }, - { complete: true, naturalWidth: 1, naturalHeight: 1 }, - ]); + await expect(migratedImages.nth(0)).toHaveAttribute("src", /^blob:/); + await expect(migratedImages.nth(1)).toHaveAttribute("src", /^blob:/); + await expect + .poll(() => + migratedImages.evaluateAll((images) => + images.map((image) => ({ + complete: (image as HTMLImageElement).complete, + naturalWidth: (image as HTMLImageElement).naturalWidth, + naturalHeight: (image as HTMLImageElement).naturalHeight, + })), + ), + ) + .toEqual([ + { complete: true, naturalWidth: 1, naturalHeight: 1 }, + { complete: true, naturalWidth: 1, naturalHeight: 1 }, + ]); expect(await migrated.formatted.textContent()).toContain("Before"); expect(await migrated.formatted.textContent()).toContain("After"); expect(await migrated.formatted.textContent()).not.toContain("asset://"); diff --git a/frontend/e2e/settings-archive-content.spec.ts b/frontend/e2e/settings-archive-content.spec.ts index c247c0afb7..027cc9fae2 100644 --- a/frontend/e2e/settings-archive-content.spec.ts +++ b/frontend/e2e/settings-archive-content.spec.ts @@ -59,6 +59,10 @@ test.describe("Settings archive content", () => { const dropRadio = page.getByRole("radio", { name: "Drop" }); await expect(keepRadio).toHaveAttribute("aria-checked", "true"); await expect(dropRadio).toHaveAttribute("aria-checked", "false"); + await expect(page.getByRole("radio", { name: "Offload" })).toHaveAttribute( + "aria-checked", + "false", + ); await expect(page.locator(".archive-content-settings").getByRole("status")).toHaveCount(0); }); @@ -96,15 +100,15 @@ test.describe("Settings archive content", () => { headers: { Origin: baseURL! }, data: { tool_result_images: "keep" }, }); - // A silent restore failure would leave the daemon on drop and turn the + // A silent restore failure would leave the daemon on offload and turn the // retry into a pass that asserts nothing. expect(restored.ok()).toBe(true); }); - test("Drop selection persists after page reload", async ({ page }) => { + test("Offload selection persists after page reload", async ({ page }) => { await openArchiveContent(page); - // Fails loudly if a prior restore left the daemon on drop, because + // Fails loudly if a prior restore left the daemon on offload, because // clicking an already-selected option issues no PUT at all. await expect(page.getByRole("radio", { name: "Keep" })).toHaveAttribute( "aria-checked", @@ -116,9 +120,9 @@ test.describe("Settings archive content", () => { response.request().method() === "PUT" && new URL(response.url()).pathname === "/api/v1/settings", ); - await page.getByRole("radio", { name: "Drop" }).click(); - expect(await (await putEcho).json()).toMatchObject({ tool_result_images: "drop" }); - await expect(page.getByRole("radio", { name: "Drop" })).toHaveAttribute( + await page.getByRole("radio", { name: "Offload" }).click(); + expect(await (await putEcho).json()).toMatchObject({ tool_result_images: "offload" }); + await expect(page.getByRole("radio", { name: "Offload" })).toHaveAttribute( "aria-checked", "true", ); @@ -127,7 +131,7 @@ test.describe("Settings archive content", () => { const nav = page.getByRole("navigation", { name: "Settings" }); await nav.locator("button", { hasText: "Archive content" }).click(); - await expect(page.getByRole("radio", { name: "Drop" })).toHaveAttribute( + await expect(page.getByRole("radio", { name: "Offload" })).toHaveAttribute( "aria-checked", "true", ); diff --git a/frontend/messages/en.json b/frontend/messages/en.json index c6416d0afd..f9e9b8ba91 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -2060,7 +2060,8 @@ "settings_tool_result_images": "Tool-result images", "settings_tool_result_images_keep": "Keep", "settings_tool_result_images_drop": "Drop", - "settings_tool_result_images_hint": "Stored results keep the policy they were written under, and switching back to Keep cannot restore payloads already removed. Run agentsview db strip --images to project existing rows.", + "settings_tool_result_images_offload": "Offload", + "settings_tool_result_images_hint": "Stored results keep the policy they were written under, and switching back to Keep cannot restore payloads already removed. Run agentsview db migrate --images to project existing rows.", "settings_tool_result_images_restart_notice": "Restart the AgentsView daemon to apply this change to newly ingested sessions.", "settings_tool_images_title": "Tool-result images", "settings_tool_images_description": "Inline images stored inside tool results.", diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json index ef6dbf0a0f..9284350f8e 100644 --- a/frontend/messages/fr.json +++ b/frontend/messages/fr.json @@ -2059,7 +2059,8 @@ "settings_tool_result_images": "Images des résultats d'outils", "settings_tool_result_images_keep": "Conserver", "settings_tool_result_images_drop": "Supprimer", - "settings_tool_result_images_hint": "Les résultats déjà stockés gardent la règle appliquée lors de leur écriture, et revenir à Conserver ne restaure pas les données déjà supprimées. Lancez agentsview db strip --images pour traiter les lignes existantes.", + "settings_tool_result_images_offload": "Stocker séparément", + "settings_tool_result_images_hint": "Les résultats déjà stockés gardent la règle appliquée lors de leur écriture, et revenir à Conserver ne restaure pas les données déjà supprimées. Lancez agentsview db migrate --images pour traiter les lignes existantes.", "settings_tool_result_images_restart_notice": "Redémarrez le démon AgentsView pour appliquer ce changement aux nouvelles sessions ingérées.", "settings_tool_images_title": "Images des résultats d'outils", "settings_tool_images_description": "Images intégrées stockées dans les résultats d'outils.", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index a20491b5a7..8f6262592e 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -2060,7 +2060,8 @@ "settings_tool_result_images": "ツール結果の画像", "settings_tool_result_images_keep": "保持", "settings_tool_result_images_drop": "破棄", - "settings_tool_result_images_hint": "保存済みの結果は書き込み時のポリシーを保ち、保持に戻しても削除済みのデータは復元できません。既存の行は agentsview db strip --images で処理してください。", + "settings_tool_result_images_offload": "外部保存", + "settings_tool_result_images_hint": "保存済みの結果は書き込み時のポリシーを保ち、保持に戻しても削除済みのデータは復元できません。既存の行は agentsview db migrate --images で処理してください。", "settings_tool_result_images_restart_notice": "この変更を新しく取り込むセッションに適用するには、AgentsView デーモンを再起動してください。", "settings_tool_images_title": "ツール結果の画像", "settings_tool_images_description": "ツール結果内に保存されたインライン画像。", diff --git a/frontend/messages/ko.json b/frontend/messages/ko.json index 2b8fc3c5ae..280c2f9a05 100644 --- a/frontend/messages/ko.json +++ b/frontend/messages/ko.json @@ -1995,7 +1995,8 @@ "settings_tool_result_images": "도구 결과 이미지", "settings_tool_result_images_keep": "유지", "settings_tool_result_images_drop": "삭제", - "settings_tool_result_images_hint": "이미 저장된 결과는 기록될 당시의 정책을 유지하며, 유지로 되돌려도 이미 삭제된 데이터는 복원되지 않습니다. 기존 행은 agentsview db strip --images 로 처리합니다.", + "settings_tool_result_images_offload": "별도 저장", + "settings_tool_result_images_hint": "이미 저장된 결과는 기록될 당시의 정책을 유지하며, 유지로 되돌려도 이미 삭제된 데이터는 복원되지 않습니다. 기존 행은 agentsview db migrate --images 로 처리합니다.", "settings_tool_result_images_restart_notice": "이 변경을 새로 수집되는 세션에 적용하려면 AgentsView 데몬을 다시 시작하세요.", "settings_tool_images_title": "도구 결과 이미지", "settings_tool_images_description": "도구 결과 내에 저장된 인라인 이미지.", diff --git a/frontend/messages/zh-CN.json b/frontend/messages/zh-CN.json index f635de9ca1..cf2a28601f 100644 --- a/frontend/messages/zh-CN.json +++ b/frontend/messages/zh-CN.json @@ -1995,7 +1995,8 @@ "settings_tool_result_images": "工具结果图片", "settings_tool_result_images_keep": "保留", "settings_tool_result_images_drop": "丢弃", - "settings_tool_result_images_hint": "已存储的结果保持写入时的策略,改回「保留」也无法恢复已删除的数据。运行 agentsview db strip --images 可处理已有记录。", + "settings_tool_result_images_offload": "单独存储", + "settings_tool_result_images_hint": "已存储的结果保持写入时的策略,改回「保留」也无法恢复已删除的数据。运行 agentsview db migrate --images 可处理已有记录。", "settings_tool_result_images_restart_notice": "重启 AgentsView 守护进程后,此更改才会应用到新采集的会话。", "settings_tool_images_title": "工具结果图像", "settings_tool_images_description": "存储在工具结果中的内联图像。", diff --git a/frontend/messages/zh-TW.json b/frontend/messages/zh-TW.json index 01676061ca..354a0f23eb 100644 --- a/frontend/messages/zh-TW.json +++ b/frontend/messages/zh-TW.json @@ -1995,7 +1995,8 @@ "settings_tool_result_images": "工具結果圖片", "settings_tool_result_images_keep": "保留", "settings_tool_result_images_drop": "捨棄", - "settings_tool_result_images_hint": "已儲存的結果維持寫入時的政策,改回「保留」也無法還原已刪除的資料。執行 agentsview db strip --images 可處理既有記錄。", + "settings_tool_result_images_offload": "另存圖片", + "settings_tool_result_images_hint": "已儲存的結果維持寫入時的政策,改回「保留」也無法還原已刪除的資料。執行 agentsview db migrate --images 可處理既有記錄。", "settings_tool_result_images_restart_notice": "重新啟動 AgentsView 常駐程式後,此變更才會套用到新擷取的對話。", "settings_tool_images_title": "工具結果圖像", "settings_tool_images_description": "儲存在工具結果中的內嵌圖像。", diff --git a/frontend/src/lib/api/generated/models/settingsResponseToolResultImages.ts b/frontend/src/lib/api/generated/models/settingsResponseToolResultImages.ts index 1e1353b304..0b0591b7a6 100644 --- a/frontend/src/lib/api/generated/models/settingsResponseToolResultImages.ts +++ b/frontend/src/lib/api/generated/models/settingsResponseToolResultImages.ts @@ -11,4 +11,5 @@ export type SettingsResponseToolResultImages = export const SettingsResponseToolResultImages = { keep: "keep", drop: "drop", + offload: "offload", } as const; diff --git a/frontend/src/lib/api/generated/models/settingsUpdateRequestToolResultImages.ts b/frontend/src/lib/api/generated/models/settingsUpdateRequestToolResultImages.ts index 0c09cd7454..dc26ed3819 100644 --- a/frontend/src/lib/api/generated/models/settingsUpdateRequestToolResultImages.ts +++ b/frontend/src/lib/api/generated/models/settingsUpdateRequestToolResultImages.ts @@ -11,4 +11,5 @@ export type SettingsUpdateRequestToolResultImages = export const SettingsUpdateRequestToolResultImages = { keep: "keep", drop: "drop", + offload: "offload", } as const; diff --git a/frontend/src/lib/components/activity/ActivityInsight.svelte b/frontend/src/lib/components/activity/ActivityInsight.svelte index ff77e2e9bf..5dbcff70de 100644 --- a/frontend/src/lib/components/activity/ActivityInsight.svelte +++ b/frontend/src/lib/components/activity/ActivityInsight.svelte @@ -14,7 +14,7 @@ import { insights } from "../../stores/insights.svelte.js"; import { router } from "../../stores/router.svelte.js"; import { ui } from "../../stores/ui.svelte.js"; - import { renderMarkdown } from "../../utils/markdown.js"; + import { loadAssetImages, renderMarkdown } from "../../utils/markdown.js"; import { highlightCodeFences } from "../../utils/highlight-fences.js"; import type { AgentName } from "../../api/types.js"; import { LightbulbIcon, PlusIcon } from "../../icons.js"; @@ -241,6 +241,7 @@
{@html renderMarkdown(insight.content, { renderUnknownXmlBlocksAsPreformatted: ui.renderUnknownXmlBlocksAsPreformatted, diff --git a/frontend/src/lib/components/content/MessageContent.svelte b/frontend/src/lib/components/content/MessageContent.svelte index 3f257be466..50bafe9a2d 100644 --- a/frontend/src/lib/components/content/MessageContent.svelte +++ b/frontend/src/lib/components/content/MessageContent.svelte @@ -36,7 +36,7 @@ import { sync } from "../../stores/sync.svelte.js"; import { applyHighlight } from "../../utils/highlight.js"; import { highlightCodeFences } from "../../utils/highlight-fences.js"; - import { renderMarkdown } from "../../utils/markdown.js"; + import { loadAssetImages, renderMarkdown } from "../../utils/markdown.js"; import { displayToolName } from "../../utils/toolDisplay.js"; import { CirclePlayIcon, PinIcon } from "../../icons.js"; import type { Session } from "../../api/types.js"; @@ -508,6 +508,7 @@ content: segment.content, current: isCurrentHighlight, }} + use:loadAssetImages={segment.content} > {@html renderMarkdown(segment.content, { renderUnknownXmlBlocksAsPreformatted: ui.renderUnknownXmlBlocksAsPreformatted, diff --git a/frontend/src/lib/components/content/SkillBlock.svelte b/frontend/src/lib/components/content/SkillBlock.svelte index 9f67775a02..ba63b0d34f 100644 --- a/frontend/src/lib/components/content/SkillBlock.svelte +++ b/frontend/src/lib/components/content/SkillBlock.svelte @@ -1,6 +1,6 @@