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:
@ -256,8 +256,22 @@ fn progress_entity_key(progress: &ReplyProgressDto) -> Option<String> {
|
||||
|
||||
fn reply_chunk_bytes(chunk: &ReplyChunk) -> usize {
|
||||
match chunk {
|
||||
ReplyChunk::UserPrompt { text, attachments } => {
|
||||
text.len() + attachments.iter().map(String::len).sum::<usize>()
|
||||
ReplyChunk::UserPrompt {
|
||||
text,
|
||||
attachments,
|
||||
attachment_refs,
|
||||
} => {
|
||||
text.len()
|
||||
+ attachments.iter().map(String::len).sum::<usize>()
|
||||
+ attachment_refs
|
||||
.iter()
|
||||
.map(|attachment| {
|
||||
attachment.reference.len()
|
||||
+ attachment.filename.len()
|
||||
+ attachment.mime.len()
|
||||
+ attachment.readable_path.len()
|
||||
})
|
||||
.sum::<usize>()
|
||||
}
|
||||
ReplyChunk::TextDelta { text } => text.len(),
|
||||
ReplyChunk::ToolActivity { label } => label.len(),
|
||||
|
||||
@ -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).
|
||||
///
|
||||
|
||||
@ -106,8 +106,22 @@ fn progress_chunk_with_id(
|
||||
|
||||
fn chunk_bytes(chunk: &ReplyChunk) -> usize {
|
||||
match chunk {
|
||||
ReplyChunk::UserPrompt { text, attachments } => {
|
||||
text.len() + attachments.iter().map(String::len).sum::<usize>()
|
||||
ReplyChunk::UserPrompt {
|
||||
text,
|
||||
attachments,
|
||||
attachment_refs,
|
||||
} => {
|
||||
text.len()
|
||||
+ attachments.iter().map(String::len).sum::<usize>()
|
||||
+ attachment_refs
|
||||
.iter()
|
||||
.map(|attachment| {
|
||||
attachment.reference.len()
|
||||
+ attachment.filename.len()
|
||||
+ attachment.mime.len()
|
||||
+ attachment.readable_path.len()
|
||||
})
|
||||
.sum::<usize>()
|
||||
}
|
||||
ReplyChunk::TextDelta { text } => text.len(),
|
||||
ReplyChunk::ToolActivity { label } => label.len(),
|
||||
|
||||
@ -8,9 +8,9 @@
|
||||
|
||||
use app_tauri_lib::dto::{
|
||||
CellKind, ChatAttachmentDto, ChatAttachmentInputDto, ChatDraftScopeRequestDto,
|
||||
ImportChatAttachmentsRequestDto, ImportChatAttachmentsResponseDto, ReadChatDraftResponseDto,
|
||||
ReattachChatDto, ReplyChunk, ReplyProgressDto, ReplyProgressKindDto, ReplyProgressSourceDto,
|
||||
ReplyProgressStageDto, SaveChatDraftRequestDto, TerminalSessionDto,
|
||||
ChatPromptAttachmentDto, ImportChatAttachmentsRequestDto, ImportChatAttachmentsResponseDto,
|
||||
ReadChatDraftResponseDto, ReattachChatDto, ReplyChunk, ReplyProgressDto, ReplyProgressKindDto,
|
||||
ReplyProgressSourceDto, ReplyProgressStageDto, SaveChatDraftRequestDto, TerminalSessionDto,
|
||||
};
|
||||
use application::{LaunchAgentOutput, StructuredSessionDescriptor};
|
||||
use domain::project::ProjectPath;
|
||||
@ -28,6 +28,7 @@ fn reply_chunk_user_prompt_serialises_exact_camel_case() {
|
||||
let v = serde_json::to_value(ReplyChunk::UserPrompt {
|
||||
text: "run tests".into(),
|
||||
attachments: Vec::new(),
|
||||
attachment_refs: Vec::new(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(v, json!({ "kind": "userPrompt", "text": "run tests" }));
|
||||
@ -38,6 +39,13 @@ 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()],
|
||||
attachment_refs: vec![ChatPromptAttachmentDto {
|
||||
reference: "[image 1]".into(),
|
||||
filename: "clipboard.png".into(),
|
||||
mime: "image/png".into(),
|
||||
size_bytes: 9,
|
||||
readable_path: "/project/.ideai/attachments/agent-chat/s/attach-1-clipboard.png".into(),
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@ -45,7 +53,14 @@ fn reply_chunk_user_prompt_serialises_attachment_labels() {
|
||||
json!({
|
||||
"kind": "userPrompt",
|
||||
"text": "describe this",
|
||||
"attachments": ["clipboard.png"]
|
||||
"attachments": ["clipboard.png"],
|
||||
"attachmentRefs": [{
|
||||
"reference": "[image 1]",
|
||||
"filename": "clipboard.png",
|
||||
"mime": "image/png",
|
||||
"sizeBytes": 9,
|
||||
"readablePath": "/project/.ideai/attachments/agent-chat/s/attach-1-clipboard.png"
|
||||
}]
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -128,6 +143,14 @@ fn reply_chunk_round_trips_through_json_for_every_variant() {
|
||||
ReplyChunk::UserPrompt {
|
||||
text: "u".into(),
|
||||
attachments: vec!["clipboard.png".into()],
|
||||
attachment_refs: vec![ChatPromptAttachmentDto {
|
||||
reference: "[image 1]".into(),
|
||||
filename: "clipboard.png".into(),
|
||||
mime: "image/png".into(),
|
||||
size_bytes: 9,
|
||||
readable_path: "/project/.ideai/attachments/agent-chat/s/attach-1-clipboard.png"
|
||||
.into(),
|
||||
}],
|
||||
},
|
||||
ReplyChunk::TextDelta { text: "x".into() },
|
||||
ReplyChunk::ToolActivity {
|
||||
@ -168,7 +191,8 @@ fn reply_chunk_deserialises_from_camel_case_wire_payload() {
|
||||
back,
|
||||
ReplyChunk::UserPrompt {
|
||||
text: "hi".into(),
|
||||
attachments: Vec::new()
|
||||
attachments: Vec::new(),
|
||||
attachment_refs: Vec::new()
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -343,6 +367,7 @@ fn reattach_chat_dto_serialises_camel_case_with_typed_scrollback() {
|
||||
ReplyChunk::UserPrompt {
|
||||
text: "Hi".into(),
|
||||
attachments: Vec::new(),
|
||||
attachment_refs: Vec::new(),
|
||||
},
|
||||
ReplyChunk::TextDelta { text: "Hi".into() },
|
||||
ReplyChunk::Final {
|
||||
|
||||
@ -3206,6 +3206,22 @@ pub struct ChatAttachmentDto {
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
/// Canonical attachment projection attached to a streamed user prompt.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChatPromptAttachmentDto {
|
||||
/// Stable textual reference inserted in the model prompt, e.g. `[file 1]`.
|
||||
pub reference: String,
|
||||
/// Original display filename.
|
||||
pub filename: String,
|
||||
/// MIME type.
|
||||
pub mime: String,
|
||||
/// Size in bytes.
|
||||
pub size_bytes: u64,
|
||||
/// Absolute managed path readable by the target agent sandbox.
|
||||
pub readable_path: String,
|
||||
}
|
||||
|
||||
impl From<domain::ChatAttachment> for ChatAttachmentDto {
|
||||
fn from(value: domain::ChatAttachment) -> Self {
|
||||
Self {
|
||||
@ -3359,6 +3375,9 @@ pub enum ReplyChunk {
|
||||
/// Display labels for attachments submitted with the prompt.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
attachments: Vec<String>,
|
||||
/// Canonical attachment references for this prompt, in backend import order.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
attachment_refs: Vec<ChatPromptAttachmentDto>,
|
||||
},
|
||||
/// An assistant text fragment (incremental chat rendering).
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
Reference in New Issue
Block a user