summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-19 23:48:50 +0900
committerAdam Malczewski <[email protected]>2026-10-19 23:48:50 +0900
commitaa63d6b9f4cb14535fd0191ab358c27f0ee62ce8 (patch)
treed089ad4db3522d5ba8338f6badd4f78ab24fbd7f /src/features/chat
parent6122a90cf0c9f938945e684e19e74153b38e7b4b (diff)
downloaddispatch-web-aa63d6b9f4cb14535fd0191ab358c27f0ee62ce8.tar.gz
dispatch-web-aa63d6b9f4cb14535fd0191ab358c27f0ee62ce8.zip
feat(markdown): render local-path images inline via backend image route
Diffstat (limited to 'src/features/chat')
-rw-r--r--src/features/chat/ui.test.ts19
-rw-r--r--src/features/chat/ui/ChatView.svelte13
2 files changed, 31 insertions, 1 deletions
diff --git a/src/features/chat/ui.test.ts b/src/features/chat/ui.test.ts
index 10af76c..c557e4f 100644
--- a/src/features/chat/ui.test.ts
+++ b/src/features/chat/ui.test.ts
@@ -56,6 +56,25 @@ describe("ChatView", () => {
expect(screen.getByText("Hello!")).toBeInTheDocument();
});
+ it("renders local-path Markdown images through the backend image route", () => {
+ const chunks: RenderedChunk[] = [
+ {
+ seq: 1,
+ role: "assistant",
+ chunk: { type: "text", text: "![pic](/home/u/pic.png)" },
+ provisional: false,
+ },
+ ];
+
+ const { container } = render(ChatView, {
+ props: { chunks, apiBaseUrl: "http://localhost:24203", cwd: "/w/foo" },
+ });
+
+ expect(container.querySelector("img")?.getAttribute("src")).toBe(
+ "http://localhost:24203/local-image?path=%2Fhome%2Fu%2Fpic.png",
+ );
+ });
+
it("shows the show-earlier button only when earlier history is unloaded, and pages it in", async () => {
const chunks: RenderedChunk[] = [
{ seq: 26, role: "user", chunk: { type: "text", text: "later" }, provisional: false },
diff --git a/src/features/chat/ui/ChatView.svelte b/src/features/chat/ui/ChatView.svelte
index 4fd0911..f34de9a 100644
--- a/src/features/chat/ui/ChatView.svelte
+++ b/src/features/chat/ui/ChatView.svelte
@@ -30,6 +30,7 @@
thinkingKeyBase = 0,
providerRetry = null,
apiBaseUrl = "",
+ cwd = null,
cacheTimer = null,
}: {
chunks: readonly RenderedChunk[];
@@ -60,6 +61,12 @@
*/
apiBaseUrl?: string;
/**
+ * The active conversation's working directory (null when unset). Relative
+ * Markdown image srcs (`![x](shot.png)`) resolve against it; local absolute
+ * and `file://` paths do not need it. Passed to `Markdown` with `apiBaseUrl`.
+ */
+ cwd?: string | null;
+ /**
* The cache timer (gaps between model requests): a running step gap shows
* live at the bottom (under the executing tools), a running turn gap live
* on that turn's metrics row; finished gaps are stamped on their step /
@@ -187,7 +194,11 @@
<div class="chat chat-start [&>.chat-bubble]:max-w-5xl">
<div class="chat-bubble w-full bg-transparent">
{#if rendered.chunk.type === "text"}
- <Markdown text={rendered.chunk.text} streaming={rendered.streaming ?? false} />
+ <Markdown
+ text={rendered.chunk.text}
+ streaming={rendered.streaming ?? false}
+ image={{ apiBase: apiBaseUrl, cwd }}
+ />
{:else if rendered.chunk.type === "error"}
<div class="text-error" role="alert">
{rendered.chunk.message}