Fix uploaded images reaching Claude Code empty
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
81d9df4e6e
commit
a2b9528e0b
3 files changed
+33
-1
No files matched your search
@@ -3,6 +3,13 @@
|
|||||||
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
||||||
until 1.0, minor versions may contain breaking changes.
|
until 1.0, minor versions may contain breaking changes.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Images you upload reach Claude Code**: its chats read the history without the image bytes, so the
|
||||||
|
files it was pointed to were empty. Images without data are no longer written as empty files.
|
||||||
|
|
||||||
## [v202611-00039] – 2026-11-15
|
## [v202611-00039] – 2026-11-15
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -361,7 +361,12 @@ fn save_images(cwd: &str, parts: &[Part]) -> Vec<String> {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for (i, p) in parts.iter().enumerate() {
|
for (i, p) in parts.iter().enumerate() {
|
||||||
let Part::Image { image } = p else { continue };
|
let Part::Image { image } = p else { continue };
|
||||||
|
// An image without bytes (an unresolved reference) would become an empty file.
|
||||||
let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(&image.data) else { continue };
|
let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(&image.data) else { continue };
|
||||||
|
if bytes.is_empty() {
|
||||||
|
tracing::warn!("claude code: image {i} of the message has no data – not saved");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let ext = image.media_type.split('/').nth(1).unwrap_or("png").split('+').next().unwrap_or("png");
|
let ext = image.media_type.split('/').nth(1).unwrap_or("png").split('+').next().unwrap_or("png");
|
||||||
let path = dir.join(format!("chat-{}-{i}.{ext}", now_ms()));
|
let path = dir.join(format!("chat-{}-{i}.{ext}", now_ms()));
|
||||||
if std::fs::create_dir_all(&dir).is_ok() {
|
if std::fs::create_dir_all(&dir).is_ok() {
|
||||||
@@ -379,7 +384,8 @@ pub async fn run(app: Arc<App>, session_id: &str, cancel: CancellationToken) ->
|
|||||||
let session = app.session(session_id)?;
|
let session = app.session(session_id)?;
|
||||||
let owner = session.owner_id.clone();
|
let owner = session.owner_id.clone();
|
||||||
let model_id = session.model.strip_prefix("claude-code/").unwrap_or("default").to_string();
|
let model_id = session.model.strip_prefix("claude-code/").unwrap_or("default").to_string();
|
||||||
let history = crate::runner::active_history(app.store.messages(session_id)?);
|
// With the bytes of the images filled in (they are stored as references).
|
||||||
|
let history = crate::runner::active_history(app.store.messages_for_model(session_id)?);
|
||||||
let Some(last_user) = history.iter().rposition(|m| m.role == Role::User && m.parts.iter().any(|p| matches!(p, Part::Text { .. } | Part::Image { .. }))) else {
|
let Some(last_user) = history.iter().rposition(|m| m.role == Role::User && m.parts.iter().any(|p| matches!(p, Part::Text { .. } | Part::Image { .. }))) else {
|
||||||
bail!("nothing to do: no user message");
|
bail!("nothing to do: no user message");
|
||||||
};
|
};
|
||||||
@@ -956,6 +962,21 @@ pub async fn update() -> Result<String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn uploaded_images_are_saved_with_their_bytes_only() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("cc-uploads-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let parts = vec![
|
||||||
|
Part::Image { image: ImageData { media_type: "image/png".into(), data: "QUJD".into(), blob: None, size: None } },
|
||||||
|
// A reference whose bytes were not filled in: must not become an empty file.
|
||||||
|
Part::Image { image: ImageData { media_type: "image/png".into(), data: String::new(), blob: Some("x".into()), size: None } },
|
||||||
|
];
|
||||||
|
let saved = save_images(&dir.display().to_string(), &parts);
|
||||||
|
assert_eq!(saved.len(), 1);
|
||||||
|
assert_eq!(std::fs::read(&saved[0]).unwrap(), b"ABC");
|
||||||
|
std::fs::remove_dir_all(&dir).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn questions_roundtrip_with_the_cli_format() {
|
fn questions_roundtrip_with_the_cli_format() {
|
||||||
let input = json!({"questions": [
|
let input = json!({"questions": [
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
||||||
*/
|
*/
|
||||||
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
||||||
|
unreleased: {
|
||||||
|
en: ["Images you upload are now visible to Claude Code (they arrived empty before)"],
|
||||||
|
de: ["Hochgeladene Bilder sind für Claude Code jetzt sichtbar (sie kamen vorher leer an)"],
|
||||||
|
},
|
||||||
"202611-00039": {
|
"202611-00039": {
|
||||||
en: ["The chat colour no longer tints the chat background", "Claude Code can ask you questions again (question card in the chat)"],
|
en: ["The chat colour no longer tints the chat background", "Claude Code can ask you questions again (question card in the chat)"],
|
||||||
de: ["Die Chat-Farbe färbt den Hintergrund des Chats nicht mehr ein", "Claude Code kann dir wieder Fragen stellen (Fragenkarte im Chat)"],
|
de: ["Die Chat-Farbe färbt den Hintergrund des Chats nicht mehr ein", "Claude Code kann dir wieder Fragen stellen (Fragenkarte im Chat)"],
|
||||||
|
|||||||
Reference in new issue
Block a user