mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
feat: drop artifact tool and feature (#15851)
This commit is contained in:
1 parent
7ef3cfe63e
commit
b00a05c785
14 files changed
+4
-672
No files matched your search
Generated
-1
@@ -1843,7 +1843,6 @@ dependencies = [
|
|||||||
"codex-app-server-protocol",
|
"codex-app-server-protocol",
|
||||||
"codex-apply-patch",
|
"codex-apply-patch",
|
||||||
"codex-arg0",
|
"codex-arg0",
|
||||||
"codex-artifacts",
|
|
||||||
"codex-async-utils",
|
"codex-async-utils",
|
||||||
"codex-code-mode",
|
"codex-code-mode",
|
||||||
"codex-config",
|
"codex-config",
|
||||||
|
|||||||
@@ -396,6 +396,7 @@ unwrap_used = "deny"
|
|||||||
ignored = [
|
ignored = [
|
||||||
"icu_provider",
|
"icu_provider",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
|
"codex-artifacts",
|
||||||
"codex-utils-readiness",
|
"codex-utils-readiness",
|
||||||
"codex-utils-template",
|
"codex-utils-template",
|
||||||
"codex-v8-poc",
|
"codex-v8-poc",
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ codex-hooks = { workspace = true }
|
|||||||
codex-instructions = { workspace = true }
|
codex-instructions = { workspace = true }
|
||||||
codex-network-proxy = { workspace = true }
|
codex-network-proxy = { workspace = true }
|
||||||
codex-otel = { workspace = true }
|
codex-otel = { workspace = true }
|
||||||
codex-artifacts = { workspace = true }
|
|
||||||
codex-plugin = { workspace = true }
|
codex-plugin = { workspace = true }
|
||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
codex-rollout = { workspace = true }
|
codex-rollout = { workspace = true }
|
||||||
|
|||||||
@@ -338,9 +338,6 @@
|
|||||||
"apps": {
|
"apps": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"artifact": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"child_agents_md": {
|
"child_agents_md": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -1980,9 +1977,6 @@
|
|||||||
"apps": {
|
"apps": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"artifact": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"child_agents_md": {
|
"child_agents_md": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ pub(crate) fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
|
|||||||
|
|
||||||
let mut validation = ObjectValidation::default();
|
let mut validation = ObjectValidation::default();
|
||||||
for feature in FEATURES {
|
for feature in FEATURES {
|
||||||
|
if feature.id == codex_features::Feature::Artifact {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
validation
|
validation
|
||||||
.properties
|
.properties
|
||||||
.insert(feature.key.to_string(), schema_gen.subschema_for::<bool>());
|
.insert(feature.key.to_string(), schema_gen.subschema_for::<bool>());
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ pub mod models_manager;
|
|||||||
mod network_policy_decision;
|
mod network_policy_decision;
|
||||||
pub mod network_proxy_loader;
|
pub mod network_proxy_loader;
|
||||||
mod original_image_detail;
|
mod original_image_detail;
|
||||||
mod packages;
|
|
||||||
pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY;
|
pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY;
|
||||||
pub use mcp_connection_manager::MCP_SANDBOX_STATE_METHOD;
|
pub use mcp_connection_manager::MCP_SANDBOX_STATE_METHOD;
|
||||||
pub use mcp_connection_manager::SandboxState;
|
pub use mcp_connection_manager::SandboxState;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
pub(crate) mod versions;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
/// Pinned versions for package-manager-backed installs.
|
|
||||||
pub(crate) const ARTIFACT_RUNTIME: &str = "2.5.6";
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use codex_artifacts::ArtifactBuildRequest;
|
|
||||||
use codex_artifacts::ArtifactCommandOutput;
|
|
||||||
use codex_artifacts::ArtifactRuntimeManager;
|
|
||||||
use codex_artifacts::ArtifactRuntimeManagerConfig;
|
|
||||||
use codex_artifacts::ArtifactsClient;
|
|
||||||
use codex_artifacts::ArtifactsError;
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use crate::codex::Session;
|
|
||||||
use crate::codex::TurnContext;
|
|
||||||
use crate::exec::ExecToolCallOutput;
|
|
||||||
use crate::exec::StreamOutput;
|
|
||||||
use crate::function_tool::FunctionCallError;
|
|
||||||
use crate::packages::versions;
|
|
||||||
use crate::protocol::ExecCommandSource;
|
|
||||||
use crate::tools::context::FunctionToolOutput;
|
|
||||||
use crate::tools::context::ToolInvocation;
|
|
||||||
use crate::tools::context::ToolPayload;
|
|
||||||
use crate::tools::events::ToolEmitter;
|
|
||||||
use crate::tools::events::ToolEventCtx;
|
|
||||||
use crate::tools::events::ToolEventFailure;
|
|
||||||
use crate::tools::events::ToolEventStage;
|
|
||||||
use crate::tools::registry::ToolHandler;
|
|
||||||
use crate::tools::registry::ToolKind;
|
|
||||||
use codex_features::Feature;
|
|
||||||
|
|
||||||
const ARTIFACTS_TOOL_NAME: &str = "artifacts";
|
|
||||||
const ARTIFACT_TOOL_PRAGMA_PREFIX: &str = "// codex-artifact-tool:";
|
|
||||||
const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30);
|
|
||||||
|
|
||||||
pub struct ArtifactsHandler;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
struct ArtifactsToolArgs {
|
|
||||||
source: String,
|
|
||||||
timeout_ms: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ToolHandler for ArtifactsHandler {
|
|
||||||
type Output = FunctionToolOutput;
|
|
||||||
|
|
||||||
fn kind(&self) -> ToolKind {
|
|
||||||
ToolKind::Function
|
|
||||||
}
|
|
||||||
|
|
||||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
|
||||||
matches!(payload, ToolPayload::Custom { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
|
||||||
let ToolInvocation {
|
|
||||||
session,
|
|
||||||
turn,
|
|
||||||
payload,
|
|
||||||
call_id,
|
|
||||||
..
|
|
||||||
} = invocation;
|
|
||||||
|
|
||||||
if !session.enabled(Feature::Artifact) {
|
|
||||||
return Err(FunctionCallError::RespondToModel(
|
|
||||||
"artifacts is disabled by feature flag".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = match payload {
|
|
||||||
ToolPayload::Custom { input } => parse_freeform_args(&input)?,
|
|
||||||
_ => {
|
|
||||||
return Err(FunctionCallError::RespondToModel(
|
|
||||||
"artifacts expects freeform JavaScript input authored against the preloaded @oai/artifact-tool exports".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = ArtifactsClient::from_runtime_manager(default_runtime_manager(
|
|
||||||
turn.config.codex_home.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let started_at = Instant::now();
|
|
||||||
emit_exec_begin(session.as_ref(), turn.as_ref(), &call_id).await;
|
|
||||||
|
|
||||||
let result = client
|
|
||||||
.execute_build(ArtifactBuildRequest {
|
|
||||||
source: args.source,
|
|
||||||
cwd: turn.cwd.to_path_buf(),
|
|
||||||
timeout: Some(Duration::from_millis(
|
|
||||||
args.timeout_ms
|
|
||||||
.unwrap_or(DEFAULT_EXECUTION_TIMEOUT.as_millis() as u64),
|
|
||||||
)),
|
|
||||||
env: Default::default(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let (success, output) = match result {
|
|
||||||
Ok(output) => (output.success(), output),
|
|
||||||
Err(error) => (false, error_output(&error)),
|
|
||||||
};
|
|
||||||
|
|
||||||
emit_exec_end(
|
|
||||||
session.as_ref(),
|
|
||||||
turn.as_ref(),
|
|
||||||
&call_id,
|
|
||||||
&output,
|
|
||||||
started_at.elapsed(),
|
|
||||||
success,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(FunctionToolOutput::from_text(
|
|
||||||
format_artifact_output(&output),
|
|
||||||
Some(success),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_freeform_args(input: &str) -> Result<ArtifactsToolArgs, FunctionCallError> {
|
|
||||||
if input.trim().is_empty() {
|
|
||||||
return Err(FunctionCallError::RespondToModel(
|
|
||||||
"artifacts expects raw JavaScript source text (non-empty) authored against the preloaded @oai/artifact-tool exports. Provide JS only, optionally with first-line `// codex-artifact-tool: timeout_ms=15000`."
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut args = ArtifactsToolArgs {
|
|
||||||
source: input.to_string(),
|
|
||||||
timeout_ms: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut lines = input.splitn(2, '\n');
|
|
||||||
let first_line = lines.next().unwrap_or_default();
|
|
||||||
let rest = lines.next().unwrap_or_default();
|
|
||||||
let trimmed = first_line.trim_start();
|
|
||||||
let Some(pragma) = parse_pragma_prefix(trimmed) else {
|
|
||||||
reject_json_or_quoted_source(&args.source)?;
|
|
||||||
return Ok(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut timeout_ms = None;
|
|
||||||
let directive = pragma.trim();
|
|
||||||
if !directive.is_empty() {
|
|
||||||
for token in directive.split_whitespace() {
|
|
||||||
let (key, value) = token.split_once('=').ok_or_else(|| {
|
|
||||||
FunctionCallError::RespondToModel(format!(
|
|
||||||
"artifacts pragma expects space-separated key=value pairs (supported keys: timeout_ms); got `{token}`"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
match key {
|
|
||||||
"timeout_ms" => {
|
|
||||||
if timeout_ms.is_some() {
|
|
||||||
return Err(FunctionCallError::RespondToModel(
|
|
||||||
"artifacts pragma specifies timeout_ms more than once".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let parsed = value.parse::<u64>().map_err(|_| {
|
|
||||||
FunctionCallError::RespondToModel(format!(
|
|
||||||
"artifacts pragma timeout_ms must be an integer; got `{value}`"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
timeout_ms = Some(parsed);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(FunctionCallError::RespondToModel(format!(
|
|
||||||
"artifacts pragma only supports timeout_ms; got `{key}`"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rest.trim().is_empty() {
|
|
||||||
return Err(FunctionCallError::RespondToModel(
|
|
||||||
"artifacts pragma must be followed by JavaScript source on subsequent lines"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
reject_json_or_quoted_source(rest)?;
|
|
||||||
args.source = rest.to_string();
|
|
||||||
args.timeout_ms = timeout_ms;
|
|
||||||
Ok(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reject_json_or_quoted_source(code: &str) -> Result<(), FunctionCallError> {
|
|
||||||
let trimmed = code.trim();
|
|
||||||
if trimmed.starts_with("```") {
|
|
||||||
return Err(FunctionCallError::RespondToModel(
|
|
||||||
"artifacts expects raw JavaScript source, not markdown code fences. Resend plain JS only (optional first line `// codex-artifact-tool: ...`)."
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let Ok(value) = serde_json::from_str::<JsonValue>(trimmed) else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
match value {
|
|
||||||
JsonValue::Object(_) | JsonValue::String(_) => Err(FunctionCallError::RespondToModel(
|
|
||||||
"artifacts is a freeform tool and expects raw JavaScript source authored against the preloaded @oai/artifact-tool exports. Resend plain JS only (optional first line `// codex-artifact-tool: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences."
|
|
||||||
.to_string(),
|
|
||||||
)),
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_pragma_prefix(line: &str) -> Option<&str> {
|
|
||||||
line.strip_prefix(ARTIFACT_TOOL_PRAGMA_PREFIX)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_runtime_manager(codex_home: std::path::PathBuf) -> ArtifactRuntimeManager {
|
|
||||||
ArtifactRuntimeManager::new(ArtifactRuntimeManagerConfig::with_default_release(
|
|
||||||
codex_home,
|
|
||||||
versions::ARTIFACT_RUNTIME,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn emit_exec_begin(session: &Session, turn: &TurnContext, call_id: &str) {
|
|
||||||
let emitter = ToolEmitter::shell(
|
|
||||||
vec![ARTIFACTS_TOOL_NAME.to_string()],
|
|
||||||
turn.cwd.to_path_buf(),
|
|
||||||
ExecCommandSource::Agent,
|
|
||||||
/*freeform*/ true,
|
|
||||||
);
|
|
||||||
let ctx = ToolEventCtx::new(session, turn, call_id, /*turn_diff_tracker*/ None);
|
|
||||||
emitter.emit(ctx, ToolEventStage::Begin).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn emit_exec_end(
|
|
||||||
session: &Session,
|
|
||||||
turn: &TurnContext,
|
|
||||||
call_id: &str,
|
|
||||||
output: &ArtifactCommandOutput,
|
|
||||||
duration: Duration,
|
|
||||||
success: bool,
|
|
||||||
) {
|
|
||||||
let exec_output = ExecToolCallOutput {
|
|
||||||
exit_code: output.exit_code.unwrap_or(1),
|
|
||||||
stdout: StreamOutput::new(output.stdout.clone()),
|
|
||||||
stderr: StreamOutput::new(output.stderr.clone()),
|
|
||||||
aggregated_output: StreamOutput::new(format_artifact_output(output)),
|
|
||||||
duration,
|
|
||||||
timed_out: false,
|
|
||||||
};
|
|
||||||
let emitter = ToolEmitter::shell(
|
|
||||||
vec![ARTIFACTS_TOOL_NAME.to_string()],
|
|
||||||
turn.cwd.to_path_buf(),
|
|
||||||
ExecCommandSource::Agent,
|
|
||||||
/*freeform*/ true,
|
|
||||||
);
|
|
||||||
let ctx = ToolEventCtx::new(session, turn, call_id, /*turn_diff_tracker*/ None);
|
|
||||||
let stage = if success {
|
|
||||||
ToolEventStage::Success(exec_output)
|
|
||||||
} else {
|
|
||||||
ToolEventStage::Failure(ToolEventFailure::Output(exec_output))
|
|
||||||
};
|
|
||||||
emitter.emit(ctx, stage).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_artifact_output(output: &ArtifactCommandOutput) -> String {
|
|
||||||
let stdout = output.stdout.trim();
|
|
||||||
let stderr = output.stderr.trim();
|
|
||||||
let mut sections = vec![format!(
|
|
||||||
"exit_code: {}",
|
|
||||||
output
|
|
||||||
.exit_code
|
|
||||||
.map(|code| code.to_string())
|
|
||||||
.unwrap_or_else(|| "null".to_string())
|
|
||||||
)];
|
|
||||||
if !stdout.is_empty() {
|
|
||||||
sections.push(format!("stdout:\n{stdout}"));
|
|
||||||
}
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
sections.push(format!("stderr:\n{stderr}"));
|
|
||||||
}
|
|
||||||
if stdout.is_empty() && stderr.is_empty() && output.success() {
|
|
||||||
sections.push("artifact JS completed successfully.".to_string());
|
|
||||||
}
|
|
||||||
sections.join("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_output(error: &ArtifactsError) -> ArtifactCommandOutput {
|
|
||||||
ArtifactCommandOutput {
|
|
||||||
exit_code: Some(1),
|
|
||||||
stdout: String::new(),
|
|
||||||
stderr: error.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "artifacts_tests.rs"]
|
|
||||||
mod tests;
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::packages::versions;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_freeform_args_without_pragma() {
|
|
||||||
let args = parse_freeform_args("console.log('ok');").expect("parse args");
|
|
||||||
assert_eq!(args.source, "console.log('ok');");
|
|
||||||
assert_eq!(args.timeout_ms, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_freeform_args_with_artifact_tool_pragma() {
|
|
||||||
let args = parse_freeform_args("// codex-artifact-tool: timeout_ms=45000\nconsole.log('ok');")
|
|
||||||
.expect("parse args");
|
|
||||||
assert_eq!(args.source, "console.log('ok');");
|
|
||||||
assert_eq!(args.timeout_ms, Some(45_000));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_freeform_args_rejects_json_wrapped_code() {
|
|
||||||
let err = parse_freeform_args("{\"code\":\"console.log('ok')\"}").expect_err("expected error");
|
|
||||||
assert!(
|
|
||||||
err.to_string()
|
|
||||||
.contains("artifacts is a freeform tool and expects raw JavaScript source")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn default_runtime_manager_uses_openai_codex_release_base() {
|
|
||||||
let codex_home = TempDir::new().expect("create temp codex home");
|
|
||||||
let manager = default_runtime_manager(codex_home.path().to_path_buf());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
manager.config().release().base_url().as_str(),
|
|
||||||
"https://github.com/openai/codex/releases/download/"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
manager.config().release().runtime_version(),
|
|
||||||
versions::ARTIFACT_RUNTIME
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_cached_runtime_reads_pinned_cache_path() {
|
|
||||||
let codex_home = TempDir::new().expect("create temp codex home");
|
|
||||||
let platform =
|
|
||||||
codex_artifacts::ArtifactRuntimePlatform::detect_current().expect("detect platform");
|
|
||||||
let install_dir = codex_home
|
|
||||||
.path()
|
|
||||||
.join("packages")
|
|
||||||
.join("artifacts")
|
|
||||||
.join(versions::ARTIFACT_RUNTIME)
|
|
||||||
.join(platform.as_str());
|
|
||||||
std::fs::create_dir_all(&install_dir).expect("create install dir");
|
|
||||||
std::fs::create_dir_all(install_dir.join("dist")).expect("create build entrypoint dir");
|
|
||||||
std::fs::write(
|
|
||||||
install_dir.join("package.json"),
|
|
||||||
serde_json::json!({
|
|
||||||
"name": "@oai/artifact-tool",
|
|
||||||
"version": versions::ARTIFACT_RUNTIME,
|
|
||||||
"type": "module",
|
|
||||||
"exports": {
|
|
||||||
".": "./dist/artifact_tool.mjs"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.expect("write package json");
|
|
||||||
std::fs::write(
|
|
||||||
install_dir.join("dist/artifact_tool.mjs"),
|
|
||||||
"export const ok = true;\n",
|
|
||||||
)
|
|
||||||
.expect("write build entrypoint");
|
|
||||||
|
|
||||||
let runtime = codex_artifacts::load_cached_runtime(
|
|
||||||
&codex_home
|
|
||||||
.path()
|
|
||||||
.join(codex_artifacts::DEFAULT_CACHE_ROOT_RELATIVE),
|
|
||||||
versions::ARTIFACT_RUNTIME,
|
|
||||||
)
|
|
||||||
.expect("resolve runtime");
|
|
||||||
assert_eq!(runtime.runtime_version(), versions::ARTIFACT_RUNTIME);
|
|
||||||
assert_eq!(
|
|
||||||
runtime.build_js_path(),
|
|
||||||
install_dir.join("dist/artifact_tool.mjs")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn format_artifact_output_includes_success_message_when_silent() {
|
|
||||||
let formatted = format_artifact_output(&ArtifactCommandOutput {
|
|
||||||
exit_code: Some(0),
|
|
||||||
stdout: String::new(),
|
|
||||||
stderr: String::new(),
|
|
||||||
});
|
|
||||||
assert!(formatted.contains("artifact JS completed successfully."));
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
pub(crate) mod agent_jobs;
|
pub(crate) mod agent_jobs;
|
||||||
pub mod apply_patch;
|
pub mod apply_patch;
|
||||||
mod artifacts;
|
|
||||||
mod dynamic;
|
mod dynamic;
|
||||||
mod js_repl;
|
mod js_repl;
|
||||||
mod list_dir;
|
mod list_dir;
|
||||||
@@ -35,7 +34,6 @@ use crate::sandboxing::SandboxPermissions;
|
|||||||
pub(crate) use crate::tools::code_mode::CodeModeExecuteHandler;
|
pub(crate) use crate::tools::code_mode::CodeModeExecuteHandler;
|
||||||
pub(crate) use crate::tools::code_mode::CodeModeWaitHandler;
|
pub(crate) use crate::tools::code_mode::CodeModeWaitHandler;
|
||||||
pub use apply_patch::ApplyPatchHandler;
|
pub use apply_patch::ApplyPatchHandler;
|
||||||
pub use artifacts::ArtifactsHandler;
|
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
pub use dynamic::DynamicToolHandler;
|
pub use dynamic::DynamicToolHandler;
|
||||||
|
|||||||
@@ -334,7 +334,6 @@ pub(crate) struct ToolsConfig {
|
|||||||
pub can_request_original_image_detail: bool,
|
pub can_request_original_image_detail: bool,
|
||||||
pub collab_tools: bool,
|
pub collab_tools: bool,
|
||||||
pub multi_agent_v2: bool,
|
pub multi_agent_v2: bool,
|
||||||
pub artifact_tools: bool,
|
|
||||||
pub request_user_input: bool,
|
pub request_user_input: bool,
|
||||||
pub default_mode_request_user_input: bool,
|
pub default_mode_request_user_input: bool,
|
||||||
pub experimental_supported_tools: Vec<String>,
|
pub experimental_supported_tools: Vec<String>,
|
||||||
@@ -394,8 +393,6 @@ impl ToolsConfig {
|
|||||||
&& features.enabled(Feature::Apps)
|
&& features.enabled(Feature::Apps)
|
||||||
&& features.enabled(Feature::Plugins);
|
&& features.enabled(Feature::Plugins);
|
||||||
let include_original_image_detail = can_request_original_image_detail(features, model_info);
|
let include_original_image_detail = can_request_original_image_detail(features, model_info);
|
||||||
let include_artifact_tools =
|
|
||||||
features.enabled(Feature::Artifact) && codex_artifacts::can_manage_artifact_runtime();
|
|
||||||
let include_image_gen_tool =
|
let include_image_gen_tool =
|
||||||
features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info);
|
features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info);
|
||||||
let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals);
|
let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals);
|
||||||
@@ -471,7 +468,6 @@ impl ToolsConfig {
|
|||||||
can_request_original_image_detail: include_original_image_detail,
|
can_request_original_image_detail: include_original_image_detail,
|
||||||
collab_tools: include_collab_tools,
|
collab_tools: include_collab_tools,
|
||||||
multi_agent_v2: include_multi_agent_v2,
|
multi_agent_v2: include_multi_agent_v2,
|
||||||
artifact_tools: include_artifact_tools,
|
|
||||||
request_user_input: include_request_user_input,
|
request_user_input: include_request_user_input,
|
||||||
default_mode_request_user_input: include_default_mode_request_user_input,
|
default_mode_request_user_input: include_default_mode_request_user_input,
|
||||||
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
|
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
|
||||||
@@ -2125,33 +2121,6 @@ JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_artifacts_tool() -> ToolSpec {
|
|
||||||
const ARTIFACTS_FREEFORM_GRAMMAR: &str = r#"
|
|
||||||
start: pragma_source | plain_source
|
|
||||||
|
|
||||||
pragma_source: PRAGMA_LINE NEWLINE js_source
|
|
||||||
plain_source: PLAIN_JS_SOURCE
|
|
||||||
|
|
||||||
js_source: JS_SOURCE
|
|
||||||
|
|
||||||
PRAGMA_LINE: /[ \t]*\/\/ codex-artifact-tool:[^\r\n]*/
|
|
||||||
NEWLINE: /\r?\n/
|
|
||||||
PLAIN_JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
|
||||||
JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
|
||||||
"#;
|
|
||||||
|
|
||||||
ToolSpec::Freeform(FreeformTool {
|
|
||||||
name: "artifacts".to_string(),
|
|
||||||
description: "Runs raw JavaScript against the installed `@oai/artifact-tool` package for creating presentations or spreadsheets. This is plain JavaScript executed by a local Node-compatible runtime with top-level await, not TypeScript: do not use type annotations, `interface`, `type`, or `import type`. Author code the same way you would for `import { Presentation, Workbook, PresentationFile, SpreadsheetFile, FileBlob, ... } from \"@oai/artifact-tool\"`, but omit that import line because the package is preloaded before your code runs. Named exports are copied onto `globalThis`, and the full module namespace is available as `globalThis.artifactTool`. This matches the upstream library-first API: create with `Presentation.create()` / `Workbook.create()`, preview with `presentation.export(...)` or `slide.export(...)`, and save files with `PresentationFile.exportPptx(...)` or `SpreadsheetFile.exportXlsx(...)`. Node built-ins such as `node:fs/promises` may still be imported when needed for saving preview bytes. This is a freeform tool: send raw JavaScript source text, optionally with a first-line pragma like `// codex-artifact-tool: timeout_ms=15000`; do not send JSON/quotes/markdown fences."
|
|
||||||
.to_string(),
|
|
||||||
format: FreeformToolFormat {
|
|
||||||
r#type: "grammar".to_string(),
|
|
||||||
syntax: "lark".to_string(),
|
|
||||||
definition: ARTIFACTS_FREEFORM_GRAMMAR.to_string(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_js_repl_reset_tool() -> ToolSpec {
|
fn create_js_repl_reset_tool() -> ToolSpec {
|
||||||
ToolSpec::Function(ResponsesApiTool {
|
ToolSpec::Function(ResponsesApiTool {
|
||||||
name: "js_repl_reset".to_string(),
|
name: "js_repl_reset".to_string(),
|
||||||
@@ -2588,7 +2557,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
|||||||
dynamic_tools: &[DynamicToolSpec],
|
dynamic_tools: &[DynamicToolSpec],
|
||||||
) -> ToolRegistryBuilder {
|
) -> ToolRegistryBuilder {
|
||||||
use crate::tools::handlers::ApplyPatchHandler;
|
use crate::tools::handlers::ApplyPatchHandler;
|
||||||
use crate::tools::handlers::ArtifactsHandler;
|
|
||||||
use crate::tools::handlers::CodeModeExecuteHandler;
|
use crate::tools::handlers::CodeModeExecuteHandler;
|
||||||
use crate::tools::handlers::CodeModeWaitHandler;
|
use crate::tools::handlers::CodeModeWaitHandler;
|
||||||
use crate::tools::handlers::DynamicToolHandler;
|
use crate::tools::handlers::DynamicToolHandler;
|
||||||
@@ -2639,7 +2607,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
|||||||
let code_mode_wait_handler = Arc::new(CodeModeWaitHandler);
|
let code_mode_wait_handler = Arc::new(CodeModeWaitHandler);
|
||||||
let js_repl_handler = Arc::new(JsReplHandler);
|
let js_repl_handler = Arc::new(JsReplHandler);
|
||||||
let js_repl_reset_handler = Arc::new(JsReplResetHandler);
|
let js_repl_reset_handler = Arc::new(JsReplResetHandler);
|
||||||
let artifacts_handler = Arc::new(ArtifactsHandler);
|
|
||||||
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
|
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
|
||||||
|
|
||||||
if config.code_mode_enabled {
|
if config.code_mode_enabled {
|
||||||
@@ -2956,16 +2923,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
|||||||
);
|
);
|
||||||
builder.register_handler("view_image", view_image_handler);
|
builder.register_handler("view_image", view_image_handler);
|
||||||
|
|
||||||
if config.artifact_tools {
|
|
||||||
push_tool_spec(
|
|
||||||
&mut builder,
|
|
||||||
create_artifacts_tool(),
|
|
||||||
/*supports_parallel_tool_calls*/ false,
|
|
||||||
config.code_mode_enabled,
|
|
||||||
);
|
|
||||||
builder.register_handler("artifacts", artifacts_handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.collab_tools {
|
if config.collab_tools {
|
||||||
push_tool_spec(
|
push_tool_spec(
|
||||||
&mut builder,
|
&mut builder,
|
||||||
|
|||||||
@@ -775,28 +775,6 @@ fn view_image_tool_includes_detail_with_original_detail_feature() {
|
|||||||
assert!(description.contains("omit this field for default resized behavior"));
|
assert!(description.contains("omit this field for default resized behavior"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_build_specs_artifact_tool_enabled() {
|
|
||||||
let mut config = test_config();
|
|
||||||
let runtime_root = tempfile::TempDir::new().expect("create temp codex home");
|
|
||||||
config.codex_home = runtime_root.path().to_path_buf();
|
|
||||||
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
|
||||||
let mut features = Features::with_defaults();
|
|
||||||
features.enable(Feature::Artifact);
|
|
||||||
let available_models = Vec::new();
|
|
||||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
||||||
model_info: &model_info,
|
|
||||||
available_models: &available_models,
|
|
||||||
features: &features,
|
|
||||||
web_search_mode: Some(WebSearchMode::Cached),
|
|
||||||
session_source: SessionSource::Cli,
|
|
||||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
||||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
||||||
});
|
|
||||||
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
|
||||||
assert_contains_tool_names(&tools, &["artifacts"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_specs_agent_job_worker_tools_enabled() {
|
fn test_build_specs_agent_job_worker_tools_enabled() {
|
||||||
let config = test_config();
|
let config = test_config();
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
Create and edit PowerPoint presentation artifacts inside the current thread.
|
|
||||||
|
|
||||||
- This is a stateful built-in tool. `artifact_id` values are returned by earlier calls and persist only for 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`
|
|
||||||
- `import_pptx`
|
|
||||||
- `export_pptx`
|
|
||||||
- `export_preview`
|
|
||||||
- `get_summary`
|
|
||||||
- `list_slides`
|
|
||||||
- `list_layouts`
|
|
||||||
- `list_layout_placeholders`
|
|
||||||
- `list_slide_placeholders`
|
|
||||||
- `inspect`
|
|
||||||
- `resolve`
|
|
||||||
- `to_proto`
|
|
||||||
- `record_patch`
|
|
||||||
- `apply_patch`
|
|
||||||
- `undo`
|
|
||||||
- `redo`
|
|
||||||
- `create_layout`
|
|
||||||
- `add_layout_placeholder`
|
|
||||||
- `set_slide_layout`
|
|
||||||
- `update_placeholder_text`
|
|
||||||
- `set_theme`
|
|
||||||
- `add_style`
|
|
||||||
- `get_style`
|
|
||||||
- `describe_styles`
|
|
||||||
- `set_notes`
|
|
||||||
- `set_notes_rich_text`
|
|
||||||
- `append_notes`
|
|
||||||
- `clear_notes`
|
|
||||||
- `set_notes_visibility`
|
|
||||||
- `set_active_slide`
|
|
||||||
- `add_slide`
|
|
||||||
- `insert_slide`
|
|
||||||
- `duplicate_slide`
|
|
||||||
- `move_slide`
|
|
||||||
- `delete_slide`
|
|
||||||
- `set_slide_background`
|
|
||||||
- `add_text_shape`
|
|
||||||
- `add_shape`
|
|
||||||
- `add_connector`
|
|
||||||
- `add_image`
|
|
||||||
- `replace_image`
|
|
||||||
- `add_table`
|
|
||||||
- `update_table_style`
|
|
||||||
- `style_table_block`
|
|
||||||
- `update_table_cell`
|
|
||||||
- `merge_table_cells`
|
|
||||||
- `add_chart`
|
|
||||||
- `update_chart`
|
|
||||||
- `add_chart_series`
|
|
||||||
- `update_text`
|
|
||||||
- `set_rich_text`
|
|
||||||
- `format_text_range`
|
|
||||||
- `replace_text`
|
|
||||||
- `insert_text_after`
|
|
||||||
- `set_hyperlink`
|
|
||||||
- `set_comment_author`
|
|
||||||
- `add_comment_thread`
|
|
||||||
- `add_comment_reply`
|
|
||||||
- `toggle_comment_reaction`
|
|
||||||
- `resolve_comment_thread`
|
|
||||||
- `reopen_comment_thread`
|
|
||||||
- `update_shape_style`
|
|
||||||
- `bring_to_front`
|
|
||||||
- `send_to_back`
|
|
||||||
- `delete_element`
|
|
||||||
- `delete_artifact`
|
|
||||||
|
|
||||||
Example create:
|
|
||||||
`{"actions":[{"action":"create","args":{"name":"Quarterly Update"}}]}`
|
|
||||||
|
|
||||||
Example create with custom slide size:
|
|
||||||
`{"actions":[{"action":"create","args":{"name":"Quarterly Update","slide_size":{"width":960,"height":540}}}]}`
|
|
||||||
|
|
||||||
Example edit:
|
|
||||||
`{"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. Tables also support `style_options`, `borders`, and `right_to_left`, with `update_table_style` and `style_table_block` available for incremental styling after creation.
|
|
||||||
|
|
||||||
Example export:
|
|
||||||
`{"artifact_id":"presentation_x","actions":[{"action":"export_pptx","args":{"path":"artifacts/q2-update.pptx"}}]}`
|
|
||||||
|
|
||||||
Example layout flow:
|
|
||||||
`{"artifact_id":"presentation_x","actions":[{"action":"create_layout","args":{"name":"Title Slide"}}]}`
|
|
||||||
|
|
||||||
`{"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","actions":[{"action":"set_slide_layout","args":{"slide_index":0,"layout_id":"layout_1"}}]}`
|
|
||||||
|
|
||||||
`{"artifact_id":"presentation_x","actions":[{"action":"list_layout_placeholders","args":{"layout_id":"layout_1"}}]}`
|
|
||||||
|
|
||||||
`{"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","actions":[{"action":"inspect","args":{"include":"deck,slide,textbox,shape,table,chart,image,notes,layoutList,textRange,comment","exclude":"notes","search":"roadmap","max_chars":12000}}]}`
|
|
||||||
|
|
||||||
Example inspect target window:
|
|
||||||
`{"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","actions":[{"action":"resolve","args":{"id":"sh/element_3"}}]}`
|
|
||||||
|
|
||||||
Example proto export:
|
|
||||||
`{"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.
|
|
||||||
|
|
||||||
Rich text is supported on notes, text boxes, shapes with text, and table cells. Use `set_rich_text` to replace a full rich-text payload, `set_notes_rich_text` for speaker notes, and `format_text_range` to annotate a substring by `query` or explicit codepoint range. `inspect`, `resolve`, and `to_proto` surface text-range anchors as `tr/<range_id>`.
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Example patch recording:
|
|
||||||
`{"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","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","actions":[{"action":"undo","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.
|
|
||||||
|
|
||||||
Theme snapshots and `to_proto` both expose the deck theme hex color map via `hex_color_map` / `hexColorMap`.
|
|
||||||
|
|
||||||
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","actions":[{"action":"add_style","args":{"name":"callout","font_size":18,"color":"#336699","italic":true,"underline":true}}]}`
|
|
||||||
|
|
||||||
Example style lookup:
|
|
||||||
`{"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.
|
|
||||||
|
|
||||||
Text-bearing elements also support literal `replace_text` and `insert_text_after` helpers for in-place edits without resending the full string.
|
|
||||||
|
|
||||||
Example rich text update:
|
|
||||||
`{"artifact_id":"presentation_x","actions":[{"action":"set_rich_text","args":{"element_id":"element_3","text":[[{"run":"Quarterly ","text_style":{"bold":true}},"update pipeline"]],"text_layout":{"wrap":"square","auto_fit":"shrinkText","vertical_alignment":"middle","insets":{"left":6,"right":6,"top":4,"bottom":4}}}}]}`
|
|
||||||
|
|
||||||
Example substring formatting:
|
|
||||||
`{"artifact_id":"presentation_x","actions":[{"action":"format_text_range","args":{"element_id":"element_3","query":"update","styling":{"italic":true},"link":{"uri":"https://example.com/update","is_external":true}}}]}`
|
|
||||||
|
|
||||||
Text boxes and shapes support whole-element hyperlinks via `set_hyperlink`. Supported `link_type` values are `url`, `slide`, `first_slide`, `last_slide`, `next_slide`, `previous_slide`, `end_show`, `email`, and `file`. Use `clear: true` to remove an existing hyperlink.
|
|
||||||
|
|
||||||
Notes visibility is honored on export: `set_notes_visibility` controls whether speaker notes are emitted into exported PPTX output.
|
|
||||||
|
|
||||||
Example comment thread:
|
|
||||||
`{"artifact_id":"presentation_x","actions":[{"action":"set_comment_author","args":{"display_name":"Jamie Fox","initials":"JF"}},{"action":"add_comment_thread","args":{"slide_index":0,"element_id":"element_3","query":"Quarterly","text":"Tighten this headline"}},{"action":"add_comment_reply","args":{"thread_id":"thread_1","text":"Applied to the draft."}}]}`
|
|
||||||
|
|
||||||
Image placeholders can be prompt-only. `add_image` accepts `prompt` without `path`/`data_url`, and unresolved placeholders export as a visible placeholder box instead of failing.
|
|
||||||
|
|
||||||
Layout placeholders with `placeholder_type: "picture"` or `"image"` materialize as placeholder image elements on slides, so they appear in `list_slide_placeholders`, `inspect`, and `resolve` as `image` records rather than generic shapes.
|
|
||||||
|
|
||||||
Remote images are supported. `add_image` and `replace_image` accept `uri` in addition to local `path`, raw base64 `blob`, and `data_url`.
|
|
||||||
|
|
||||||
Image edits can target inspect/resolve anchors like `im/element_3`, and `update_shape_style` now accepts image `fit`, `crop`, `rotation`, `flip_horizontal`, `flip_vertical`, and `lock_aspect_ratio` updates.
|
|
||||||
|
|
||||||
`add_image` and `replace_image` also accept optional `rotation`, `flip_horizontal`, and `flip_vertical` fields for image transforms.
|
|
||||||
|
|
||||||
`update_shape_style.position` accepts partial updates, so you can move or resize an element without resending the full rect.
|
|
||||||
|
|
||||||
Shape strokes accept an optional `style` field such as `solid`, `dashed`, `dotted`, `dash-dot`, `dash-dot-dot`, `long-dash`, or `long-dash-dot`. This applies to ordinary shapes via `add_shape.stroke` and `update_shape_style.stroke`.
|
|
||||||
|
|
||||||
`add_shape` also accepts optional `rotation`, `flip_horizontal`, and `flip_vertical` fields. Those same transform fields can also be provided inside the `position` object for `add_shape`, `add_image`, and `update_shape_style`.
|
|
||||||
|
|
||||||
Connectors are supported via `add_connector`, with straight/elbow/curved types plus dash styles and arrow heads.
|
|
||||||
|
|
||||||
Example chart update:
|
|
||||||
`{"artifact_id":"presentation_x","actions":[{"action":"update_chart","args":{"element_id":"element_7","style_index":12,"legend_position":"bottom","y_axis_title":"USD (millions)"}},{"action":"add_chart_series","args":{"element_id":"element_7","name":"Target","values":[11,13],"fill":"#70AD47","marker":{"symbol":"diamond","size":6}}}]}`
|
|
||||||
|
|
||||||
Example preview:
|
|
||||||
`{"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","actions":[{"action":"export_preview","args":{"slide_index":0,"path":"artifacts/q2-update-slide1.jpg","format":"jpeg","scale":0.75,"quality":85}}]}`
|
|
||||||
Reference in new issue
Block a user