phase: 123_chat_image_questions
Build and Push Containers / build-and-push-app (push) Successful in 1m54s
Build and Push Containers / build-and-push-db (push) Failing after 1m50s

All gates green. Verification complete.

**Phase 123 — final verification pass (all 4 tasks already in `complete/`)**

- Verified the full implementation is in the working tree: `app/api/chat_images.py` (upload/serve pair), `ChatRequest.image`/`ChatMessage.image` (path-validated, omitted-when-None), toggle-off + stale-file hinted error frames, `build_user_content` multimodal build at both sites (chat.py deflected branch + `run_agent`), config-gated composer attach/preview/upload-then-send, restore + shared rendering, CSP `img-src 'self' data:` carve-out, mock-LLM capture buffer.
- `uv run pytest` → **2796 passed**, exit 0 (unit + integration).
- `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (29/4615 missed; phase-123 modules 99–100%).
- `uv run pytest tests/e2e/test_chat_image_questions.py -v --no-cov` → **5 passed** in isolation.
- `uv run ruff check . && uv run pyright` → clean (0 errors).

**Completion criteria:** (1) attach→send→multimodal text+image to the model, bubble/reload/shared all render it, saved chat stores the PATH with `"base64" not in json.dumps(stored)` — **verified** (E2E tests 1–4 + integration round-trip); (2) `BOR_IMAGES=false` — control hidden, exact hinted error frame, zero model calls / no query_log row — **verified** (E2E test 5 + integration); (3) text-only byte-identical (`content` stays a plain `str`) — **verified** (unit + integration); (4) all gates green — **verified**; (5) commit + phase move — left to the harness per pipeline rules (no `git add`/`commit` run).

No defects found; no live-infrastructure changes (repo + local dev DB only). **Next pending phase: none** — 123 is the last phase in `todo/`.
This commit is contained in:
2026-09-25 05:19:18 -04:00
parent a19d78d284
commit bef24e05e2
48 changed files with 3910 additions and 71 deletions
+44 -2
View File
@@ -318,9 +318,42 @@ function addStoppedNote(wrap) {
meta.appendChild(note);
}
/* The question's attached image (phase 123, task 03, TODO L6) — the
* local copy of the chat page's attachBubbleImage (the per-page
* duplication house style: this file keeps its own small copies of
* the chat page's message-fragment builders). The SAME .msg-image
* treatment the chat page uses — styles.css is shared by both pages,
* so the rule needs no second copy: the img at the TOP of the user
* bubble (the attachment is part of the question), lazy, alt = the
* record's text or the fallback. The load failure degrades IDENTICALLY
* to the chat page: the img is replaced by the small "image
* unavailable" line (the stored file was deleted out-of-band — the
* record keeps its path, the render degrades; never a broken icon).
* The serve route is public (the token is the shared chat's
* credential, like saved-chat content), so the img loads for guests
* exactly as it does for the owner. */
function addBubbleImage(wrap, src, alt) {
const bubble = wrap?.querySelector?.(".bubble");
if (!bubble) return;
const img = document.createElement("img");
img.className = "msg-image";
img.src = src;
img.alt = alt || "attached image";
img.loading = "lazy";
img.onerror = () => {
const note = document.createElement("span");
note.className = "msg-image-unavailable";
note.textContent = "image unavailable";
img.replaceWith(note);
};
bubble.prepend(img);
}
/* One stored record through the SAME .msg structure the chat page
* uses (pixel-parity with the chat page's restore path): user → the
* .msg.user bubble; brain → the .msg.brain bubble with the optional
* .msg.user bubble (with the question's attached image when the
* record carries the stored path — phase 123, task 03); brain → the
* .msg.brain bubble with the optional
* thinking block (restored COLLAPSED — phase 17), the tool lines,
* the deflection treatment + the plain-text "Maybe try" chips, the
* plain-text source chips, and the stopped note. NO interactive
@@ -331,7 +364,16 @@ function addStoppedNote(wrap) {
* unchanged. */
function renderSharedMessage(m) {
if (m.who === "user") {
addSharedMessage("user", renderMarkdown(m.text));
const wrap = addSharedMessage("user", renderMarkdown(m.text));
// Phase 123 (task 03, TODO L6): the question's attached image —
// the record's `m.image` carries the STORED PATH (A5: never
// base64; a pre-phase record has no key at all, so it renders
// byte-identically — no img). The public image route makes the
// shared view faithful: the SAME bubble treatment, alt, and
// load-failure degradation as the chat page's restore.
if (typeof m.image === "string" && m.image) {
addBubbleImage(wrap, m.image, m.text || "attached image");
}
return;
}
const wrap = addSharedMessage("brain", renderMarkdown(m.text));