From f5dc22f7c14d6c0dd4bcedee5a85b21ecd294aed Mon Sep 17 00:00:00 2001
From: Adam Malczewski <github@tradam.dev>
Date: Sat, 27 Jun 2026 20:20:02 +0900
Subject: feat(vision): resolve persisted image URLs against the API base
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Images are now stored on disk under tmp (not SQLite) and served via
GET /images/:conversationId/:imageId. Persisted ImageChunk.url is a compact
relative HTTP path (/images/<conv>/<uuid>.png) instead of a base64 data URL.
No wire/transport-contract type change (behavior only) — re-mirrored the
delta notes.

- New pure resolveImageUrl(url, apiBase) helper (core/chunks/image-url.ts,
  +8 tests): data/absolute URLs pass through; relative paths are prepended
  with the API base (no double slash; empty base -> root-relative). Exported
  from core/chunks + re-exported from features/chat.
- ChatView: new apiBaseUrl prop; <img src> uses resolveImageUrl. The
  optimistic echo's data URL passes through; persisted relative paths
  resolve against the base. +3 tests.
- AppStore exposes httpBase (getter); App.svelte passes apiBaseUrl into
  ChatView and the heartbeat RunModal (also renders image chunks).

Verification: svelte-check 0/0; vitest 959/959 (run twice, +11); biome
clean; vite build OK. See backend-handoff.md §2j-update-2.

Not merged or pushed.
---
 src/features/chat/ui/ChatView.svelte | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

(limited to 'src/features/chat/ui/ChatView.svelte')

diff --git a/src/features/chat/ui/ChatView.svelte b/src/features/chat/ui/ChatView.svelte
index 8081951..cd69071 100644
--- a/src/features/chat/ui/ChatView.svelte
+++ b/src/features/chat/ui/ChatView.svelte
@@ -1,6 +1,6 @@
 <script lang="ts">
   import type { TurnProviderRetryEvent } from "@dispatch/wire";
-  import { groupRenderedChunks, type RenderedChunk, viewProviderRetry } from "../index";
+  import { groupRenderedChunks, resolveImageUrl, type RenderedChunk, viewProviderRetry } from "../index";
   import {
     interleaveTurnMetrics,
     viewCacheRate,
@@ -24,6 +24,7 @@
     onShowEarlier,
     thinkingKeyBase = 0,
     providerRetry = null,
+    apiBaseUrl = "",
   }: {
     chunks: readonly RenderedChunk[];
     turnMetrics?: readonly TurnMetricsEntry[];
@@ -44,6 +45,14 @@
      * newest attempt + delay, and is cleared when content resumes / turn ends.
      */
     providerRetry?: TurnProviderRetryEvent | null;
+    /**
+     * The HTTP API base URL (e.g. `http://localhost:24203`). Persisted image
+     * chunks carry a compact relative path (`/images/<conv>/<uuid>.png`); this
+     * base is prepended to render them. The optimistic echo's data URL and any
+     * absolute URL pass through unchanged (see `resolveImageUrl`). Defaults to
+     * "" (root-relative — a browser resolves `/images/…` against its origin).
+     */
+    apiBaseUrl?: string;
   } = $props();
 
   // True while a show-earlier page-in is awaited (disables the button).
@@ -96,15 +105,17 @@
 {#snippet chunkRow(rendered: RenderedChunk)}
   {#if rendered.role === "user"}
     <!-- User: a speech bubble, left-aligned. A user message may be multi-chunk
-         ([text, image, image, …]); each chunk renders in its own bubble (the
-         image's url is a base64 data URL or an https URL — render it directly). -->
+         ([text, image, image, …]); each chunk renders in its own bubble. A
+         persisted image chunk's url is a compact relative path (`/images/…`)
+         served by the backend — resolve it against the API base. The
+         optimistic echo's data URL (and any absolute URL) passes through. -->
     <div class="chat chat-start">
       <div class="chat-bubble chat-bubble-primary">
         {#if rendered.chunk.type === "text"}
           <p>{rendered.chunk.text}</p>
         {:else if rendered.chunk.type === "image"}
           <img
-            src={rendered.chunk.url}
+            src={resolveImageUrl(rendered.chunk.url, apiBaseUrl)}
             alt={rendered.chunk.mimeType ?? "pasted image"}
             loading="lazy"
             decoding="async"
-- 
cgit v1.2.3

