Lets users take a selfie (or pick any image) on mobile and drop it into the
chat. Designed to merge cleanly alongside concurrent work on main.
To avoid stepping on another agent working on main, nearly all of the code
lives in new files. Edits to existing files are kept intentionally tiny
and non-overlapping with the message-rendering path.
api/
images.ts ← Hono sub-app: POST /api/images, GET /api/images/:id
database/
images.ts ← chat_images_v1 table + blob storage helpers
shared/
image-types.ts ← ChatImage type, upload limits (separate from types.ts)
frontend/
components/CameraButton.tsx ← Mobile camera button (icon + label)
hooks/useImageUpload.ts ← Capture → compress → upload flow
index.ts — +3 lines: import imagesRoutes, mount with app.route("/", imagesRoutes).frontend/components/App.tsx — +1 import, +6 JSX lines: render <CameraButton /> above <MessageInput />. Does not touch MessageList, MessageInput, useMessages, db.ts, types.ts, or index.html.Rendering mermaid diagram...
<input type="file" accept="image/*" capture="user">
element opens the front-facing camera on iOS and Android.POST /api/images with a multipart form. Server validates MIME
type, stores bytes in std/blob under chat-images/v1/{id}, stores
metadata in a new chat_images_v1 SQLite table.insertMessage() to
append a text message of the form 📷 /api/images/{id}. This means the
image is delivered to every other client through the existing SSE
stream — zero changes to the message pipeline.GET /api/images/:id streams the bytes back with an
immutable cache header.capture="user" → front camera on iPhone/Android.accept="image/*" → also allows picking from the gallery if the user prefers.font-size: 16px on any input (inherited from the app) avoids iOS zoom-on-focus.min-h-10, rounded pill) for easy thumb-tapping.createImageBitmap({ imageOrientation: 'from-image' }).Currently, images show up in the chat as a text line 📷 /api/images/123. To
render them as real <img> tags, MessageList.tsx could be taught to detect
this prefix and render accordingly. That change is intentionally not made
here so this branch stays orthogonal to any concurrent work on MessageList.
A one-liner enhancement once merged:
// In MessageList.tsx, inside the message map:
const imageMatch = msg.text.match(/^📷\s+(\/api\/images\/\d+)/);
if (imageMatch) {
return <img src={imageMatch[1]} class="max-w-xs rounded-lg" />;
}
std/blob at chat-images/v1/{id}chat_images_v1 (brand new — no migration of
existing tables)application/octet-stream
from iOS camera is normalized to image/jpeg.