fix(chat): restaure le collage des images dans le composer custom CLI (#184)
Le collage d'une image (ex. capture d'écran) échouait en environnement desktop : le presse-papier image n'est souvent pas exposé comme un File synchrone dans clipboardData.files, mais uniquement via l'Async Clipboard API. On ajoute donc un repli sur navigator.clipboard.read() (MIME image/*) quand l'événement de collage ne fournit aucun fichier. Le preventDefault() n'est émis que lorsqu'on a effectivement lu des fichiers issus de l'événement, afin de ne pas bloquer le collage texte quand le repli async ne remonte rien. - refactor clipboardFiles(event) -> dataTransferFiles(DataTransfer) réutilisable - dedupeKey robuste pour les images sans nom (mime + taille + contenu base64) - ReplyChunk::UserPrompt porte désormais les labels d'attachments (DTO + accounting reply_chunk_bytes, skip_serializing_if Vec::is_empty) - tests ciblés frontend (+219) et backend (chat_bridge/dto/attachments) verts QA : tests ciblés verts. Réserve : environnement headless, pas de validation manuelle desktop — validation AppImage à confirmer en suivi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -27,11 +27,29 @@ use uuid::Uuid;
|
||||
fn reply_chunk_user_prompt_serialises_exact_camel_case() {
|
||||
let v = serde_json::to_value(ReplyChunk::UserPrompt {
|
||||
text: "run tests".into(),
|
||||
attachments: Vec::new(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(v, json!({ "kind": "userPrompt", "text": "run tests" }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_chunk_user_prompt_serialises_attachment_labels() {
|
||||
let v = serde_json::to_value(ReplyChunk::UserPrompt {
|
||||
text: "describe this".into(),
|
||||
attachments: vec!["clipboard.png".into()],
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
v,
|
||||
json!({
|
||||
"kind": "userPrompt",
|
||||
"text": "describe this",
|
||||
"attachments": ["clipboard.png"]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_chunk_text_delta_serialises_exact_camel_case() {
|
||||
let v = serde_json::to_value(ReplyChunk::TextDelta {
|
||||
@ -105,7 +123,10 @@ fn reply_chunk_error_serialises_exact_camel_case() {
|
||||
#[test]
|
||||
fn reply_chunk_round_trips_through_json_for_every_variant() {
|
||||
for chunk in [
|
||||
ReplyChunk::UserPrompt { text: "u".into() },
|
||||
ReplyChunk::UserPrompt {
|
||||
text: "u".into(),
|
||||
attachments: vec!["clipboard.png".into()],
|
||||
},
|
||||
ReplyChunk::TextDelta { text: "x".into() },
|
||||
ReplyChunk::ToolActivity {
|
||||
label: "runs".into(),
|
||||
@ -140,7 +161,13 @@ fn reply_chunk_deserialises_from_camel_case_wire_payload() {
|
||||
// The shape the frontend (or a mock gateway) emits.
|
||||
let back: ReplyChunk =
|
||||
serde_json::from_value(json!({ "kind": "userPrompt", "text": "hi" })).unwrap();
|
||||
assert_eq!(back, ReplyChunk::UserPrompt { text: "hi".into() });
|
||||
assert_eq!(
|
||||
back,
|
||||
ReplyChunk::UserPrompt {
|
||||
text: "hi".into(),
|
||||
attachments: Vec::new()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -310,7 +337,10 @@ fn reattach_chat_dto_serialises_camel_case_with_typed_scrollback() {
|
||||
let dto = ReattachChatDto {
|
||||
session_id: "sess-1".into(),
|
||||
scrollback: vec![
|
||||
ReplyChunk::UserPrompt { text: "Hi".into() },
|
||||
ReplyChunk::UserPrompt {
|
||||
text: "Hi".into(),
|
||||
attachments: Vec::new(),
|
||||
},
|
||||
ReplyChunk::TextDelta { text: "Hi".into() },
|
||||
ReplyChunk::Final {
|
||||
content: "Hi".into(),
|
||||
|
||||
Reference in New Issue
Block a user