mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
go further
This commit is contained in:
@@ -104,6 +104,7 @@ impl PresentationArtifactManager {
|
||||
"get_summary" => self.get_summary(request),
|
||||
"list_slides" => self.list_slides(request),
|
||||
"list_layouts" => self.list_layouts(request),
|
||||
"list_masters" => self.list_masters(request),
|
||||
"list_layout_placeholders" => self.list_layout_placeholders(request),
|
||||
"list_slide_placeholders" => self.list_slide_placeholders(request),
|
||||
"inspect" => self.inspect(request),
|
||||
@@ -149,6 +150,8 @@ impl PresentationArtifactManager {
|
||||
"insert_text_after" => self.insert_text_after(request),
|
||||
"set_hyperlink" => self.set_hyperlink(request),
|
||||
"set_comment_author" => self.set_comment_author(request),
|
||||
"list_comment_threads" => self.list_comment_threads(request),
|
||||
"get_comment_thread" => self.get_comment_thread(request),
|
||||
"add_comment_thread" => self.add_comment_thread(request),
|
||||
"add_comment_reply" => self.add_comment_reply(request),
|
||||
"toggle_comment_reaction" => self.toggle_comment_reaction(request),
|
||||
@@ -453,6 +456,24 @@ impl PresentationArtifactManager {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn list_masters(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
||||
let artifact_id = required_artifact_id(&request)?;
|
||||
let document = self.get_document(&artifact_id, &request.action)?;
|
||||
let masters = master_layout_list(document);
|
||||
let mut response = PresentationArtifactResponse::new(
|
||||
artifact_id,
|
||||
request.action,
|
||||
format!("Listed {} masters", masters.len()),
|
||||
snapshot_for_document(document),
|
||||
);
|
||||
response.layout_list = Some(masters);
|
||||
response.theme = Some(document.theme_snapshot());
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn list_layout_placeholders(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
@@ -2512,6 +2533,54 @@ impl PresentationArtifactManager {
|
||||
))
|
||||
}
|
||||
|
||||
fn list_comment_threads(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
||||
let artifact_id = required_artifact_id(&request)?;
|
||||
let document = self.get_document(&artifact_id, &request.action)?;
|
||||
let mut response = PresentationArtifactResponse::new(
|
||||
artifact_id,
|
||||
request.action,
|
||||
format!("Listed {} comment threads", document.comment_threads.len()),
|
||||
snapshot_for_document(document),
|
||||
);
|
||||
response.resolved_record = Some(serde_json::json!({
|
||||
"commentSelf": document.comment_self.as_ref().map(comment_author_to_proto),
|
||||
"commentThreads": document
|
||||
.comment_threads
|
||||
.iter()
|
||||
.map(comment_thread_to_proto)
|
||||
.collect::<Vec<_>>(),
|
||||
}));
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_comment_thread(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
||||
let args: CommentThreadIdArgs = parse_args(&request.action, &request.args)?;
|
||||
let artifact_id = required_artifact_id(&request)?;
|
||||
let document = self.get_document(&artifact_id, &request.action)?;
|
||||
let thread = document
|
||||
.comment_threads
|
||||
.iter()
|
||||
.find(|thread| thread.thread_id == args.thread_id)
|
||||
.ok_or_else(|| PresentationArtifactError::InvalidArgs {
|
||||
action: request.action.clone(),
|
||||
message: format!("unknown comment thread `{}`", args.thread_id),
|
||||
})?;
|
||||
let mut response = PresentationArtifactResponse::new(
|
||||
artifact_id,
|
||||
request.action,
|
||||
format!("Retrieved comment thread `{}`", args.thread_id),
|
||||
snapshot_for_document(document),
|
||||
);
|
||||
response.resolved_record = Some(comment_thread_to_proto(thread));
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn add_comment_thread(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
|
||||
@@ -285,7 +285,7 @@ enum TextVerticalAlignment {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CommentAuthorProfile {
|
||||
pub(super) struct CommentAuthorProfile {
|
||||
display_name: String,
|
||||
initials: String,
|
||||
email: Option<String>,
|
||||
@@ -332,7 +332,7 @@ enum CommentTarget {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CommentThread {
|
||||
pub(super) struct CommentThread {
|
||||
thread_id: String,
|
||||
target: CommentTarget,
|
||||
position: Option<CommentPosition>,
|
||||
|
||||
@@ -25,8 +25,11 @@ fn is_read_only_action(action: &str) -> bool {
|
||||
"get_summary"
|
||||
| "list_slides"
|
||||
| "list_layouts"
|
||||
| "list_masters"
|
||||
| "list_layout_placeholders"
|
||||
| "list_slide_placeholders"
|
||||
| "list_comment_threads"
|
||||
| "get_comment_thread"
|
||||
| "inspect"
|
||||
| "resolve"
|
||||
| "to_proto"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
const CODEX_METADATA_ENTRY: &str = "ppt/codex-document.json";
|
||||
const DEFAULT_SLIDE_MASTER_TEXT_STYLES: &str = r#"<p:txStyles>
|
||||
<p:titleStyle/>
|
||||
<p:bodyStyle/>
|
||||
<p:otherStyle/>
|
||||
</p:txStyles>"#;
|
||||
|
||||
fn import_codex_metadata_document(path: &Path) -> Result<Option<PresentationDocument>, String> {
|
||||
let file = std::fs::File::open(path).map_err(|error| error.to_string())?;
|
||||
@@ -333,6 +338,12 @@ fn patch_pptx_package(
|
||||
.map_err(|error| error.to_string())?;
|
||||
continue;
|
||||
}
|
||||
if name == "ppt/slideMasters/slideMaster1.xml" {
|
||||
writer
|
||||
.write_all(update_slide_master_xml(bytes)?.as_bytes())
|
||||
.map_err(|error| error.to_string())?;
|
||||
continue;
|
||||
}
|
||||
if let Some(slide_number) = parse_slide_xml_path(&name) {
|
||||
writer
|
||||
.write_all(
|
||||
@@ -452,6 +463,24 @@ fn update_presentation_xml_dimensions(
|
||||
)
|
||||
}
|
||||
|
||||
fn update_slide_master_xml(existing_bytes: Vec<u8>) -> Result<String, String> {
|
||||
let existing = String::from_utf8(existing_bytes).map_err(|error| error.to_string())?;
|
||||
if existing.contains("<p:txStyles>") {
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
let closing_tag = "</p:sldMaster>";
|
||||
let start = existing
|
||||
.find(closing_tag)
|
||||
.ok_or_else(|| "slide master xml is missing `</p:sldMaster>`".to_string())?;
|
||||
Ok(format!(
|
||||
"{}{}{}",
|
||||
&existing[..start],
|
||||
DEFAULT_SLIDE_MASTER_TEXT_STYLES,
|
||||
&existing[start..]
|
||||
))
|
||||
}
|
||||
|
||||
fn replace_self_closing_xml_tag(xml: &str, tag: &str, replacement: &str) -> Result<String, String> {
|
||||
let start = xml
|
||||
.find(&format!("<{tag} "))
|
||||
|
||||
@@ -472,7 +472,7 @@ fn chart_data_label_override_to_proto(override_spec: &ChartDataLabelOverride) ->
|
||||
})
|
||||
}
|
||||
|
||||
fn comment_author_to_proto(author: &CommentAuthorProfile) -> Value {
|
||||
pub(super) fn comment_author_to_proto(author: &CommentAuthorProfile) -> Value {
|
||||
serde_json::json!({
|
||||
"displayName": author.display_name,
|
||||
"initials": author.initials,
|
||||
@@ -480,7 +480,7 @@ fn comment_author_to_proto(author: &CommentAuthorProfile) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn comment_thread_to_proto(thread: &CommentThread) -> Value {
|
||||
pub(super) fn comment_thread_to_proto(thread: &CommentThread) -> Value {
|
||||
serde_json::json!({
|
||||
"kind": "comment",
|
||||
"threadId": thread.thread_id,
|
||||
|
||||
@@ -1184,7 +1184,9 @@ fn push_text_line(
|
||||
if !matches!(glyph.ch, ' ' | '\t') {
|
||||
break;
|
||||
}
|
||||
let trimmed = current.pop().expect("last glyph must exist");
|
||||
let Some(trimmed) = current.pop() else {
|
||||
break;
|
||||
};
|
||||
*current_width = current_width.saturating_sub(measure_glyph_width(&trimmed, font_px));
|
||||
if trimmed.ch == ' ' {
|
||||
*current_spaces = current_spaces.saturating_sub(1);
|
||||
|
||||
@@ -69,6 +69,13 @@ fn layout_list(document: &PresentationDocument) -> Vec<LayoutListEntry> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn master_layout_list(document: &PresentationDocument) -> Vec<LayoutListEntry> {
|
||||
layout_list(document)
|
||||
.into_iter()
|
||||
.filter(|layout| layout.kind == "master")
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn points_to_emu(points: u32) -> u32 {
|
||||
points.saturating_mul(POINT_TO_EMU)
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ fn manager_can_import_exported_presentation() -> Result<(), Box<dyn std::error::
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "add_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
@@ -281,6 +281,48 @@ fn exported_images_are_real_pictures_with_media_parts() -> Result<(), Box<dyn st
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exported_slide_master_includes_text_styles() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Slide Master Styles" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(created.artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let export_path = temp_dir.path().join("slide-master-styles.pptx");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(created.artifact_id),
|
||||
action: "export_pptx".to_string(),
|
||||
args: serde_json::json!({ "path": export_path }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let slide_master_xml = zip_entry_text(
|
||||
&temp_dir.path().join("slide-master-styles.pptx"),
|
||||
"ppt/slideMasters/slideMaster1.xml",
|
||||
)?;
|
||||
assert!(slide_master_xml.contains("<p:txStyles>"));
|
||||
assert!(slide_master_xml.contains("<p:titleStyle/>"));
|
||||
assert!(slide_master_xml.contains("<p:bodyStyle/>"));
|
||||
assert!(slide_master_xml.contains("<p:otherStyle/>"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exported_charts_are_real_pictures_with_media_parts() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
@@ -489,7 +531,7 @@ fn exported_text_shapes_preserve_text_styling() -> Result<(), Box<dyn std::error
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "add_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
@@ -1627,6 +1669,23 @@ fn manager_supports_layout_theme_notes_and_inspect() -> Result<(), Box<dyn std::
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(child_layouts.layout_list.as_ref().map(Vec::len), Some(2));
|
||||
let listed_masters = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "list_masters".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(listed_masters.layout_list.as_ref().map(Vec::len), Some(1));
|
||||
assert_eq!(
|
||||
listed_masters
|
||||
.layout_list
|
||||
.as_ref()
|
||||
.and_then(|layouts| layouts.first())
|
||||
.map(|layout| (layout.name.clone(), layout.kind.clone())),
|
||||
Some(("Brand Master".to_string(), "master".to_string()))
|
||||
);
|
||||
let layout_id = child_layouts
|
||||
.layout_list
|
||||
.as_ref()
|
||||
@@ -3488,6 +3547,41 @@ fn rich_text_comments_tables_and_charts_roundtrip_through_metadata()
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let comment_threads = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "list_comment_threads".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let comment_threads = comment_threads
|
||||
.resolved_record
|
||||
.expect("comment thread collection");
|
||||
assert_eq!(
|
||||
comment_threads["commentSelf"]["displayName"],
|
||||
serde_json::json!("Jamie Fox")
|
||||
);
|
||||
assert_eq!(
|
||||
comment_threads["commentThreads"].as_array().map(Vec::len),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
comment_threads["commentThreads"][0]["threadId"],
|
||||
serde_json::json!("thread_1")
|
||||
);
|
||||
let comment_thread = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "get_comment_thread".to_string(),
|
||||
args: serde_json::json!({ "thread_id": "thread_1" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let comment_thread = comment_thread.resolved_record.expect("comment thread");
|
||||
assert_eq!(comment_thread["anchor"], serde_json::json!("th/thread_1"));
|
||||
assert_eq!(comment_thread["status"], serde_json::json!("active"));
|
||||
assert_eq!(comment_thread["messages"].as_array().map(Vec::len), Some(2));
|
||||
|
||||
let table_added = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
|
||||
@@ -14,6 +14,7 @@ Supported actions:
|
||||
- `get_summary`
|
||||
- `list_slides`
|
||||
- `list_layouts`
|
||||
- `list_masters`
|
||||
- `list_layout_placeholders`
|
||||
- `list_slide_placeholders`
|
||||
- `inspect`
|
||||
@@ -63,6 +64,8 @@ Supported actions:
|
||||
- `insert_text_after`
|
||||
- `set_hyperlink`
|
||||
- `set_comment_author`
|
||||
- `list_comment_threads`
|
||||
- `get_comment_thread`
|
||||
- `add_comment_thread`
|
||||
- `add_comment_reply`
|
||||
- `toggle_comment_reaction`
|
||||
@@ -104,6 +107,8 @@ Example layout flow:
|
||||
|
||||
Layout references in `create_layout.parent_layout_id`, `add_layout_placeholder.layout_id`, `add_slide`, `insert_slide`, `set_slide_layout`, and `list_layout_placeholders` accept either a layout id or a layout name. Name matching prefers exact id, then exact name, then case-insensitive name.
|
||||
|
||||
Use `list_masters` when you only want layouts with `kind: "master"` instead of the full mixed layout list.
|
||||
|
||||
`insert_slide` accepts `index` or `after_slide_index`. If neither is provided, the new slide is inserted immediately after the active slide, or appended if no active slide is set yet.
|
||||
|
||||
Example inspect:
|
||||
@@ -124,6 +129,14 @@ Rich text is supported on notes, text boxes, shapes with text, and table cells.
|
||||
|
||||
Comment threads are supported through `set_comment_author`, `add_comment_thread`, `add_comment_reply`, `toggle_comment_reaction`, `resolve_comment_thread`, and `reopen_comment_thread`. Thread anchors resolve as `th/<thread_id>`, and comment records appear in both `inspect` and `to_proto`.
|
||||
|
||||
Use `list_comment_threads` for an explicit collection payload and `get_comment_thread` when you already know the thread id.
|
||||
|
||||
Example list comment threads:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"list_comment_threads","args":{}}]}`
|
||||
|
||||
Example get comment thread:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"get_comment_thread","args":{"thread_id":"thread_1"}}]}`
|
||||
|
||||
Charts support richer series metadata plus `update_chart` and `add_chart_series`, including legend, axis, data-label, marker, fill, and per-point override state.
|
||||
|
||||
Exported PPTX files embed Codex metadata so rich text, comment threads, and advanced table/chart state round-trip through `export_pptx` and `import_pptx` even when the base OOXML representation is lossy.
|
||||
|
||||
Reference in New Issue
Block a user