feat: add multi-actions to presentation tool (#13357)
This commit is contained in:
parent
ad393fa753
commit
1df040e62b
8 changed files with 262 additions and 55 deletions
|
|
@ -85,6 +85,27 @@ pub struct PresentationArtifactRequest {
|
|||
pub args: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PresentationArtifactToolRequest {
|
||||
pub artifact_id: Option<String>,
|
||||
pub actions: Vec<PresentationArtifactToolAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PresentationArtifactExecutionRequest {
|
||||
pub artifact_id: Option<String>,
|
||||
pub requests: Vec<PresentationArtifactRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PresentationArtifactToolAction {
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub args: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PathAccessKind {
|
||||
Read,
|
||||
|
|
@ -99,6 +120,10 @@ pub struct PathAccessRequirement {
|
|||
}
|
||||
|
||||
impl PresentationArtifactRequest {
|
||||
pub fn is_mutating(&self) -> bool {
|
||||
!is_read_only_action(&self.action)
|
||||
}
|
||||
|
||||
pub fn required_path_accesses(
|
||||
&self,
|
||||
cwd: &Path,
|
||||
|
|
@ -175,3 +200,50 @@ impl PresentationArtifactRequest {
|
|||
Ok(access)
|
||||
}
|
||||
}
|
||||
|
||||
impl PresentationArtifactToolRequest {
|
||||
pub fn is_mutating(&self) -> Result<bool, PresentationArtifactError> {
|
||||
Ok(self.actions.iter().any(|request| !is_read_only_action(&request.action)))
|
||||
}
|
||||
|
||||
pub fn into_execution_request(
|
||||
self,
|
||||
) -> Result<PresentationArtifactExecutionRequest, PresentationArtifactError> {
|
||||
if self.actions.is_empty() {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: "presentation_artifact".to_string(),
|
||||
message: "`actions` must contain at least one item".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(PresentationArtifactExecutionRequest {
|
||||
artifact_id: self.artifact_id,
|
||||
requests: self
|
||||
.actions
|
||||
.into_iter()
|
||||
.map(|request| PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: request.action,
|
||||
args: request.args,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn required_path_accesses(
|
||||
&self,
|
||||
cwd: &Path,
|
||||
) -> Result<Vec<PathAccessRequirement>, PresentationArtifactError> {
|
||||
let mut accesses = Vec::new();
|
||||
for request in &self.actions {
|
||||
accesses.extend(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: request.action.clone(),
|
||||
args: request.args.clone(),
|
||||
}
|
||||
.required_path_accesses(cwd)?,
|
||||
);
|
||||
}
|
||||
Ok(accesses)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,47 @@ struct HistoryEntry {
|
|||
}
|
||||
|
||||
impl PresentationArtifactManager {
|
||||
pub fn execute_requests(
|
||||
&mut self,
|
||||
request: PresentationArtifactExecutionRequest,
|
||||
cwd: &Path,
|
||||
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
||||
let PresentationArtifactExecutionRequest {
|
||||
artifact_id,
|
||||
requests,
|
||||
} = request;
|
||||
let request_count = requests.len();
|
||||
let mut current_artifact_id = artifact_id;
|
||||
let mut executed_actions = Vec::with_capacity(request_count);
|
||||
let mut exported_paths = Vec::new();
|
||||
let mut last_response = None;
|
||||
|
||||
for mut request in requests {
|
||||
if request.artifact_id.is_none() {
|
||||
request.artifact_id = current_artifact_id.clone();
|
||||
}
|
||||
let response = self.execute(request, cwd)?;
|
||||
current_artifact_id = Some(response.artifact_id.clone());
|
||||
exported_paths.extend(response.exported_paths.iter().cloned());
|
||||
executed_actions.push(response.action.clone());
|
||||
last_response = Some(response);
|
||||
}
|
||||
|
||||
let mut response = last_response.ok_or_else(|| PresentationArtifactError::InvalidArgs {
|
||||
action: "presentation_artifact".to_string(),
|
||||
message: "request sequence must contain at least one action".to_string(),
|
||||
})?;
|
||||
if request_count > 1 {
|
||||
let final_summary = response.summary.clone();
|
||||
response.action = "batch".to_string();
|
||||
response.summary =
|
||||
format!("Executed {request_count} actions sequentially. {final_summary}");
|
||||
response.executed_actions = Some(executed_actions);
|
||||
response.exported_paths = exported_paths;
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
|
|
@ -2163,6 +2204,7 @@ impl PresentationArtifactManager {
|
|||
removed.artifact_id,
|
||||
removed.slides.len()
|
||||
),
|
||||
executed_actions: None,
|
||||
exported_paths: Vec::new(),
|
||||
artifact_snapshot: None,
|
||||
slide_list: None,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ pub struct PresentationArtifactResponse {
|
|||
pub artifact_id: String,
|
||||
pub action: String,
|
||||
pub summary: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub executed_actions: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub exported_paths: Vec<PathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -38,6 +40,7 @@ impl PresentationArtifactResponse {
|
|||
artifact_id,
|
||||
action,
|
||||
summary,
|
||||
executed_actions: None,
|
||||
exported_paths: Vec::new(),
|
||||
artifact_snapshot: Some(artifact_snapshot),
|
||||
slide_list: None,
|
||||
|
|
@ -63,6 +66,7 @@ fn response_for_document_state(
|
|||
artifact_id,
|
||||
action,
|
||||
summary,
|
||||
executed_actions: None,
|
||||
exported_paths: Vec::new(),
|
||||
artifact_snapshot: document.map(snapshot_for_document),
|
||||
slide_list: None,
|
||||
|
|
@ -132,4 +136,3 @@ pub struct ThemeSnapshot {
|
|||
pub major_font: Option<String>,
|
||||
pub minor_font: Option<String>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -258,6 +258,85 @@ fn exported_images_are_real_pictures_with_media_parts() -> Result<(), Box<dyn st
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_request_accepts_sequential_actions() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let request: PresentationArtifactToolRequest = serde_json::from_value(serde_json::json!({
|
||||
"actions": [
|
||||
{
|
||||
"action": "create",
|
||||
"args": { "name": "Batch Deck" }
|
||||
},
|
||||
{
|
||||
"action": "export_pptx",
|
||||
"args": { "path": "deck.pptx" }
|
||||
}
|
||||
]
|
||||
}))?;
|
||||
|
||||
let execution = request.into_execution_request()?;
|
||||
assert_eq!(execution.artifact_id, None);
|
||||
assert_eq!(execution.requests.len(), 2);
|
||||
assert_eq!(execution.requests[0].action, "create");
|
||||
assert_eq!(execution.requests[1].action, "export_pptx");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_can_execute_sequential_actions() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let response = manager.execute_requests(
|
||||
PresentationArtifactExecutionRequest {
|
||||
artifact_id: None,
|
||||
requests: vec![
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Batch Deck" }),
|
||||
},
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "add_text_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"text": "hello",
|
||||
"position": { "left": 40, "top": 40, "width": 200, "height": 80 }
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
assert_eq!(response.action, "batch");
|
||||
assert_eq!(
|
||||
response.executed_actions,
|
||||
Some(vec![
|
||||
"create".to_string(),
|
||||
"add_slide".to_string(),
|
||||
"add_text_shape".to_string(),
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.artifact_snapshot
|
||||
.as_ref()
|
||||
.map(|snapshot| snapshot.slide_count),
|
||||
Some(1)
|
||||
);
|
||||
assert!(
|
||||
response
|
||||
.summary
|
||||
.contains("Executed 3 actions sequentially.")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imported_pptx_surfaces_image_elements() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ use async_channel::Sender;
|
|||
use chrono::Local;
|
||||
use chrono::Utc;
|
||||
use codex_artifact_presentation::PresentationArtifactError;
|
||||
use codex_artifact_presentation::PresentationArtifactRequest;
|
||||
use codex_artifact_presentation::PresentationArtifactExecutionRequest;
|
||||
use codex_artifact_presentation::PresentationArtifactResponse;
|
||||
use codex_hooks::HookEvent;
|
||||
use codex_hooks::HookEventAfterAgent;
|
||||
|
|
@ -1782,11 +1782,11 @@ impl Session {
|
|||
|
||||
pub(crate) async fn execute_presentation_artifact(
|
||||
&self,
|
||||
request: PresentationArtifactRequest,
|
||||
request: PresentationArtifactExecutionRequest,
|
||||
cwd: &Path,
|
||||
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
||||
let mut state = self.state.lock().await;
|
||||
state.presentation_artifacts.execute(request, cwd)
|
||||
state.presentation_artifacts.execute_requests(request, cwd)
|
||||
}
|
||||
|
||||
async fn record_initial_history(&self, conversation_history: InitialHistory) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use async_trait::async_trait;
|
|||
use codex_artifact_presentation::PathAccessKind;
|
||||
use codex_artifact_presentation::PathAccessRequirement;
|
||||
use codex_artifact_presentation::PresentationArtifactError;
|
||||
use codex_artifact_presentation::PresentationArtifactRequest;
|
||||
use codex_artifact_presentation::PresentationArtifactToolRequest;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use serde_json::to_string;
|
||||
|
|
@ -37,23 +37,10 @@ impl ToolHandler for PresentationArtifactHandler {
|
|||
let ToolPayload::Function { arguments } = &invocation.payload else {
|
||||
return true;
|
||||
};
|
||||
let Ok(request) = parse_arguments::<PresentationArtifactRequest>(arguments) else {
|
||||
let Ok(request) = parse_arguments::<PresentationArtifactToolRequest>(arguments) else {
|
||||
return true;
|
||||
};
|
||||
!matches!(
|
||||
request.action.as_str(),
|
||||
"get_summary"
|
||||
| "list_slides"
|
||||
| "list_layouts"
|
||||
| "list_layout_placeholders"
|
||||
| "list_slide_placeholders"
|
||||
| "inspect"
|
||||
| "resolve"
|
||||
| "to_proto"
|
||||
| "get_style"
|
||||
| "describe_styles"
|
||||
| "record_patch"
|
||||
)
|
||||
request.is_mutating().unwrap_or(true)
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
|
||||
|
|
@ -80,7 +67,7 @@ impl ToolHandler for PresentationArtifactHandler {
|
|||
}
|
||||
};
|
||||
|
||||
let request: PresentationArtifactRequest = parse_arguments(&arguments)?;
|
||||
let request: PresentationArtifactToolRequest = parse_arguments(&arguments)?;
|
||||
for access in request
|
||||
.required_path_accesses(&turn.cwd)
|
||||
.map_err(presentation_error)?
|
||||
|
|
@ -89,7 +76,12 @@ impl ToolHandler for PresentationArtifactHandler {
|
|||
}
|
||||
|
||||
let response = session
|
||||
.execute_presentation_artifact(request, &turn.cwd)
|
||||
.execute_presentation_artifact(
|
||||
request
|
||||
.into_execution_request()
|
||||
.map_err(presentation_error)?,
|
||||
&turn.cwd,
|
||||
)
|
||||
.await
|
||||
.map_err(presentation_error)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -567,6 +567,26 @@ fn create_view_image_tool() -> ToolSpec {
|
|||
}
|
||||
|
||||
fn create_presentation_artifact_tool() -> ToolSpec {
|
||||
let action_step_schema = JsonSchema::Object {
|
||||
properties: BTreeMap::from([
|
||||
(
|
||||
"action".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Action name to run for this step.".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"args".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
additional_properties: Some(true.into()),
|
||||
},
|
||||
),
|
||||
]),
|
||||
required: Some(vec!["action".to_string(), "args".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
};
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"artifact_id".to_string(),
|
||||
|
|
@ -577,17 +597,12 @@ fn create_presentation_artifact_tool() -> ToolSpec {
|
|||
},
|
||||
),
|
||||
(
|
||||
"action".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Action name to run against the artifact.".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"args".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
additional_properties: Some(true.into()),
|
||||
"actions".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(action_step_schema),
|
||||
description: Some(
|
||||
"Array of `(action, args)` steps to execute sequentially.".to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
|
@ -598,7 +613,7 @@ fn create_presentation_artifact_tool() -> ToolSpec {
|
|||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["action".to_string(), "args".to_string()]),
|
||||
required: Some(vec!["actions".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Create and edit PowerPoint presentation artifacts inside the current thread.
|
|||
- Resume and fork do not restore live artifact state. Export files if you need a durable handoff.
|
||||
- Relative paths resolve from the current working directory.
|
||||
- Position and size values are in slide points.
|
||||
- Every tool call uses a top-level `actions` array of sequential steps. Each call operates on a single top-level `artifact_id` when one is needed. If a call starts with `create` or `import_pptx`, later steps in the same call automatically reuse the returned artifact id.
|
||||
|
||||
Supported actions:
|
||||
- `create`
|
||||
|
|
@ -61,60 +62,63 @@ Supported actions:
|
|||
- `delete_artifact`
|
||||
|
||||
Example create:
|
||||
`{"action":"create","args":{"name":"Quarterly Update"}}`
|
||||
`{"actions":[{"action":"create","args":{"name":"Quarterly Update"}}]}`
|
||||
|
||||
Example create with custom slide size:
|
||||
`{"action":"create","args":{"name":"Quarterly Update","slide_size":{"width":960,"height":540}}}`
|
||||
`{"actions":[{"action":"create","args":{"name":"Quarterly Update","slide_size":{"width":960,"height":540}}}]}`
|
||||
|
||||
Example edit:
|
||||
`{"artifact_id":"presentation_x","action":"add_text_shape","args":{"slide_index":0,"text":"Revenue up 24%","position":{"left":48,"top":72,"width":260,"height":80}}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"add_text_shape","args":{"slide_index":0,"text":"Revenue up 24%","position":{"left":48,"top":72,"width":260,"height":80}}}]}`
|
||||
|
||||
Example sequential batch:
|
||||
`{"actions":[{"action":"create","args":{"name":"Quarterly Update"}},{"action":"add_slide","args":{}},{"action":"add_text_shape","args":{"slide_index":0,"text":"Revenue up 24%","position":{"left":48,"top":72,"width":260,"height":80}}}]}`
|
||||
|
||||
Table creation also accepts optional `column_widths` and `row_heights` arrays in points when you need explicit table sizing instead of even splits.
|
||||
|
||||
Example export:
|
||||
`{"artifact_id":"presentation_x","action":"export_pptx","args":{"path":"artifacts/q2-update.pptx"}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"export_pptx","args":{"path":"artifacts/q2-update.pptx"}}]}`
|
||||
|
||||
Example layout flow:
|
||||
`{"artifact_id":"presentation_x","action":"create_layout","args":{"name":"Title Slide"}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"create_layout","args":{"name":"Title Slide"}}]}`
|
||||
|
||||
`{"artifact_id":"presentation_x","action":"add_layout_placeholder","args":{"layout_id":"layout_1","name":"title","placeholder_type":"title","text":"Click to add title","position":{"left":48,"top":48,"width":624,"height":72}}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"add_layout_placeholder","args":{"layout_id":"layout_1","name":"title","placeholder_type":"title","text":"Click to add title","position":{"left":48,"top":48,"width":624,"height":72}}}]}`
|
||||
|
||||
`{"artifact_id":"presentation_x","action":"set_slide_layout","args":{"slide_index":0,"layout_id":"layout_1"}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"set_slide_layout","args":{"slide_index":0,"layout_id":"layout_1"}}]}`
|
||||
|
||||
`{"artifact_id":"presentation_x","action":"list_layout_placeholders","args":{"layout_id":"layout_1"}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"list_layout_placeholders","args":{"layout_id":"layout_1"}}]}`
|
||||
|
||||
`{"artifact_id":"presentation_x","action":"list_slide_placeholders","args":{"slide_index":0}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"list_slide_placeholders","args":{"slide_index":0}}]}`
|
||||
|
||||
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.
|
||||
|
||||
`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:
|
||||
`{"artifact_id":"presentation_x","action":"inspect","args":{"include":"deck,slide,textbox,shape,table,chart,image,notes,layoutList","exclude":"notes","search":"roadmap","max_chars":12000}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"inspect","args":{"include":"deck,slide,textbox,shape,table,chart,image,notes,layoutList","exclude":"notes","search":"roadmap","max_chars":12000}}]}`
|
||||
|
||||
Example inspect target window:
|
||||
`{"artifact_id":"presentation_x","action":"inspect","args":{"include":"textbox","target":{"id":"sh/element_3","before_lines":1,"after_lines":1}}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"inspect","args":{"include":"textbox","target":{"id":"sh/element_3","before_lines":1,"after_lines":1}}}]}`
|
||||
|
||||
Example resolve:
|
||||
`{"artifact_id":"presentation_x","action":"resolve","args":{"id":"sh/element_3"}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"resolve","args":{"id":"sh/element_3"}}]}`
|
||||
|
||||
Example proto export:
|
||||
`{"artifact_id":"presentation_x","action":"to_proto","args":{}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"to_proto","args":{}}]}`
|
||||
|
||||
`to_proto` returns a full JSON snapshot of the current in-memory presentation document, including slide/layout records, anchors, notes, theme state, and typed element payloads.
|
||||
|
||||
Example patch recording:
|
||||
`{"artifact_id":"presentation_x","action":"record_patch","args":{"operations":[{"action":"add_text_shape","args":{"slide_index":0,"text":"Headline","position":{"left":48,"top":48,"width":320,"height":72}}},{"action":"set_slide_background","args":{"slide_index":0,"fill":"#F7F1E8"}}]}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"record_patch","args":{"operations":[{"action":"add_text_shape","args":{"slide_index":0,"text":"Headline","position":{"left":48,"top":48,"width":320,"height":72}}},{"action":"set_slide_background","args":{"slide_index":0,"fill":"#F7F1E8"}}]}}]}`
|
||||
|
||||
Example patch application:
|
||||
`{"artifact_id":"presentation_x","action":"apply_patch","args":{"patch":{"version":1,"artifactId":"presentation_x","operations":[{"action":"add_text_shape","args":{"slide_index":0,"text":"Headline","position":{"left":48,"top":48,"width":320,"height":72}}},{"action":"set_slide_background","args":{"slide_index":0,"fill":"#F7F1E8"}}]}}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"apply_patch","args":{"patch":{"version":1,"artifactId":"presentation_x","operations":[{"action":"add_text_shape","args":{"slide_index":0,"text":"Headline","position":{"left":48,"top":48,"width":320,"height":72}}},{"action":"set_slide_background","args":{"slide_index":0,"fill":"#F7F1E8"}}]}}}]}`
|
||||
|
||||
Patch payloads are single-artifact and currently support existing in-memory editing actions like slide/element/layout/theme/text updates. Lifecycle, import/export, and nested history actions are intentionally excluded.
|
||||
|
||||
Example undo/redo:
|
||||
`{"artifact_id":"presentation_x","action":"undo","args":{}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"undo","args":{}}]}`
|
||||
|
||||
`{"artifact_id":"presentation_x","action":"redo","args":{}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"redo","args":{}}]}`
|
||||
|
||||
Deck summaries, slide listings, `inspect`, and `resolve` now include active-slide metadata. Use `set_active_slide` to change it explicitly.
|
||||
|
||||
|
|
@ -123,10 +127,10 @@ Theme snapshots and `to_proto` both expose the deck theme hex color map via `hex
|
|||
Named text styles are supported through `add_style`, `get_style`, and `describe_styles`. Built-in styles include `title`, `heading1`, `body`, `list`, and `numberedList`.
|
||||
|
||||
Example style creation:
|
||||
`{"artifact_id":"presentation_x","action":"add_style","args":{"name":"callout","font_size":18,"color":"#336699","italic":true,"underline":true}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"add_style","args":{"name":"callout","font_size":18,"color":"#336699","italic":true,"underline":true}}]}`
|
||||
|
||||
Example style lookup:
|
||||
`{"artifact_id":"presentation_x","action":"get_style","args":{"name":"title"}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"get_style","args":{"name":"title"}}]}`
|
||||
|
||||
Text styling payloads on `add_text_shape`, `add_shape.text_style`, `update_text.styling`, and `update_table_cell.styling` accept `style` and `underline` in addition to the existing whole-element fields.
|
||||
|
||||
|
|
@ -155,9 +159,9 @@ Shape strokes accept an optional `style` field such as `solid`, `dashed`, `dotte
|
|||
Connectors are supported via `add_connector`, with straight/elbow/curved types plus dash styles and arrow heads.
|
||||
|
||||
Example preview:
|
||||
`{"artifact_id":"presentation_x","action":"export_preview","args":{"slide_index":0,"path":"artifacts/q2-update-slide1.png"}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"export_preview","args":{"slide_index":0,"path":"artifacts/q2-update-slide1.png"}}]}`
|
||||
|
||||
`export_preview` also accepts `format`, `scale`, and `quality` for rendered previews. `format` currently supports `png`, `jpeg`, and `svg`.
|
||||
|
||||
Example JPEG preview:
|
||||
`{"artifact_id":"presentation_x","action":"export_preview","args":{"slide_index":0,"path":"artifacts/q2-update-slide1.jpg","format":"jpeg","scale":0.75,"quality":85}}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"export_preview","args":{"slide_index":0,"path":"artifacts/q2-update-slide1.jpg","format":"jpeg","scale":0.75,"quality":85}}]}`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue