feat(cli): références d'attachments [file n]/[image n] dans le transcript + zone chat redimensionnable — #188 (QA verte)

- Attachments: projection canonique ChatPromptAttachmentDto (référence [file n]/[image n] par compteur de type, ordre d'import) insérée dans le prompt modèle et transportée par ReplyChunk::UserPrompt (attachmentRefs) ; rendu compact côté CustomAgentChatView.
- Resize: la zone chat (LeafView/LayoutGrid) est redimensionnable, taille actuelle conservée par défaut, préférence locale persistée.
- Tests: commands (projection des refs), dto_chat, chat_bridge, CustomAgentChatView, LayoutGrid.chat.
This commit is contained in:
2026-08-13 13:01:03 +02:00
parent 8b8468dacd
commit 4f38e1c506
10 changed files with 513 additions and 42 deletions

View File

@ -48,7 +48,7 @@ use crate::dto::{
AppExitWorkGuardStateDto, AssignSkillRequestDto, AttachBackgroundTaskResultDto,
AttachLiveAgentRequestDto, AttachLiveAgentResponseDto, BackgroundTaskDto, CellKind,
ChangeAgentProfileDto, ChangeAgentProfileRequestDto, ChatAttachmentDto, ChatAttachmentInputDto,
ChatDraftScopeRequestDto, CloneOpenCodeProfileFromSeedRequestDto,
ChatDraftScopeRequestDto, ChatPromptAttachmentDto, CloneOpenCodeProfileFromSeedRequestDto,
CloneProfileFromSeedRequestDto, ConfigureProfilesRequestDto, ConversationDetailsDto,
CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateLayoutRequestDto,
CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto,
@ -2356,7 +2356,9 @@ pub async fn agent_send(
attachments.unwrap_or_default(),
)
.await?;
let prompt_for_model = prompt_with_imported_attachments(&prompt, &imported_attachments);
let attachment_refs = chat_prompt_attachment_refs(&imported_attachments);
let prompt_for_model =
prompt_with_imported_attachments(&prompt, &imported_attachments, &attachment_refs);
let attachment_labels = imported_attachments
.iter()
.map(|attachment| attachment.filename.clone())
@ -2369,6 +2371,7 @@ pub async fn agent_send(
ReplyChunk::UserPrompt {
text: prompt.clone(),
attachments: attachment_labels,
attachment_refs,
},
);
@ -2629,15 +2632,21 @@ fn decode_attachment_content_base64(raw: &str) -> Result<Vec<u8>, ErrorDto> {
})
}
fn prompt_with_imported_attachments(prompt: &str, attachments: &[ChatAttachmentDto]) -> String {
fn prompt_with_imported_attachments(
prompt: &str,
attachments: &[ChatAttachmentDto],
refs: &[ChatPromptAttachmentDto],
) -> String {
if attachments.is_empty() {
return prompt.to_owned();
}
let mut out = String::with_capacity(prompt.len() + attachments.len() * 160);
out.push_str(prompt);
out.push_str("\n\nAttachments imported by IdeA and readable from the agent sandbox:\n");
for attachment in attachments {
out.push_str("- id: ");
for (attachment, attachment_ref) in attachments.iter().zip(refs.iter()) {
out.push_str("- ");
out.push_str(&attachment_ref.reference);
out.push_str(" id: ");
out.push_str(&attachment.id);
out.push_str("; file: ");
out.push_str(&attachment.filename);
@ -2652,6 +2661,81 @@ fn prompt_with_imported_attachments(prompt: &str, attachments: &[ChatAttachmentD
out
}
fn chat_prompt_attachment_refs(attachments: &[ChatAttachmentDto]) -> Vec<ChatPromptAttachmentDto> {
let mut image_count = 0usize;
let mut file_count = 0usize;
attachments
.iter()
.map(|attachment| {
let reference = if attachment.mime.starts_with("image/") {
image_count += 1;
format!("[image {image_count}]")
} else {
file_count += 1;
format!("[file {file_count}]")
};
ChatPromptAttachmentDto {
reference,
filename: attachment.filename.clone(),
mime: attachment.mime.clone(),
size_bytes: attachment.size_bytes,
readable_path: attachment.readable_path.clone(),
}
})
.collect()
}
#[cfg(test)]
mod chat_prompt_attachment_projection_tests {
use super::*;
fn attachment(filename: &str, mime: &str, size_bytes: u64) -> ChatAttachmentDto {
ChatAttachmentDto {
id: format!("id-{filename}"),
filename: filename.to_owned(),
mime: mime.to_owned(),
size_bytes,
source_kind: domain::ChatAttachmentSourceKind::Clipboard,
storage_path: format!("agent-chat/session/id-{filename}-{filename}"),
readable_path: format!("/project/.ideai/attachments/agent-chat/session/{filename}"),
created_at: size_bytes,
}
}
#[test]
fn chat_prompt_attachment_refs_follow_import_order_and_kind_counters() {
let attachments = vec![
attachment("first.png", "image/png", 10),
attachment("notes.pdf", "application/pdf", 20),
attachment("second.jpg", "image/jpeg", 30),
];
let refs = chat_prompt_attachment_refs(&attachments);
assert_eq!(refs[0].reference, "[image 1]");
assert_eq!(refs[0].filename, "first.png");
assert_eq!(refs[1].reference, "[file 1]");
assert_eq!(refs[1].filename, "notes.pdf");
assert_eq!(refs[2].reference, "[image 2]");
assert_eq!(refs[2].filename, "second.jpg");
}
#[test]
fn prompt_with_imported_attachments_uses_canonical_refs() {
let attachments = vec![
attachment("first.png", "image/png", 10),
attachment("notes.pdf", "application/pdf", 20),
];
let refs = chat_prompt_attachment_refs(&attachments);
let prompt = prompt_with_imported_attachments("describe", &attachments, &refs);
assert!(prompt.contains("- [image 1] id: id-first.png; file: first.png"));
assert!(prompt.contains("- [file 1] id: id-notes.pdf; file: notes.pdf"));
assert!(prompt.contains("; path: /project/.ideai/attachments/agent-chat/session/notes.pdf"));
}
}
/// `cancel_resume` — annule la **reprise automatique** armée pour un agent limité
/// (ARCHITECTURE §21.1-4, fenêtre annulable).
///