mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
imagegen: preserve backend error messages
This commit is contained in:
@@ -3935,6 +3935,7 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
revised_prompt: None,
|
||||
result: "ok".to_string(),
|
||||
saved_path: None,
|
||||
error: None,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -4225,6 +4225,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+6
@@ -17182,6 +17182,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+6
@@ -14990,6 +14990,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1089,6 +1089,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1089,6 +1089,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1233,6 +1233,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1717,6 +1717,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1532,6 +1532,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+6
@@ -1532,6 +1532,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1532,6 +1532,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1717,6 +1717,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1532,6 +1532,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1717,6 +1717,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1532,6 +1532,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1532,6 +1532,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1233,6 +1233,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1233,6 +1233,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1233,6 +1233,12 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -99,4 +99,4 @@ reasoningEffort: ReasoningEffort | null,
|
||||
/**
|
||||
* Last known status of the target agents, when available.
|
||||
*/
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: AbsolutePathBuf, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: AbsolutePathBuf, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, error: string | null, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
|
||||
@@ -603,6 +603,7 @@ impl ThreadHistoryBuilder {
|
||||
revised_prompt: None,
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
error: None,
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
@@ -614,6 +615,7 @@ impl ThreadHistoryBuilder {
|
||||
revised_prompt: payload.revised_prompt.clone(),
|
||||
result: payload.result.clone(),
|
||||
saved_path: payload.saved_path.clone(),
|
||||
error: payload.error.clone(),
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
@@ -1582,10 +1584,11 @@ mod tests {
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(ImageGenerationEndEvent {
|
||||
call_id: "ig_123".into(),
|
||||
status: "completed".into(),
|
||||
status: "failed".into(),
|
||||
revised_prompt: Some("final prompt".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()),
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
error: Some("bad image request".into()),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-image".into(),
|
||||
@@ -1619,10 +1622,11 @@ mod tests {
|
||||
},
|
||||
ThreadItem::ImageGeneration {
|
||||
id: "ig_123".into(),
|
||||
status: "completed".into(),
|
||||
status: "failed".into(),
|
||||
revised_prompt: Some("final prompt".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()),
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
error: Some("bad image request".into()),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -363,6 +363,7 @@ pub enum ThreadItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
saved_path: Option<AbsolutePathBuf>,
|
||||
error: Option<String>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
@@ -840,6 +841,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
revised_prompt: image.revised_prompt,
|
||||
result: image.result,
|
||||
saved_path: image.saved_path,
|
||||
error: image.error,
|
||||
},
|
||||
CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange {
|
||||
id: file_change.id,
|
||||
|
||||
@@ -1316,6 +1316,7 @@ Today both notifications carry an empty `items` array even when item events were
|
||||
- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`.
|
||||
- `webSearch` — `{id, query, action?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion.
|
||||
- `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool.
|
||||
- `imageGeneration` — `{id, status, revisedPrompt, result, savedPath?, error}` for standalone image generation; failed items use `status: "failed"`, an empty `result`, and a user-facing `error` message, while successful and in-progress items set `error` to `null`.
|
||||
- `enteredReviewMode` — `{id, review}` sent when the reviewer starts; `review` is a short user-facing label such as `"current changes"` or the requested target description.
|
||||
- `exitedReviewMode` — `{id, review}` emitted when the reviewer finishes; `review` is the full plain-text review (usually, overall notes plus bullet point findings).
|
||||
- `contextCompaction` — `{id}` emitted when codex compacts the conversation history. This can happen automatically.
|
||||
|
||||
@@ -97,6 +97,7 @@ mod tests {
|
||||
revised_prompt: Some("revised".to_string()),
|
||||
result: "base64-result".to_string(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
error: None,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ async fn standalone_image_generation_failure_emits_terminal_item() -> Result<()>
|
||||
revised_prompt: Some("paint a blue whale".to_string()),
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
error: Some("image backend failed".to_string()),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -806,6 +806,7 @@ fn append_resume_redaction_history(
|
||||
revised_prompt: Some("secret revised prompt".to_string()),
|
||||
result: "base64-image-result".to_string(),
|
||||
saved_path: Some(test_absolute_path("/tmp/ig-1.png")),
|
||||
error: None,
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
|
||||
@@ -206,6 +206,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
||||
revised_prompt: revised_prompt.clone(),
|
||||
result: result.clone(),
|
||||
saved_path: None,
|
||||
error: None,
|
||||
},
|
||||
)),
|
||||
_ => None,
|
||||
|
||||
@@ -504,6 +504,7 @@ mod tests {
|
||||
revised_prompt: None,
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
error: None,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
@@ -515,6 +516,7 @@ mod tests {
|
||||
revised_prompt: Some("A tiny blue square".to_string()),
|
||||
result: "cG5n".to_string(),
|
||||
saved_path: Some(test_path_buf("/tmp/extension-claimed.png").abs()),
|
||||
error: None,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
@@ -578,6 +580,7 @@ mod tests {
|
||||
revised_prompt: None,
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
error: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -588,6 +591,7 @@ mod tests {
|
||||
revised_prompt: Some("A tiny blue square".to_string()),
|
||||
result: "cG5n".to_string(),
|
||||
saved_path: Some(expected_path.clone()),
|
||||
error: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
use codex_api::ApiError;
|
||||
use codex_api::ImageEditRequest;
|
||||
use codex_api::ImageGenerationRequest;
|
||||
use codex_api::ImageResponse;
|
||||
use codex_api::ImagesClient;
|
||||
use codex_api::ReqwestTransport;
|
||||
use codex_api::TransportError;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_model_provider::SharedModelProvider;
|
||||
use http::HeaderMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
const MAX_ERROR_MESSAGE_CHARS: usize = 4000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CodexImagesBackend {
|
||||
@@ -46,7 +51,7 @@ impl CodexImagesBackend {
|
||||
.await?
|
||||
.generate(&request, HeaderMap::new())
|
||||
.await
|
||||
.map_err(|err| err.to_string())
|
||||
.map_err(image_api_error_message)
|
||||
}
|
||||
|
||||
/// Sends a standalone image edit request through the configured Images client.
|
||||
@@ -55,6 +60,72 @@ impl CodexImagesBackend {
|
||||
.await?
|
||||
.edit(&request, HeaderMap::new())
|
||||
.await
|
||||
.map_err(|err| err.to_string())
|
||||
.map_err(image_api_error_message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ErrorPayload {
|
||||
error: Option<ErrorMessage>,
|
||||
detail: Option<ErrorDetail>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ErrorMessage {
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ErrorDetail {
|
||||
Message(String),
|
||||
Object { message: Option<String> },
|
||||
}
|
||||
|
||||
fn image_api_error_message(err: ApiError) -> String {
|
||||
let message = if let ApiError::Transport(TransportError::Http {
|
||||
body: Some(body), ..
|
||||
}) = &err
|
||||
{
|
||||
if let Some(message) = parse_error_message(body) {
|
||||
message
|
||||
} else if !body.trim().is_empty() {
|
||||
body.trim().to_string()
|
||||
} else {
|
||||
err.to_string()
|
||||
}
|
||||
} else {
|
||||
err.to_string()
|
||||
};
|
||||
|
||||
truncate_error_message(&message)
|
||||
}
|
||||
|
||||
fn parse_error_message(body: &str) -> Option<String> {
|
||||
let payload = serde_json::from_str::<ErrorPayload>(body).ok()?;
|
||||
payload
|
||||
.error
|
||||
.and_then(|error| error.message)
|
||||
.or(match payload.detail {
|
||||
Some(ErrorDetail::Message(message)) => Some(message),
|
||||
Some(ErrorDetail::Object { message }) => message,
|
||||
None => None,
|
||||
})
|
||||
.filter(|message| !message.trim().is_empty())
|
||||
}
|
||||
|
||||
fn truncate_error_message(message: &str) -> String {
|
||||
if message.chars().count() <= MAX_ERROR_MESSAGE_CHARS {
|
||||
return message.to_string();
|
||||
}
|
||||
|
||||
message
|
||||
.chars()
|
||||
.take(MAX_ERROR_MESSAGE_CHARS - 1)
|
||||
.chain(std::iter::once('…'))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "backend_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use codex_api::ApiError;
|
||||
use codex_api::TransportError;
|
||||
use http::StatusCode;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::MAX_ERROR_MESSAGE_CHARS;
|
||||
use super::image_api_error_message;
|
||||
|
||||
#[test]
|
||||
fn image_api_error_message_extracts_codex_backend_error_envelopes() {
|
||||
let cases = [
|
||||
(
|
||||
r#"{"error":{"message":"bad image request"}}"#,
|
||||
"bad image request",
|
||||
),
|
||||
(r#"{"detail":"Forbidden"}"#, "Forbidden"),
|
||||
(
|
||||
r#"{"detail":{"message":"workspace access denied","code":"codex_workspace_access_denied"}}"#,
|
||||
"workspace access denied",
|
||||
),
|
||||
(
|
||||
r#"{"detail":{"message":"current message"},"error":{"message":"legacy message"}}"#,
|
||||
"legacy message",
|
||||
),
|
||||
];
|
||||
|
||||
for (body, expected) in cases {
|
||||
assert_eq!(
|
||||
image_api_error_message(http_error(body.to_string())),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_api_error_message_truncates_plain_text_body() {
|
||||
let body = "x".repeat(MAX_ERROR_MESSAGE_CHARS + 1);
|
||||
|
||||
let message = image_api_error_message(http_error(body));
|
||||
|
||||
assert_eq!(message.chars().count(), MAX_ERROR_MESSAGE_CHARS);
|
||||
assert!(message.ends_with('…'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_api_error_message_truncates_json_message() {
|
||||
let backend_message = "x".repeat(MAX_ERROR_MESSAGE_CHARS + 1);
|
||||
let body = serde_json::json!({
|
||||
"error": {
|
||||
"message": backend_message,
|
||||
},
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let message = image_api_error_message(http_error(body));
|
||||
|
||||
assert_eq!(message.chars().count(), MAX_ERROR_MESSAGE_CHARS);
|
||||
assert!(message.ends_with('…'));
|
||||
assert!(!message.contains("error"));
|
||||
}
|
||||
|
||||
fn http_error(body: String) -> ApiError {
|
||||
ApiError::Transport(TransportError::Http {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
url: None,
|
||||
headers: None,
|
||||
body: Some(body),
|
||||
})
|
||||
}
|
||||
@@ -115,13 +115,13 @@ impl ImageGenerationTool {
|
||||
revised_prompt: None,
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
error: None,
|
||||
}))
|
||||
.await;
|
||||
let result = match request {
|
||||
ImageRequest::Generate(request) => self.backend.generate(request).await,
|
||||
ImageRequest::Edit(request) => self.backend.edit(request).await,
|
||||
}
|
||||
.map_err(|err| format!("image generation failed: {err}"))
|
||||
.and_then(|response| {
|
||||
response
|
||||
.data
|
||||
@@ -133,6 +133,7 @@ impl ImageGenerationTool {
|
||||
let result = match result {
|
||||
Ok(result) => result,
|
||||
Err(message) => {
|
||||
let model_message = format!("image generation failed: {message}");
|
||||
call.turn_item_emitter
|
||||
.emit_completed(ExtensionTurnItem::ImageGeneration(ImageGenerationItem {
|
||||
id: call.call_id.clone(),
|
||||
@@ -140,9 +141,10 @@ impl ImageGenerationTool {
|
||||
revised_prompt: Some(args.prompt.clone()),
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
error: Some(message),
|
||||
}))
|
||||
.await;
|
||||
return Err(FunctionCallError::RespondToModel(message));
|
||||
return Err(FunctionCallError::RespondToModel(model_message));
|
||||
}
|
||||
};
|
||||
call.turn_item_emitter
|
||||
@@ -152,6 +154,7 @@ impl ImageGenerationTool {
|
||||
revised_prompt: Some(args.prompt),
|
||||
result: result.clone(),
|
||||
saved_path: None,
|
||||
error: None,
|
||||
}))
|
||||
.await;
|
||||
let output_path =
|
||||
|
||||
@@ -151,6 +151,9 @@ pub struct ImageGenerationItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub saved_path: Option<AbsolutePathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
|
||||
@@ -501,6 +504,7 @@ impl ImageGenerationItem {
|
||||
revised_prompt: self.revised_prompt.clone(),
|
||||
result: self.result.clone(),
|
||||
saved_path: self.saved_path.clone(),
|
||||
error: self.error.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2358,6 +2358,9 @@ pub struct ImageGenerationEndEvent {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub saved_path: Option<AbsolutePathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
// Conversation kept for backward compatibility.
|
||||
@@ -4737,6 +4740,7 @@ mod tests {
|
||||
revised_prompt: None,
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
error: None,
|
||||
}),
|
||||
started_at_ms: 0,
|
||||
};
|
||||
@@ -4833,6 +4837,7 @@ mod tests {
|
||||
revised_prompt: Some("A tiny blue square".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
error: None,
|
||||
}),
|
||||
completed_at_ms: 0,
|
||||
};
|
||||
|
||||
@@ -154,9 +154,10 @@ impl ChatWidget {
|
||||
status,
|
||||
revised_prompt,
|
||||
saved_path,
|
||||
error,
|
||||
..
|
||||
} => {
|
||||
self.on_image_generation_end(id, status, revised_prompt, saved_path);
|
||||
self.on_image_generation_end(id, status, revised_prompt, saved_path, error);
|
||||
}
|
||||
ThreadItem::EnteredReviewMode { review, .. } => {
|
||||
if from_replay {
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@ source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
expression: "lines_to_single_string(&cells[0])"
|
||||
---
|
||||
✗ Image generation failed
|
||||
└ A tiny blue square
|
||||
└ Error: image backend failed
|
||||
|
||||
@@ -867,6 +867,7 @@ async fn image_generation_call_adds_history_cell() {
|
||||
"completed",
|
||||
Some("A tiny blue square".into()),
|
||||
Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
None,
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
@@ -884,6 +885,7 @@ async fn image_generation_call_adds_history_cell() {
|
||||
"failed",
|
||||
Some("A tiny blue square".into()),
|
||||
/*saved_path*/ None,
|
||||
Some("image backend failed".into()),
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
|
||||
@@ -697,6 +697,7 @@ pub(super) fn handle_image_generation_end(
|
||||
status: impl Into<String>,
|
||||
revised_prompt: Option<String>,
|
||||
saved_path: Option<AbsolutePathBuf>,
|
||||
error: Option<String>,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
@@ -709,6 +710,7 @@ pub(super) fn handle_image_generation_end(
|
||||
revised_prompt,
|
||||
result: String::new(),
|
||||
saved_path,
|
||||
error,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
|
||||
@@ -32,6 +32,7 @@ impl ChatWidget {
|
||||
status: String,
|
||||
revised_prompt: Option<String>,
|
||||
saved_path: Option<AbsolutePathBuf>,
|
||||
error: Option<String>,
|
||||
) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.add_to_history(history_cell::new_image_generation_call(
|
||||
@@ -39,6 +40,7 @@ impl ChatWidget {
|
||||
&status,
|
||||
revised_prompt,
|
||||
saved_path,
|
||||
error,
|
||||
));
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -76,9 +76,18 @@ pub(crate) fn new_image_generation_call(
|
||||
status: &str,
|
||||
revised_prompt: Option<String>,
|
||||
saved_path: Option<AbsolutePathBuf>,
|
||||
error: Option<String>,
|
||||
) -> PlainHistoryCell {
|
||||
let detail = revised_prompt.unwrap_or(call_id);
|
||||
let heading = if status == "failed" {
|
||||
let failed = status == "failed";
|
||||
let detail = if failed {
|
||||
error
|
||||
.map(|error| format!("Error: {}", truncate_text(error.trim(), 1000)))
|
||||
.or(revised_prompt)
|
||||
.unwrap_or(call_id)
|
||||
} else {
|
||||
revised_prompt.unwrap_or(call_id)
|
||||
};
|
||||
let heading = if failed {
|
||||
vec!["✗ ".red().bold(), "Image generation failed".bold()].into()
|
||||
} else {
|
||||
vec!["• ".dim(), "Generated Image:".bold()].into()
|
||||
|
||||
@@ -477,6 +477,7 @@ fn image_generation_call_renders_saved_path() {
|
||||
"completed",
|
||||
Some("A tiny blue square".to_string()),
|
||||
Some(saved_path),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user