Store chat images separately; JPEG for large PNGs; gzip/brotli responses
Images of messages (pasted images, screenshots, generated images) were base64 inside the message JSON, so opening a chat downloaded all of them at once (66 MB for a chat with 120 screenshots). They now live in a `blobs` table, addressed by SHA-256; messages keep a reference (blob, size). The UI loads them from /api/blobs/<hash> lazily with an immutable cache header, the runner fills the bytes in before model requests, JSON exports stay self-contained. - Store::add_message moves inline images out; large opaque PNGs become JPEG (q85) when smaller. Older chats are converted once at start-up in the background, unused images are removed (also after deleting a chat) and the database is vacuumed when that frees 32 MB or more. - Responses are compressed with gzip/brotli (not images, SSE streams or binary downloads). Example (copy of production): chat load 66 MB -> 1.5 MB (370 KB brotli), database 90 MB -> 27 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6d656a355d
commit
e8e455b779
27 files changed
+474
-31
No files matched your search
+2
-2
@@ -421,7 +421,7 @@ impl App {
|
||||
_ => None,
|
||||
};
|
||||
if let (Some(mt), Ok(bytes)) = (mt, std::fs::read(&path)) {
|
||||
self.images.push(ImageData { media_type: mt.into(), data: base64::engine::general_purpose::STANDARD.encode(bytes) });
|
||||
self.images.push(ImageData { blob: None, size: None, media_type: mt.into(), data: base64::engine::general_purpose::STANDARD.encode(bytes) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -840,7 +840,7 @@ impl App {
|
||||
for (cmd, args) in tries {
|
||||
if let Ok(out) = std::process::Command::new(cmd).args(*args).stderr(std::process::Stdio::null()).output() {
|
||||
if out.status.success() && out.stdout.starts_with(b"\x89PNG") {
|
||||
self.images.push(ImageData { media_type: "image/png".into(), data: base64::engine::general_purpose::STANDARD.encode(&out.stdout) });
|
||||
self.images.push(ImageData { blob: None, size: None, media_type: "image/png".into(), data: base64::engine::general_purpose::STANDARD.encode(&out.stdout) });
|
||||
self.status = format!("image {} attached", self.images.len());
|
||||
return;
|
||||
}
|
||||
|
||||
+1
-1
@@ -408,7 +408,7 @@ pub async fn run(ctx: &Ctx, name: &str, input: &Value) -> Result<Output> {
|
||||
return Ok(Output {
|
||||
content: format!("Image {} ({} bytes)", rel(&ctx.cwd, &path), bytes.len()),
|
||||
is_error: false,
|
||||
images: vec![ImageData { media_type: mt.into(), data: base64::engine::general_purpose::STANDARD.encode(&bytes) }],
|
||||
images: vec![ImageData { blob: None, size: None, media_type: mt.into(), data: base64::engine::general_purpose::STANDARD.encode(&bytes) }],
|
||||
diff: None,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user