mirror of
https://github.com/openai/codex.git
synced 2026-03-04 13:43:19 +00:00
Compare commits
11 Commits
main
...
jif/render
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f4899b146 | ||
|
|
ef405153d9 | ||
|
|
8c32263808 | ||
|
|
bc52a662bb | ||
|
|
a99525eb00 | ||
|
|
1452f52d2c | ||
|
|
59a1058b30 | ||
|
|
61d77d8daf | ||
|
|
aa14b3cd2a | ||
|
|
57a13ada75 | ||
|
|
9047e4dc6f |
8
MODULE.bazel.lock
generated
8
MODULE.bazel.lock
generated
@@ -623,6 +623,7 @@
|
||||
"arbitrary_1.4.2": "{\"dependencies\":[{\"name\":\"derive_arbitrary\",\"optional\":true,\"req\":\"~1.4.0\"},{\"kind\":\"dev\",\"name\":\"exhaustigen\",\"req\":\"^0.1.0\"}],\"features\":{\"derive\":[\"derive_arbitrary\"]}}",
|
||||
"arboard_3.6.1": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"clipboard-win\",\"req\":\"^5.3.1\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"features\":[\"png\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"default_features\":false,\"features\":[\"tiff\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"png\",\"bmp\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(windows)\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(windows)\"},{\"name\":\"objc2\",\"req\":\"^0.6.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"objc2-core-graphics\",\"NSPasteboard\",\"NSPasteboardItem\",\"NSImage\"],\"name\":\"objc2-app-kit\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CFCGTypes\"],\"name\":\"objc2-core-foundation\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CGImage\",\"CGColorSpace\",\"CGDataProvider\"],\"name\":\"objc2-core-graphics\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"NSArray\",\"NSString\",\"NSEnumerator\",\"NSGeometry\",\"NSValue\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_DataExchange\",\"Win32_System_Memory\",\"Win32_System_Ole\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.61.0\",\"target\":\"cfg(windows)\"},{\"name\":\"wl-clipboard-rs\",\"optional\":true,\"req\":\"^0.9.0\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"x11rb\",\"req\":\"^0.13\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"}],\"features\":{\"core-graphics\":[\"dep:objc2-core-graphics\"],\"default\":[\"image-data\"],\"image\":[\"dep:image\"],\"image-data\":[\"dep:objc2-core-graphics\",\"dep:objc2-core-foundation\",\"image\",\"windows-sys\",\"core-graphics\"],\"wayland-data-control\":[\"wl-clipboard-rs\"],\"windows-sys\":[\"windows-sys/Win32_Graphics_Gdi\"],\"wl-clipboard-rs\":[\"dep:wl-clipboard-rs\"]}}",
|
||||
"arc-swap_1.8.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adaptive-barrier\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"~0.7\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"~0.8\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"~0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.177\"}],\"features\":{\"experimental-strategies\":[],\"experimental-thread-local\":[],\"internal-test-strategies\":[],\"weak\":[]}}",
|
||||
"arrayref_0.3.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"}],\"features\":{}}",
|
||||
"arrayvec_0.7.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.4\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"matches\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"ascii-canvas_3.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"diff\",\"req\":\"^0.1\"},{\"name\":\"term\",\"req\":\"^0.7\"}],\"features\":{}}",
|
||||
"ascii_1.1.0": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"name\":\"serde_test\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}",
|
||||
@@ -856,6 +857,8 @@
|
||||
"fnv_1.0.7": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"foldhash_0.1.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hashbrown\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.8\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"foldhash_0.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hashbrown\",\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rapidhash\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"std\":[]}}",
|
||||
"font8x8_0.3.1": "{\"dependencies\":[],\"features\":{\"default\":[\"unicode\",\"std\"],\"std\":[],\"unicode\":[]}}",
|
||||
"fontdue_0.9.3": "{\"dependencies\":[{\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"opentype-layout\"],\"name\":\"ttf-parser\",\"req\":\"^0.21\"}],\"features\":{\"default\":[\"simd\",\"hashbrown\"],\"parallel\":[\"rayon\",\"hashbrown\",\"hashbrown/rayon\"],\"simd\":[],\"std\":[]}}",
|
||||
"foreign-types-shared_0.1.1": "{\"dependencies\":[],\"features\":{}}",
|
||||
"foreign-types_0.3.2": "{\"dependencies\":[{\"name\":\"foreign-types-shared\",\"req\":\"^0.1\"}],\"features\":{}}",
|
||||
"form_urlencoded_1.2.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"}],\"features\":{\"alloc\":[\"percent-encoding/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"percent-encoding/std\"]}}",
|
||||
@@ -1139,6 +1142,7 @@
|
||||
"pkcs8_0.10.2": "{\"dependencies\":[{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"pkcs5\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"spki\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"3des\":[\"encryption\",\"pkcs5/3des\"],\"alloc\":[\"der/alloc\",\"der/zeroize\",\"spki/alloc\"],\"des-insecure\":[\"encryption\",\"pkcs5/des-insecure\"],\"encryption\":[\"alloc\",\"pkcs5/alloc\",\"pkcs5/pbes2\",\"rand_core\"],\"getrandom\":[\"rand_core/getrandom\"],\"pem\":[\"alloc\",\"der/pem\",\"spki/pem\"],\"sha1-insecure\":[\"encryption\",\"pkcs5/sha1-insecure\"],\"std\":[\"alloc\",\"der/std\",\"spki/std\"]}}",
|
||||
"pkg-config_0.3.32": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"}],\"features\":{}}",
|
||||
"plist_1.8.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"indexmap\",\"req\":\"^2.1.0\"},{\"name\":\"quick_xml\",\"package\":\"quick-xml\",\"req\":\"^0.38.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.8.21\"},{\"features\":[\"parsing\",\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.30\"}],\"features\":{\"default\":[\"serde\"],\"enable_unstable_features_that_may_break_with_minor_version_bumps\":[]}}",
|
||||
"png_0.17.16": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"getopts\",\"req\":\"^0.2.14\"},{\"default_features\":false,\"features\":[\"glutin\"],\"kind\":\"dev\",\"name\":\"glium\",\"req\":\"^0.32\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"term\",\"req\":\"^1.0.1\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"]}}",
|
||||
"png_0.18.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"],\"zlib-rs\":[\"flate2/zlib-rs\"]}}",
|
||||
"polling_3.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.2.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"hermit-abi\",\"req\":\"^0.5.0\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"features\":[\"event\",\"fs\",\"pipe\",\"process\",\"std\",\"time\"],\"name\":\"rustix\",\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"fuchsia\\\", target_os = \\\"vxworks\\\"))\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3.17\",\"target\":\"cfg(all(unix, not(target_os=\\\"vita\\\")))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_LibraryLoader\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
|
||||
"poly1305_0.8.0": "{\"dependencies\":[{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"universal-hash\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"universal-hash/std\"]}}",
|
||||
@@ -1333,6 +1337,7 @@
|
||||
"static_assertions_1.1.0": "{\"dependencies\":[],\"features\":{\"nightly\":[]}}",
|
||||
"stop-words_0.9.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"human_regex\",\"req\":\"^0.3.0\"},{\"kind\":\"build\",\"name\":\"serde_json\",\"req\":\"^1\"}],\"features\":{\"constructed\":[],\"default\":[\"iso\"],\"iso\":[],\"nltk\":[],\"unimplemented\":[]}}",
|
||||
"streaming-iterator_0.1.9": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}",
|
||||
"strict-num_0.1.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"float-cmp\",\"optional\":true,\"req\":\"^0.9\"}],\"features\":{\"approx-eq\":[\"float-cmp\"],\"default\":[\"approx-eq\"]}}",
|
||||
"string_cache_0.8.9": "{\"dependencies\":[{\"default_features\":false,\"name\":\"malloc_size_of\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"new_debug_unreachable\",\"req\":\"^1.0.2\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"phf_shared\",\"req\":\"^0.11\"},{\"name\":\"precomputed-hash\",\"req\":\"^0.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"serde_support\"],\"serde_support\":[\"serde\"]}}",
|
||||
"string_cache_codegen_0.5.4": "{\"dependencies\":[{\"name\":\"phf_generator\",\"req\":\"^0.11\"},{\"name\":\"phf_shared\",\"req\":\"^0.11\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"}],\"features\":{}}",
|
||||
"stringprep_0.1.5": "{\"dependencies\":[{\"name\":\"unicode-bidi\",\"req\":\"^0.3\"},{\"name\":\"unicode-normalization\",\"req\":\"^0.1\"},{\"name\":\"unicode-properties\",\"req\":\"^0.1.1\"}],\"features\":{}}",
|
||||
@@ -1380,6 +1385,8 @@
|
||||
"time-macros_0.2.27": "{\"dependencies\":[{\"name\":\"num-conv\",\"req\":\"^0.2.0\"},{\"name\":\"time-core\",\"req\":\"=0.1.8\"}],\"features\":{\"formatting\":[],\"large-dates\":[],\"parsing\":[],\"serde\":[]}}",
|
||||
"time_0.3.47": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8.1\",\"target\":\"cfg(bench)\"},{\"features\":[\"powerfmt\"],\"name\":\"deranged\",\"req\":\"^0.5.2\"},{\"name\":\"itoa\",\"optional\":true,\"req\":\"^1.0.1\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.58\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.98\",\"target\":\"cfg(target_family = \\\"unix\\\")\"},{\"name\":\"num-conv\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"num-conv\",\"req\":\"^0.2.0\"},{\"name\":\"num_threads\",\"optional\":true,\"req\":\"^0.1.2\",\"target\":\"cfg(target_family = \\\"unix\\\")\"},{\"default_features\":false,\"name\":\"powerfmt\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"rand08\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand08\",\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"name\":\"rand09\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.9.2\"},{\"default_features\":false,\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand09\",\"package\":\"rand\",\"req\":\"^0.9.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.26.1\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.7.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.184\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.68\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.126\"},{\"name\":\"time-core\",\"req\":\"=0.1.8\"},{\"name\":\"time-macros\",\"optional\":true,\"req\":\"=0.2.27\"},{\"kind\":\"dev\",\"name\":\"time-macros\",\"req\":\"=0.2.27\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.102\",\"target\":\"cfg(__ui_tests)\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\"],\"default\":[\"std\"],\"formatting\":[\"dep:itoa\",\"std\",\"time-macros?/formatting\"],\"large-dates\":[\"time-core/large-dates\",\"time-macros?/large-dates\"],\"local-offset\":[\"std\",\"dep:libc\",\"dep:num_threads\"],\"macros\":[\"dep:time-macros\"],\"parsing\":[\"time-macros?/parsing\"],\"quickcheck\":[\"dep:quickcheck\",\"alloc\",\"deranged/quickcheck\"],\"rand\":[\"rand08\",\"rand09\"],\"rand08\":[\"dep:rand08\",\"deranged/rand08\"],\"rand09\":[\"dep:rand09\",\"deranged/rand09\"],\"serde\":[\"dep:serde_core\",\"time-macros?/serde\",\"deranged/serde\"],\"serde-human-readable\":[\"serde\",\"formatting\",\"parsing\"],\"serde-well-known\":[\"serde\",\"formatting\",\"parsing\"],\"std\":[\"alloc\"],\"wasm-bindgen\":[\"dep:js-sys\"]}}",
|
||||
"tiny-keccak_2.0.2": "{\"dependencies\":[{\"name\":\"crunchy\",\"req\":\"^0.2.2\"}],\"features\":{\"cshake\":[],\"default\":[],\"fips202\":[\"keccak\",\"shake\",\"sha3\"],\"k12\":[],\"keccak\":[],\"kmac\":[\"cshake\"],\"parallel_hash\":[\"cshake\"],\"sha3\":[],\"shake\":[],\"sp800\":[\"cshake\",\"kmac\",\"tuple_hash\"],\"tuple_hash\":[\"cshake\"]}}",
|
||||
"tiny-skia-path_0.11.4": "{\"dependencies\":[{\"name\":\"arrayref\",\"req\":\"^0.3.6\"},{\"name\":\"bytemuck\",\"req\":\"^1.4\"},{\"name\":\"libm\",\"optional\":true,\"req\":\"^0.2.1\"},{\"default_features\":false,\"name\":\"strict-num\",\"req\":\"^0.1\"}],\"features\":{\"default\":[\"std\"],\"no-std-float\":[\"libm\"],\"std\":[]}}",
|
||||
"tiny-skia_0.11.4": "{\"dependencies\":[{\"name\":\"arrayref\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"name\":\"arrayvec\",\"req\":\"^0.7\"},{\"features\":[\"aarch64_simd\"],\"name\":\"bytemuck\",\"req\":\"^1.12\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"png\",\"optional\":true,\"req\":\"^0.17\"},{\"default_features\":false,\"name\":\"tiny-skia-path\",\"req\":\"^0.11.4\"}],\"features\":{\"default\":[\"std\",\"simd\",\"png-format\"],\"no-std-float\":[\"tiny-skia-path/no-std-float\"],\"png-format\":[\"std\",\"png\"],\"simd\":[],\"std\":[\"tiny-skia-path/std\"]}}",
|
||||
"tiny_http_0.12.0": "{\"dependencies\":[{\"name\":\"ascii\",\"req\":\"^1.0\"},{\"name\":\"chunked_transfer\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"fdlimit\",\"req\":\"^0.1\"},{\"name\":\"httpdate\",\"req\":\"^1.0.2\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"name\":\"openssl\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rustc-serialize\",\"req\":\"^0.3\"},{\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.20\"},{\"name\":\"rustls-pemfile\",\"optional\":true,\"req\":\"^0.2.1\"},{\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.6.0\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[],\"ssl\":[\"ssl-openssl\"],\"ssl-openssl\":[\"openssl\",\"zeroize\"],\"ssl-rustls\":[\"rustls\",\"rustls-pemfile\",\"zeroize\"]}}",
|
||||
"tinystr_0.8.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"default_features\":false,\"features\":[\"use-std\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.0.3\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"name\":\"zerovec\",\"optional\":true,\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\",\"zerovec?/alloc\"],\"databake\":[\"dep:databake\"],\"default\":[\"alloc\"],\"serde\":[\"dep:serde_core\"],\"std\":[],\"zerovec\":[\"dep:zerovec\"]}}",
|
||||
"tinyvec_1.10.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"generic-array\",\"optional\":true,\"req\":\"^1.1.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"smallvec\",\"req\":\"^1\"},{\"name\":\"tinyvec_macros\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{\"alloc\":[\"tinyvec_macros\"],\"debugger_visualizer\":[],\"default\":[],\"experimental_write_impl\":[],\"grab_spare_slice\":[],\"latest_stable_rust\":[\"rustc_1_61\"],\"nightly_slice_partition_dedup\":[],\"real_blackbox\":[\"criterion/real_blackbox\"],\"rustc_1_40\":[],\"rustc_1_55\":[],\"rustc_1_57\":[],\"rustc_1_61\":[\"rustc_1_57\"],\"std\":[\"alloc\"]}}",
|
||||
@@ -1423,6 +1430,7 @@
|
||||
"try-lock_0.2.5": "{\"dependencies\":[],\"features\":{}}",
|
||||
"ts-rs-macros_11.1.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0.28\"},{\"name\":\"termcolor\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"no-serde-warnings\":[],\"serde-compat\":[\"termcolor\"]}}",
|
||||
"ts-rs_11.1.0": "{\"dependencies\":[{\"features\":[\"serde\"],\"name\":\"bigdecimal\",\"optional\":true,\"req\":\">=0.0.13, <0.5\"},{\"name\":\"bson\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"name\":\"dprint-plugin-typescript\",\"optional\":true,\"req\":\"=0.95\"},{\"name\":\"heapless\",\"optional\":true,\"req\":\">=0.7, <0.9\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"ordered-float\",\"optional\":true,\"req\":\">=3, <6\"},{\"name\":\"semver\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"smol_str\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"sync\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.40\"},{\"name\":\"ts-rs-macros\",\"req\":\"=11.1.0\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"bigdecimal-impl\":[\"bigdecimal\"],\"bson-uuid-impl\":[\"bson\"],\"bytes-impl\":[\"bytes\"],\"chrono-impl\":[\"chrono\"],\"default\":[\"serde-compat\"],\"format\":[\"dprint-plugin-typescript\"],\"heapless-impl\":[\"heapless\"],\"import-esm\":[],\"indexmap-impl\":[\"indexmap\"],\"no-serde-warnings\":[\"ts-rs-macros/no-serde-warnings\"],\"ordered-float-impl\":[\"ordered-float\"],\"semver-impl\":[\"semver\"],\"serde-compat\":[\"ts-rs-macros/serde-compat\"],\"serde-json-impl\":[\"serde_json\"],\"smol_str-impl\":[\"smol_str\"],\"tokio-impl\":[\"tokio\"],\"url-impl\":[\"url\"],\"uuid-impl\":[\"uuid\"]}}",
|
||||
"ttf-parser_0.21.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"kind\":\"dev\",\"name\":\"pico-args\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"tiny-skia-path\",\"req\":\"^0.11.4\"},{\"kind\":\"dev\",\"name\":\"xmlwriter\",\"req\":\"^0.1\"}],\"features\":{\"apple-layout\":[],\"default\":[\"std\",\"opentype-layout\",\"apple-layout\",\"variable-fonts\",\"glyph-names\"],\"glyph-names\":[],\"gvar-alloc\":[\"std\"],\"opentype-layout\":[],\"std\":[],\"variable-fonts\":[]}}",
|
||||
"two-face_0.5.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cargo-lock\",\"req\":\"^10.1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.44.3\"},{\"default_features\":false,\"features\":[\"read\"],\"kind\":\"dev\",\"name\":\"object\",\"req\":\"^0.36.7\"},{\"name\":\"serde\",\"req\":\"^1.0.228\"},{\"name\":\"serde_derive\",\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2.7.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.26.3\"},{\"default_features\":false,\"features\":[\"dump-load\",\"parsing\"],\"name\":\"syntect\",\"req\":\"^5.3.0\"},{\"default_features\":false,\"features\":[\"html\"],\"kind\":\"dev\",\"name\":\"syntect\",\"req\":\"^5.3.0\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.23\"},{\"default_features\":false,\"features\":[\"std\",\"xxhash64\"],\"kind\":\"dev\",\"name\":\"twox-hash\",\"req\":\"^2.1.2\"}],\"features\":{\"default\":[\"syntect-onig\"],\"syntect-default-fancy\":[\"syntect-fancy\",\"syntect/default-fancy\"],\"syntect-default-onig\":[\"syntect-onig\",\"syntect/default-onig\"],\"syntect-fancy\":[\"syntect/regex-fancy\"],\"syntect-onig\":[\"syntect/regex-onig\"]}}",
|
||||
"type-map_0.5.1": "{\"dependencies\":[{\"name\":\"rustc-hash\",\"req\":\"^2\"}],\"features\":{}}",
|
||||
"typenum_1.19.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"scale-info\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"const-generics\":[],\"force_unix_path_separator\":[],\"i128\":[],\"no_std\":[],\"scale_info\":[\"scale-info/derive\"],\"strict\":[]}}",
|
||||
|
||||
80
codex-rs/Cargo.lock
generated
80
codex-rs/Cargo.lock
generated
@@ -459,6 +459,12 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
@@ -1543,15 +1549,16 @@ name = "codex-artifact-presentation"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"font8x8",
|
||||
"fontdue",
|
||||
"image",
|
||||
"ppt-rs",
|
||||
"pretty_assertions",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tiny_http",
|
||||
"tiny-skia",
|
||||
"uuid",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
@@ -4095,6 +4102,22 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "font8x8"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e"
|
||||
|
||||
[[package]]
|
||||
name = "fontdue"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -5092,7 +5115,7 @@ dependencies = [
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"png 0.18.0",
|
||||
"tiff",
|
||||
"zune-core 0.5.1",
|
||||
"zune-jpeg 0.5.12",
|
||||
@@ -7036,6 +7059,19 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.0"
|
||||
@@ -9519,6 +9555,12 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
|
||||
|
||||
[[package]]
|
||||
name = "strict-num"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
@@ -9998,6 +10040,32 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png 0.17.16",
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny_http"
|
||||
version = "0.12.0"
|
||||
@@ -10516,6 +10584,12 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.27.0"
|
||||
|
||||
@@ -218,6 +218,9 @@ os_info = "3.12.0"
|
||||
owo-colors = "4.3.0"
|
||||
path-absolutize = "3.1.1"
|
||||
pathdiff = "0.2"
|
||||
font8x8 = "0.3.1"
|
||||
fontdue = "0.9.3"
|
||||
tiny-skia = "0.11.4"
|
||||
portable-pty = "0.9.0"
|
||||
ppt-rs = "0.2.6"
|
||||
predicates = "3"
|
||||
|
||||
@@ -88,7 +88,6 @@ codex --sandbox danger-full-access
|
||||
```
|
||||
|
||||
The same setting can be persisted in `~/.codex/config.toml` via the top-level `sandbox_mode = "MODE"` key, e.g. `sandbox_mode = "workspace-write"`.
|
||||
In `workspace-write`, Codex also includes `~/.codex/memories` in its writable roots so memory maintenance does not require an extra approval.
|
||||
|
||||
## Code Organization
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
font8x8 = { workspace = true }
|
||||
fontdue = { workspace = true }
|
||||
image = { workspace = true, features = ["jpeg", "png"] }
|
||||
ppt-rs = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["blocking"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tiny-skia = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
zip = { workspace = true }
|
||||
@@ -25,4 +27,3 @@ zip = { workspace = true }
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tiny_http = { workspace = true }
|
||||
|
||||
@@ -4,9 +4,6 @@ use image::GenericImageView;
|
||||
use image::ImageFormat;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::imageops::FilterType;
|
||||
use ppt_rs::Chart;
|
||||
use ppt_rs::ChartSeries;
|
||||
use ppt_rs::ChartType;
|
||||
use ppt_rs::Hyperlink as PptHyperlink;
|
||||
use ppt_rs::HyperlinkAction as PptHyperlinkAction;
|
||||
use ppt_rs::Image;
|
||||
@@ -40,7 +37,6 @@ use std::io::Seek;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
use zip::ZipArchive;
|
||||
@@ -75,6 +71,8 @@ pub enum PresentationArtifactError {
|
||||
ImportFailed { path: PathBuf, message: String },
|
||||
#[error("failed to export PPTX `{path}`: {message}")]
|
||||
ExportFailed { path: PathBuf, message: String },
|
||||
#[error("failed to render preview for action `{action}`: {message}")]
|
||||
RenderFailed { action: String, message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@@ -163,8 +161,16 @@ impl PresentationArtifactRequest {
|
||||
}],
|
||||
ImageInputSource::DataUrl(_)
|
||||
| ImageInputSource::Blob(_)
|
||||
| ImageInputSource::Uri(_)
|
||||
| ImageInputSource::Placeholder => Vec::new(),
|
||||
ImageInputSource::Uri(uri) => {
|
||||
return Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: self.action.clone(),
|
||||
message: format!(
|
||||
"remote image URIs are not supported for `{}`; download the image locally or provide `data_url`/`blob` instead (`{uri}`)",
|
||||
self.action
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"replace_image" => {
|
||||
@@ -183,8 +189,16 @@ impl PresentationArtifactRequest {
|
||||
}],
|
||||
(None, Some(_), None, None, None)
|
||||
| (None, None, Some(_), None, None)
|
||||
| (None, None, None, Some(_), None)
|
||||
| (None, None, None, None, Some(_)) => Vec::new(),
|
||||
(None, None, None, Some(uri), None) => {
|
||||
return Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: self.action.clone(),
|
||||
message: format!(
|
||||
"remote image URIs are not supported for `{}`; download the image locally or provide `data_url`/`blob` instead (`{uri}`)",
|
||||
self.action
|
||||
),
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: self.action.clone(),
|
||||
|
||||
@@ -24,6 +24,13 @@ struct ExportPreviewArgs {
|
||||
quality: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct RenderPreviewArgs {
|
||||
slide_index: Option<u32>,
|
||||
scale: Option<f32>,
|
||||
include_background: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct AddSlideArgs {
|
||||
layout: Option<String>,
|
||||
|
||||
@@ -100,9 +100,11 @@ impl PresentationArtifactManager {
|
||||
"import_pptx" => self.import_pptx(request, cwd),
|
||||
"export_pptx" => self.export_pptx(request, cwd),
|
||||
"export_preview" => self.export_preview(request, cwd),
|
||||
"render_preview" => self.render_preview(request),
|
||||
"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),
|
||||
@@ -148,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),
|
||||
@@ -215,6 +219,9 @@ impl PresentationArtifactManager {
|
||||
}
|
||||
})?;
|
||||
let mut document = PresentationDocument::from_ppt_rs(imported);
|
||||
if let Some(slide_size) = import_pptx_slide_size(&path)? {
|
||||
document.slide_size = slide_size;
|
||||
}
|
||||
import_pptx_images(&path, &mut document, &request.action)?;
|
||||
document
|
||||
};
|
||||
@@ -286,82 +293,39 @@ impl PresentationArtifactManager {
|
||||
parse_preview_output_format(args.format.as_deref(), &output_path, &request.action)?;
|
||||
let scale = normalize_preview_scale(args.scale, &request.action)?;
|
||||
let quality = normalize_preview_quality(args.quality, &request.action)?;
|
||||
if let Some(parent) = output_path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: error.to_string(),
|
||||
let mut exported_paths = Vec::new();
|
||||
if let Some(slide_index) = args.slide_index {
|
||||
if let Some(parent) = output_path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: error.to_string(),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
let slide_index = usize::try_from(slide_index).map_err(|_| {
|
||||
PresentationArtifactError::InvalidArgs {
|
||||
action: request.action.clone(),
|
||||
message: "`slide_index` does not fit in usize".to_string(),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
let temp_dir =
|
||||
std::env::temp_dir().join(format!("presentation_preview_{}", Uuid::new_v4().simple()));
|
||||
std::fs::create_dir_all(&temp_dir).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: error.to_string(),
|
||||
}
|
||||
})?;
|
||||
let preview_document = if let Some(slide_index) = args.slide_index {
|
||||
let slide = document
|
||||
.slides
|
||||
.get(slide_index as usize)
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
index_out_of_range(&request.action, slide_index as usize, document.slides.len())
|
||||
})?;
|
||||
let slide_id = slide.slide_id.clone();
|
||||
PresentationDocument {
|
||||
artifact_id: document.artifact_id.clone(),
|
||||
name: document.name.clone(),
|
||||
slide_size: document.slide_size,
|
||||
theme: document.theme.clone(),
|
||||
custom_text_styles: document.custom_text_styles.clone(),
|
||||
layouts: Vec::new(),
|
||||
slides: vec![slide],
|
||||
active_slide_index: Some(0),
|
||||
comment_self: document.comment_self.clone(),
|
||||
comment_threads: document
|
||||
.comment_threads
|
||||
.iter()
|
||||
.filter(|thread| match &thread.target {
|
||||
CommentTarget::Slide { slide_id: target_slide_id }
|
||||
| CommentTarget::Element { slide_id: target_slide_id, .. }
|
||||
| CommentTarget::TextRange { slide_id: target_slide_id, .. } => {
|
||||
target_slide_id == &slide_id
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
next_slide_seq: 1,
|
||||
next_element_seq: 1,
|
||||
next_layout_seq: 1,
|
||||
next_text_range_seq: document.next_text_range_seq,
|
||||
next_comment_thread_seq: document.next_comment_thread_seq,
|
||||
next_comment_message_seq: document.next_comment_message_seq,
|
||||
}
|
||||
} else {
|
||||
document.clone()
|
||||
};
|
||||
write_preview_images(&preview_document, &temp_dir, &request.action)?;
|
||||
let mut exported_paths = collect_pngs(&temp_dir)?;
|
||||
if args.slide_index.is_some() {
|
||||
let rendered =
|
||||
exported_paths
|
||||
.pop()
|
||||
.ok_or_else(|| PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: "preview renderer produced no images".to_string(),
|
||||
})?;
|
||||
write_preview_image(
|
||||
&rendered,
|
||||
let png_bytes = render_slide_png(
|
||||
document,
|
||||
slide_index,
|
||||
RenderOptions {
|
||||
scale,
|
||||
include_background: true,
|
||||
},
|
||||
)?;
|
||||
write_preview_image_bytes(
|
||||
&png_bytes,
|
||||
&output_path,
|
||||
preview_format,
|
||||
scale,
|
||||
1.0,
|
||||
quality,
|
||||
&request.action,
|
||||
)?;
|
||||
exported_paths = vec![output_path];
|
||||
exported_paths.push(output_path);
|
||||
} else {
|
||||
std::fs::create_dir_all(&output_path).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
@@ -369,30 +333,27 @@ impl PresentationArtifactManager {
|
||||
message: error.to_string(),
|
||||
}
|
||||
})?;
|
||||
let mut relocated = Vec::new();
|
||||
for rendered in exported_paths {
|
||||
let filename = rendered.file_name().ok_or_else(|| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: "rendered preview had no filename".to_string(),
|
||||
}
|
||||
})?;
|
||||
let stem = Path::new(filename)
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("preview");
|
||||
let target = output_path.join(format!("{stem}.{}", preview_format.extension()));
|
||||
write_preview_image(
|
||||
&rendered,
|
||||
for slide_index in 0..document.slides.len() {
|
||||
let png_bytes = render_slide_png(
|
||||
document,
|
||||
slide_index,
|
||||
RenderOptions {
|
||||
scale,
|
||||
include_background: true,
|
||||
},
|
||||
)?;
|
||||
let target =
|
||||
output_path.join(format!("slide-{}.{}", slide_index + 1, preview_format.extension()));
|
||||
write_preview_image_bytes(
|
||||
&png_bytes,
|
||||
&target,
|
||||
preview_format,
|
||||
scale,
|
||||
1.0,
|
||||
quality,
|
||||
&request.action,
|
||||
)?;
|
||||
relocated.push(target);
|
||||
exported_paths.push(target);
|
||||
}
|
||||
exported_paths = relocated;
|
||||
}
|
||||
let mut response = PresentationArtifactResponse::new(
|
||||
artifact_id,
|
||||
@@ -404,6 +365,36 @@ impl PresentationArtifactManager {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn render_preview(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
||||
let args: RenderPreviewArgs = parse_args(&request.action, &request.args)?;
|
||||
let artifact_id = required_artifact_id(&request)?;
|
||||
let document = self.get_document(&artifact_id, &request.action)?;
|
||||
let slide_index = resolve_render_slide_index(document, args.slide_index, &request.action)?;
|
||||
let png_bytes = render_slide_png(
|
||||
document,
|
||||
slide_index,
|
||||
RenderOptions {
|
||||
scale: normalize_preview_scale(args.scale, &request.action)?,
|
||||
include_background: args.include_background.unwrap_or(true),
|
||||
},
|
||||
)?;
|
||||
let mut response = PresentationArtifactResponse::new(
|
||||
artifact_id,
|
||||
request.action,
|
||||
format!("Rendered preview for slide {}", slide_index + 1),
|
||||
snapshot_for_document(document),
|
||||
);
|
||||
response.rendered_preview = Some(RenderedPreview {
|
||||
slide_index,
|
||||
png_bytes,
|
||||
});
|
||||
response.active_slide_index = document.active_slide_index;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_summary(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
@@ -468,6 +459,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,
|
||||
@@ -1583,10 +1592,6 @@ impl PresentationArtifactManager {
|
||||
ImageInputSource::Uri(uri) => Some(load_image_payload_from_uri(&uri, "replace_image")?),
|
||||
ImageInputSource::Placeholder => None,
|
||||
};
|
||||
let fit_mode = args.fit.unwrap_or(ImageFitMode::Stretch);
|
||||
let lock_aspect_ratio = args
|
||||
.lock_aspect_ratio
|
||||
.unwrap_or(fit_mode != ImageFitMode::Stretch);
|
||||
let crop = args
|
||||
.crop
|
||||
.map(|crop| normalize_image_crop(crop, &request.action))
|
||||
@@ -1600,8 +1605,10 @@ impl PresentationArtifactManager {
|
||||
});
|
||||
};
|
||||
image.payload = image_payload;
|
||||
image.fit_mode = fit_mode;
|
||||
image.crop = crop;
|
||||
image.fit_mode = args.fit.unwrap_or(image.fit_mode);
|
||||
if let Some(crop) = crop {
|
||||
image.crop = Some(crop);
|
||||
}
|
||||
if let Some(rotation) = args.rotation {
|
||||
image.rotation_degrees = Some(rotation);
|
||||
}
|
||||
@@ -1611,9 +1618,15 @@ impl PresentationArtifactManager {
|
||||
if let Some(flip_vertical) = args.flip_vertical {
|
||||
image.flip_vertical = flip_vertical;
|
||||
}
|
||||
image.lock_aspect_ratio = lock_aspect_ratio;
|
||||
image.alt_text = args.alt;
|
||||
image.prompt = args.prompt;
|
||||
if let Some(lock_aspect_ratio) = args.lock_aspect_ratio {
|
||||
image.lock_aspect_ratio = lock_aspect_ratio;
|
||||
}
|
||||
if let Some(alt) = args.alt {
|
||||
image.alt_text = Some(alt);
|
||||
}
|
||||
if let Some(prompt) = args.prompt {
|
||||
image.prompt = Some(prompt);
|
||||
}
|
||||
image.is_placeholder = is_placeholder;
|
||||
Ok(PresentationArtifactResponse::new(
|
||||
artifact_id,
|
||||
@@ -1865,12 +1878,43 @@ impl PresentationArtifactManager {
|
||||
message: format!("element `{}` is not a table", args.element_id),
|
||||
});
|
||||
};
|
||||
let row_count = table.rows.len();
|
||||
let column_count = table.rows.first().map(Vec::len).unwrap_or(0);
|
||||
let start_row = args.start_row as usize;
|
||||
let end_row = args.end_row as usize;
|
||||
let start_column = args.start_column as usize;
|
||||
let end_column = args.end_column as usize;
|
||||
if start_row > end_row || start_column > end_column {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: request.action,
|
||||
message: "merge bounds must be ordered from top-left to bottom-right".to_string(),
|
||||
});
|
||||
}
|
||||
if end_row >= row_count || end_column >= column_count {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: request.action,
|
||||
message: format!(
|
||||
"merge bounds [{start_row},{start_column}]..=[{end_row},{end_column}] exceed table size {row_count}x{column_count}"
|
||||
),
|
||||
});
|
||||
}
|
||||
let region = TableMergeRegion {
|
||||
start_row: args.start_row as usize,
|
||||
end_row: args.end_row as usize,
|
||||
start_column: args.start_column as usize,
|
||||
end_column: args.end_column as usize,
|
||||
start_row,
|
||||
end_row,
|
||||
start_column,
|
||||
end_column,
|
||||
};
|
||||
if table.merges.iter().any(|merge| {
|
||||
merge.start_row <= region.end_row
|
||||
&& region.start_row <= merge.end_row
|
||||
&& merge.start_column <= region.end_column
|
||||
&& region.start_column <= merge.end_column
|
||||
}) {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: request.action,
|
||||
message: format!("merge region overlaps an existing merge in `{}`", args.element_id),
|
||||
});
|
||||
}
|
||||
table.merges.push(region);
|
||||
Ok(PresentationArtifactResponse::new(
|
||||
artifact_id,
|
||||
@@ -2527,6 +2571,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,
|
||||
@@ -2757,9 +2849,40 @@ impl PresentationArtifactManager {
|
||||
|| text_layout.wrap.is_some()
|
||||
|| text_layout.auto_fit.is_some()
|
||||
|| text_layout.vertical_alignment.is_some();
|
||||
let position_rotation = args
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|position| position.rotation);
|
||||
let position_flip_horizontal = args
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|position| position.flip_horizontal);
|
||||
let position_flip_vertical = args
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|position| position.flip_vertical);
|
||||
let has_position_transform = position_rotation.is_some()
|
||||
|| position_flip_horizontal.is_some()
|
||||
|| position_flip_vertical.is_some();
|
||||
let has_image_fields =
|
||||
args.fit.is_some() || args.crop.is_some() || args.lock_aspect_ratio.is_some();
|
||||
let element = document.find_element_mut(&args.element_id, &request.action)?;
|
||||
match element {
|
||||
PresentationElement::Text(text) => {
|
||||
if args.stroke.is_some()
|
||||
|| args.rotation.is_some()
|
||||
|| args.flip_horizontal.is_some()
|
||||
|| args.flip_vertical.is_some()
|
||||
|| has_position_transform
|
||||
|| has_image_fields
|
||||
{
|
||||
return Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: request.action,
|
||||
message:
|
||||
"text elements support only `position`, `z_order`, `fill`, and `text_layout` updates"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(position) = args.position {
|
||||
text.frame = apply_partial_position(text.frame, position);
|
||||
}
|
||||
@@ -2769,32 +2892,23 @@ impl PresentationArtifactManager {
|
||||
if has_text_layout {
|
||||
text.rich_text.layout = text_layout;
|
||||
}
|
||||
if args.stroke.is_some()
|
||||
|| args.rotation.is_some()
|
||||
|| args.flip_horizontal.is_some()
|
||||
|| args.flip_vertical.is_some()
|
||||
{
|
||||
}
|
||||
PresentationElement::Shape(shape) => {
|
||||
if has_image_fields {
|
||||
return Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: request.action,
|
||||
message:
|
||||
"text elements support only `position`, `z_order`, and `fill` updates"
|
||||
"shape elements support only `position`, `fill`, `stroke`, `rotation`, `flip_horizontal`, `flip_vertical`, `z_order`, and `text_layout` updates"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
PresentationElement::Shape(shape) => {
|
||||
let position_rotation = args
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|position| position.rotation);
|
||||
let position_flip_horizontal = args
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|position| position.flip_horizontal);
|
||||
let position_flip_vertical = args
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|position| position.flip_vertical);
|
||||
if has_text_layout && shape.text.is_none() {
|
||||
return Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: request.action,
|
||||
message: "shape elements without text do not support `text_layout` updates"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(position) = args.position {
|
||||
shape.frame = apply_partial_position(shape.frame, position);
|
||||
}
|
||||
@@ -2825,9 +2939,9 @@ impl PresentationArtifactManager {
|
||||
|| args.rotation.is_some()
|
||||
|| args.flip_horizontal.is_some()
|
||||
|| args.flip_vertical.is_some()
|
||||
|| args.fit.is_some()
|
||||
|| args.crop.is_some()
|
||||
|| args.lock_aspect_ratio.is_some()
|
||||
|| has_position_transform
|
||||
|| has_image_fields
|
||||
|| has_text_layout
|
||||
{
|
||||
return Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: request.action,
|
||||
@@ -2860,19 +2974,7 @@ impl PresentationArtifactManager {
|
||||
}
|
||||
}
|
||||
PresentationElement::Image(image) => {
|
||||
let position_rotation = args
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|position| position.rotation);
|
||||
let position_flip_horizontal = args
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|position| position.flip_horizontal);
|
||||
let position_flip_vertical = args
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|position| position.flip_vertical);
|
||||
if args.fill.is_some() || args.stroke.is_some() {
|
||||
if args.fill.is_some() || args.stroke.is_some() || has_text_layout {
|
||||
return Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: request.action,
|
||||
message:
|
||||
@@ -2911,6 +3013,9 @@ impl PresentationArtifactManager {
|
||||
|| args.rotation.is_some()
|
||||
|| args.flip_horizontal.is_some()
|
||||
|| args.flip_vertical.is_some()
|
||||
|| has_position_transform
|
||||
|| has_image_fields
|
||||
|| has_text_layout
|
||||
{
|
||||
return Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: request.action,
|
||||
@@ -2928,6 +3033,9 @@ impl PresentationArtifactManager {
|
||||
|| args.rotation.is_some()
|
||||
|| args.flip_horizontal.is_some()
|
||||
|| args.flip_vertical.is_some()
|
||||
|| has_position_transform
|
||||
|| has_image_fields
|
||||
|| has_text_layout
|
||||
{
|
||||
return Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: request.action,
|
||||
@@ -3031,6 +3139,7 @@ impl PresentationArtifactManager {
|
||||
proto_json: None,
|
||||
patch: None,
|
||||
active_slide_index: None,
|
||||
rendered_preview: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ include!("proto.rs");
|
||||
include!("inspect.rs");
|
||||
include!("pptx.rs");
|
||||
include!("snapshot.rs");
|
||||
include!("render.rs");
|
||||
|
||||
@@ -85,14 +85,14 @@ struct NamedTextStyle {
|
||||
built_in: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct HyperlinkState {
|
||||
target: HyperlinkTarget,
|
||||
tooltip: Option<String>,
|
||||
highlight_click: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum HyperlinkTarget {
|
||||
Url(String),
|
||||
Slide(u32),
|
||||
@@ -132,6 +132,44 @@ impl HyperlinkTarget {
|
||||
fn is_external(&self) -> bool {
|
||||
matches!(self, Self::Url(_) | Self::Email { .. } | Self::File(_))
|
||||
}
|
||||
|
||||
fn adjust_for_insert(&mut self, inserted_index: usize) {
|
||||
if let Self::Slide(slide_index) = self
|
||||
&& *slide_index as usize >= inserted_index
|
||||
{
|
||||
*slide_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn adjust_for_move(&mut self, from_index: usize, to_index: usize) {
|
||||
let Self::Slide(slide_index_ref) = self else {
|
||||
return;
|
||||
};
|
||||
let slide_index = *slide_index_ref as usize;
|
||||
*slide_index_ref = if slide_index == from_index {
|
||||
to_index as u32
|
||||
} else if from_index < to_index && (from_index + 1..=to_index).contains(&slide_index) {
|
||||
(slide_index - 1) as u32
|
||||
} else if to_index < from_index && (to_index..from_index).contains(&slide_index) {
|
||||
(slide_index + 1) as u32
|
||||
} else {
|
||||
*slide_index_ref
|
||||
};
|
||||
}
|
||||
|
||||
fn adjust_for_delete(&mut self, deleted_index: usize) -> bool {
|
||||
let Self::Slide(slide_index_ref) = self else {
|
||||
return false;
|
||||
};
|
||||
let slide_index = *slide_index_ref as usize;
|
||||
if slide_index == deleted_index {
|
||||
return true;
|
||||
}
|
||||
if slide_index > deleted_index {
|
||||
*slide_index_ref -= 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl HyperlinkState {
|
||||
@@ -217,6 +255,18 @@ impl HyperlinkState {
|
||||
ppt_rs::escape_xml(&self.target.relationship_target()),
|
||||
)
|
||||
}
|
||||
|
||||
fn adjust_for_insert(&mut self, inserted_index: usize) {
|
||||
self.target.adjust_for_insert(inserted_index);
|
||||
}
|
||||
|
||||
fn adjust_for_move(&mut self, from_index: usize, to_index: usize) {
|
||||
self.target.adjust_for_move(from_index, to_index);
|
||||
}
|
||||
|
||||
fn adjust_for_delete(&mut self, deleted_index: usize) -> bool {
|
||||
self.target.adjust_for_delete(deleted_index)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -285,7 +335,7 @@ enum TextVerticalAlignment {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CommentAuthorProfile {
|
||||
pub(super) struct CommentAuthorProfile {
|
||||
display_name: String,
|
||||
initials: String,
|
||||
email: Option<String>,
|
||||
@@ -332,7 +382,7 @@ enum CommentTarget {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CommentThread {
|
||||
pub(super) struct CommentThread {
|
||||
thread_id: String,
|
||||
target: CommentTarget,
|
||||
position: Option<CommentPosition>,
|
||||
@@ -714,6 +764,7 @@ impl PresentationDocument {
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
self.adjust_hyperlinks_for_insert(inserted_index);
|
||||
}
|
||||
|
||||
fn adjust_active_slide_for_move(&mut self, from_index: usize, to_index: usize) {
|
||||
@@ -728,6 +779,7 @@ impl PresentationDocument {
|
||||
active_index
|
||||
});
|
||||
}
|
||||
self.adjust_hyperlinks_for_move(from_index, to_index);
|
||||
}
|
||||
|
||||
fn adjust_active_slide_for_delete(&mut self, deleted_index: usize) {
|
||||
@@ -740,6 +792,36 @@ impl PresentationDocument {
|
||||
Some(active_index) if deleted_index < active_index => Some(active_index - 1),
|
||||
Some(active_index) => Some(active_index),
|
||||
};
|
||||
self.adjust_hyperlinks_for_delete(deleted_index);
|
||||
}
|
||||
|
||||
fn adjust_hyperlinks_for_insert(&mut self, inserted_index: usize) {
|
||||
self.visit_hyperlinks_mut(|hyperlink| {
|
||||
hyperlink.adjust_for_insert(inserted_index);
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
fn adjust_hyperlinks_for_move(&mut self, from_index: usize, to_index: usize) {
|
||||
self.visit_hyperlinks_mut(|hyperlink| {
|
||||
hyperlink.adjust_for_move(from_index, to_index);
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
fn adjust_hyperlinks_for_delete(&mut self, deleted_index: usize) {
|
||||
self.visit_hyperlinks_mut(|hyperlink| !hyperlink.adjust_for_delete(deleted_index));
|
||||
}
|
||||
|
||||
fn visit_hyperlinks_mut<F>(&mut self, mut visit: F)
|
||||
where
|
||||
F: FnMut(&mut HyperlinkState) -> bool,
|
||||
{
|
||||
for slide in &mut self.slides {
|
||||
for element in &mut slide.elements {
|
||||
element.visit_hyperlinks_mut(&mut visit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_layout_id(&mut self) -> String {
|
||||
@@ -926,16 +1008,16 @@ impl PresentationDocument {
|
||||
})
|
||||
}
|
||||
|
||||
fn to_ppt_rs(&self) -> Presentation {
|
||||
fn to_ppt_rs(&self) -> Result<Presentation, PresentationArtifactError> {
|
||||
let mut presentation = self
|
||||
.name
|
||||
.as_deref()
|
||||
.map(Presentation::with_title)
|
||||
.unwrap_or_default();
|
||||
for slide in &self.slides {
|
||||
presentation = presentation.add_slide(slide.to_ppt_rs(self.slide_size));
|
||||
presentation = presentation.add_slide(slide.to_ppt_rs(self.slide_size, self)?);
|
||||
}
|
||||
presentation
|
||||
Ok(presentation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1053,6 +1135,38 @@ fn import_pptx_images(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn import_pptx_slide_size(path: &Path) -> Result<Option<Rect>, PresentationArtifactError> {
|
||||
let file = std::fs::File::open(path).map_err(|error| PresentationArtifactError::ImportFailed {
|
||||
path: path.to_path_buf(),
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let mut archive =
|
||||
ZipArchive::new(file).map_err(|error| PresentationArtifactError::ImportFailed {
|
||||
path: path.to_path_buf(),
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let Some(xml) = zip_entry_string_if_exists(&mut archive, "ppt/presentation.xml").map_err(
|
||||
|message| PresentationArtifactError::ImportFailed {
|
||||
path: path.to_path_buf(),
|
||||
message,
|
||||
},
|
||||
)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(width) =
|
||||
xml_tag_attribute(&xml, "<p:sldSz", "cx").and_then(|value| value.parse::<u32>().ok())
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(height) =
|
||||
xml_tag_attribute(&xml, "<p:sldSz", "cy").and_then(|value| value.parse::<u32>().ok())
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(Rect::from_emu(0, 0, width, height)))
|
||||
}
|
||||
|
||||
fn zip_entry_string_if_exists<R: Read + Seek>(
|
||||
archive: &mut ZipArchive<R>,
|
||||
path: &str,
|
||||
@@ -1223,7 +1337,11 @@ fn xml_attribute(tag: &str, attribute: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
impl PresentationSlide {
|
||||
fn to_ppt_rs(&self, slide_size: Rect) -> SlideContent {
|
||||
fn to_ppt_rs(
|
||||
&self,
|
||||
slide_size: Rect,
|
||||
document: &PresentationDocument,
|
||||
) -> Result<SlideContent, PresentationArtifactError> {
|
||||
let mut content = SlideContent::new("").layout(SlideLayout::Blank);
|
||||
if self.notes.visible && !self.notes.text.is_empty() {
|
||||
content = content.notes(&self.notes.text);
|
||||
@@ -1391,24 +1509,22 @@ impl PresentationSlide {
|
||||
content = content.table(builder.build());
|
||||
}
|
||||
PresentationElement::Chart(chart) => {
|
||||
let mut ppt_chart = Chart::new(
|
||||
chart.title.as_deref().unwrap_or("Chart"),
|
||||
chart.chart_type.to_ppt_rs(),
|
||||
chart.categories,
|
||||
points_to_emu(chart.frame.left),
|
||||
points_to_emu(chart.frame.top),
|
||||
let chart_bytes = render_chart_png_bytes(document, &chart)?;
|
||||
let ppt_chart = Image::from_bytes(
|
||||
chart_bytes,
|
||||
points_to_emu(chart.frame.width),
|
||||
points_to_emu(chart.frame.height),
|
||||
"png",
|
||||
)
|
||||
.position(
|
||||
points_to_emu(chart.frame.left),
|
||||
points_to_emu(chart.frame.top),
|
||||
);
|
||||
for series in chart.series {
|
||||
ppt_chart =
|
||||
ppt_chart.add_series(ChartSeries::new(&series.name, series.values));
|
||||
}
|
||||
content = content.add_chart(ppt_chart);
|
||||
content = content.add_image(ppt_chart);
|
||||
}
|
||||
}
|
||||
}
|
||||
content
|
||||
Ok(content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1477,6 +1593,45 @@ impl PresentationElement {
|
||||
Self::Chart(element) => element.z_order = z_order,
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_hyperlinks_mut<F>(&mut self, visit: &mut F)
|
||||
where
|
||||
F: FnMut(&mut HyperlinkState) -> bool,
|
||||
{
|
||||
match self {
|
||||
Self::Text(element) => {
|
||||
if let Some(hyperlink) = element.hyperlink.as_mut()
|
||||
&& !visit(hyperlink)
|
||||
{
|
||||
element.hyperlink = None;
|
||||
}
|
||||
for range in &mut element.rich_text.ranges {
|
||||
if let Some(hyperlink) = range.hyperlink.as_mut()
|
||||
&& !visit(hyperlink)
|
||||
{
|
||||
range.hyperlink = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Shape(element) => {
|
||||
if let Some(hyperlink) = element.hyperlink.as_mut()
|
||||
&& !visit(hyperlink)
|
||||
{
|
||||
element.hyperlink = None;
|
||||
}
|
||||
if let Some(rich_text) = element.rich_text.as_mut() {
|
||||
for range in &mut rich_text.ranges {
|
||||
if let Some(hyperlink) = range.hyperlink.as_mut()
|
||||
&& !visit(hyperlink)
|
||||
{
|
||||
range.hyperlink = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Connector(_) | Self::Image(_) | Self::Table(_) | Self::Chart(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -1724,34 +1879,6 @@ enum ChartTypeSpec {
|
||||
Combo,
|
||||
}
|
||||
|
||||
impl ChartTypeSpec {
|
||||
fn to_ppt_rs(self) -> ChartType {
|
||||
match self {
|
||||
Self::Bar => ChartType::Bar,
|
||||
Self::BarHorizontal => ChartType::BarHorizontal,
|
||||
Self::BarStacked => ChartType::BarStacked,
|
||||
Self::BarStacked100 => ChartType::BarStacked100,
|
||||
Self::Line => ChartType::Line,
|
||||
Self::LineMarkers => ChartType::LineMarkers,
|
||||
Self::LineStacked => ChartType::LineStacked,
|
||||
Self::Pie => ChartType::Pie,
|
||||
Self::Doughnut => ChartType::Doughnut,
|
||||
Self::Area => ChartType::Area,
|
||||
Self::AreaStacked => ChartType::AreaStacked,
|
||||
Self::AreaStacked100 => ChartType::AreaStacked100,
|
||||
Self::Scatter => ChartType::Scatter,
|
||||
Self::ScatterLines => ChartType::ScatterLines,
|
||||
Self::ScatterSmooth => ChartType::ScatterSmooth,
|
||||
Self::Bubble => ChartType::Bubble,
|
||||
Self::Radar => ChartType::Radar,
|
||||
Self::RadarFilled => ChartType::RadarFilled,
|
||||
Self::StockHlc => ChartType::StockHLC,
|
||||
Self::StockOhlc => ChartType::StockOHLC,
|
||||
Self::Combo => ChartType::Combo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorKind {
|
||||
fn to_ppt_rs(self) -> ConnectorType {
|
||||
match self {
|
||||
|
||||
@@ -25,14 +25,18 @@ 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"
|
||||
| "get_style"
|
||||
| "describe_styles"
|
||||
| "record_patch"
|
||||
| "render_preview"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +44,7 @@ fn tracks_history(action: &str) -> bool {
|
||||
!is_read_only_action(action)
|
||||
&& !matches!(
|
||||
action,
|
||||
"export_pptx" | "export_preview" | "undo" | "redo" | "apply_patch"
|
||||
"export_pptx" | "export_preview" | "render_preview" | "undo" | "redo" | "apply_patch"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
2373
codex-rs/artifact-presentation/src/presentation_artifact/render.rs
Normal file
2373
codex-rs/artifact-presentation/src/presentation_artifact/render.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,14 @@ pub struct PresentationArtifactResponse {
|
||||
pub patch: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_slide_index: Option<usize>,
|
||||
#[serde(skip)]
|
||||
pub rendered_preview: Option<RenderedPreview>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderedPreview {
|
||||
pub slide_index: usize,
|
||||
pub png_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PresentationArtifactResponse {
|
||||
@@ -52,6 +60,7 @@ impl PresentationArtifactResponse {
|
||||
proto_json: None,
|
||||
patch: None,
|
||||
active_slide_index: None,
|
||||
rendered_preview: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +87,7 @@ fn response_for_document_state(
|
||||
proto_json: None,
|
||||
patch: None,
|
||||
active_slide_index: document.and_then(|current| current.active_slide_index),
|
||||
rendered_preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -223,54 +230,12 @@ fn load_image_payload_from_uri(
|
||||
uri: &str,
|
||||
action: &str,
|
||||
) -> Result<ImagePayload, PresentationArtifactError> {
|
||||
let response =
|
||||
reqwest::blocking::get(uri).map_err(|error| PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("failed to fetch image `{uri}`: {error}"),
|
||||
})?;
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("failed to fetch image `{uri}`: HTTP {status}"),
|
||||
});
|
||||
}
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(|value| value.split(';').next().unwrap_or(value).trim().to_string());
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.map_err(|error| PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("failed to read image `{uri}`: {error}"),
|
||||
})?;
|
||||
build_image_payload(
|
||||
bytes.to_vec(),
|
||||
infer_remote_image_filename(uri, content_type.as_deref()),
|
||||
action,
|
||||
)
|
||||
}
|
||||
|
||||
fn infer_remote_image_filename(uri: &str, content_type: Option<&str>) -> String {
|
||||
let path_name = reqwest::Url::parse(uri)
|
||||
.ok()
|
||||
.and_then(|url| {
|
||||
url.path_segments()
|
||||
.and_then(Iterator::last)
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.filter(|segment| !segment.is_empty());
|
||||
match (path_name, content_type) {
|
||||
(Some(path_name), _) if Path::new(&path_name).extension().is_some() => path_name,
|
||||
(Some(path_name), Some(content_type)) => {
|
||||
format!("{path_name}.{}", image_extension_from_mime(content_type))
|
||||
}
|
||||
(Some(path_name), None) => path_name,
|
||||
(None, Some(content_type)) => format!("image.{}", image_extension_from_mime(content_type)),
|
||||
(None, None) => "image.png".to_string(),
|
||||
}
|
||||
Err(PresentationArtifactError::UnsupportedFeature {
|
||||
action: action.to_string(),
|
||||
message: format!(
|
||||
"remote image URIs are not supported for `{action}`; download the image locally or provide `data_url`/`blob` instead (`{uri}`)"
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_image_payload(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1704,7 +1704,7 @@ impl SpreadsheetArtifactManager {
|
||||
let args: CreateDifferentialFormatArgs = parse_args(&request.action, &request.args)?;
|
||||
let artifact_id = required_artifact_id(&request)?;
|
||||
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||
let style_id = artifact.create_differential_format(args.format);
|
||||
let style_id = artifact.create_differential_format(args.format)?;
|
||||
let format = artifact
|
||||
.get_differential_format(style_id)
|
||||
.cloned()
|
||||
@@ -2265,6 +2265,7 @@ impl SpreadsheetArtifactManager {
|
||||
let action = request.action.clone();
|
||||
let artifact_id = required_artifact_id(&request)?;
|
||||
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||
artifact.validate_style_index(style_index, &request.action)?;
|
||||
{
|
||||
let sheet = artifact.sheet_lookup_mut(
|
||||
&request.action,
|
||||
|
||||
@@ -982,6 +982,9 @@ impl SpreadsheetSheet {
|
||||
for address in range.addresses() {
|
||||
let cell = self.get_or_create_cell_mut(address);
|
||||
cell.formula = formula.clone();
|
||||
if cell.formula.is_none() {
|
||||
cell.value = None;
|
||||
}
|
||||
}
|
||||
self.cells.retain(|_, cell| !cell.is_empty());
|
||||
Ok(())
|
||||
@@ -1094,6 +1097,9 @@ impl SpreadsheetSheet {
|
||||
};
|
||||
let cell = self.get_or_create_cell_mut(address);
|
||||
cell.formula = formula.clone();
|
||||
if cell.formula.is_none() {
|
||||
cell.value = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cells.retain(|_, cell| !cell.is_empty());
|
||||
@@ -1741,24 +1747,7 @@ impl SpreadsheetArtifact {
|
||||
}
|
||||
|
||||
match selected.as_str() {
|
||||
"xlsx" => {
|
||||
for sheet in &self.sheets {
|
||||
if !sheet.charts.is_empty()
|
||||
|| !sheet.tables.is_empty()
|
||||
|| !sheet.conditional_formats.is_empty()
|
||||
|| !sheet.pivot_tables.is_empty()
|
||||
{
|
||||
return Err(SpreadsheetArtifactError::ExportFailed {
|
||||
path: path.to_path_buf(),
|
||||
message: format!(
|
||||
"xlsx export does not yet support charts, tables, conditional formats, or pivot tables on sheet `{}`; use json or bin export instead",
|
||||
sheet.name
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
write_xlsx(self, path)
|
||||
}
|
||||
"xlsx" => write_xlsx(self, path),
|
||||
"json" => {
|
||||
let json = self.to_json()?;
|
||||
std::fs::write(path, json).map_err(|error| {
|
||||
|
||||
@@ -239,6 +239,18 @@ impl SpreadsheetNumberFormat {
|
||||
}
|
||||
|
||||
fn normalized(mut self) -> Self {
|
||||
if let Some(format_id) = self.format_id
|
||||
&& let Some(format_code) = builtin_number_format_code(format_id)
|
||||
{
|
||||
self.format_code = Some(format_code);
|
||||
return self;
|
||||
}
|
||||
if self.format_id.is_none() {
|
||||
self.format_id = self
|
||||
.format_code
|
||||
.as_deref()
|
||||
.and_then(builtin_number_format_id);
|
||||
}
|
||||
if self.format_code.is_none() {
|
||||
self.format_code = self.format_id.and_then(builtin_number_format_code);
|
||||
}
|
||||
@@ -493,6 +505,7 @@ impl SpreadsheetArtifact {
|
||||
} else {
|
||||
format
|
||||
};
|
||||
self.validate_cell_format_references(&created, "create_cell_format")?;
|
||||
Ok(insert_with_next_id(&mut self.cell_formats, created))
|
||||
}
|
||||
|
||||
@@ -500,8 +513,12 @@ impl SpreadsheetArtifact {
|
||||
self.cell_formats.get(&format_id)
|
||||
}
|
||||
|
||||
pub fn create_differential_format(&mut self, format: SpreadsheetDifferentialFormat) -> u32 {
|
||||
insert_with_next_id(&mut self.differential_formats, format)
|
||||
pub fn create_differential_format(
|
||||
&mut self,
|
||||
format: SpreadsheetDifferentialFormat,
|
||||
) -> Result<u32, SpreadsheetArtifactError> {
|
||||
self.validate_differential_format_references(&format, "create_differential_format")?;
|
||||
Ok(insert_with_next_id(&mut self.differential_formats, format))
|
||||
}
|
||||
|
||||
pub fn get_differential_format(
|
||||
@@ -534,6 +551,70 @@ impl SpreadsheetArtifact {
|
||||
wrap_text: resolved.wrap_text,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_style_index(
|
||||
&self,
|
||||
style_index: u32,
|
||||
action: &str,
|
||||
) -> Result<(), SpreadsheetArtifactError> {
|
||||
if style_index == 0 || self.cell_formats.contains_key(&style_index) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(SpreadsheetArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("style index `{style_index}` was not found"),
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_cell_format_references(
|
||||
&self,
|
||||
format: &SpreadsheetCellFormat,
|
||||
action: &str,
|
||||
) -> Result<(), SpreadsheetArtifactError> {
|
||||
validate_optional_reference(
|
||||
format.text_style_id,
|
||||
&self.text_styles,
|
||||
"text style",
|
||||
action,
|
||||
)?;
|
||||
validate_optional_reference(format.fill_id, &self.fills, "fill", action)?;
|
||||
validate_optional_reference(format.border_id, &self.borders, "border", action)?;
|
||||
validate_optional_reference(
|
||||
format.number_format_id,
|
||||
&self.number_formats,
|
||||
"number format",
|
||||
action,
|
||||
)?;
|
||||
validate_optional_reference(
|
||||
format.base_cell_style_format_id,
|
||||
&self.cell_formats,
|
||||
"base cell format",
|
||||
action,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_differential_format_references(
|
||||
&self,
|
||||
format: &SpreadsheetDifferentialFormat,
|
||||
action: &str,
|
||||
) -> Result<(), SpreadsheetArtifactError> {
|
||||
validate_optional_reference(
|
||||
format.text_style_id,
|
||||
&self.text_styles,
|
||||
"text style",
|
||||
action,
|
||||
)?;
|
||||
validate_optional_reference(format.fill_id, &self.fills, "fill", action)?;
|
||||
validate_optional_reference(format.border_id, &self.borders, "border", action)?;
|
||||
validate_optional_reference(
|
||||
format.number_format_id,
|
||||
&self.number_formats,
|
||||
"number format",
|
||||
action,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SpreadsheetSheet {
|
||||
@@ -566,6 +647,23 @@ fn resolve_cell_format_recursive(
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_optional_reference<T>(
|
||||
id: Option<u32>,
|
||||
map: &BTreeMap<u32, T>,
|
||||
kind: &str,
|
||||
action: &str,
|
||||
) -> Result<(), SpreadsheetArtifactError> {
|
||||
if let Some(id) = id
|
||||
&& !map.contains_key(&id)
|
||||
{
|
||||
return Err(SpreadsheetArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("{kind} `{id}` was not found"),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn builtin_number_format_code(format_id: u32) -> Option<String> {
|
||||
match format_id {
|
||||
0 => Some("General".to_string()),
|
||||
@@ -578,3 +676,16 @@ fn builtin_number_format_code(format_id: u32) -> Option<String> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn builtin_number_format_id(format_code: &str) -> Option<u32> {
|
||||
match format_code {
|
||||
"General" => Some(0),
|
||||
"0" => Some(1),
|
||||
"0.00" => Some(2),
|
||||
"#,##0" => Some(3),
|
||||
"#,##0.00" => Some(4),
|
||||
"0%" => Some(9),
|
||||
"0.00%" => Some(10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4454,9 +4454,11 @@ mod handlers {
|
||||
}
|
||||
|
||||
let memory_root = crate::memories::memory_root(&config.codex_home);
|
||||
if let Err(err) = crate::memories::clear_memory_root_contents(&memory_root).await {
|
||||
if let Err(err) = tokio::fs::remove_dir_all(&memory_root).await
|
||||
&& err.kind() != std::io::ErrorKind::NotFound
|
||||
{
|
||||
errors.push(format!(
|
||||
"failed clearing memory directory {}: {err}",
|
||||
"failed removing memory directory {}: {err}",
|
||||
memory_root.display()
|
||||
));
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ use crate::features::FeatureOverrides;
|
||||
use crate::features::Features;
|
||||
use crate::features::FeaturesToml;
|
||||
use crate::git_info::resolve_root_git_project_for_trust;
|
||||
use crate::memories::memory_root;
|
||||
use crate::model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
|
||||
use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
|
||||
use crate::model_provider_info::ModelProviderInfo;
|
||||
@@ -1806,15 +1805,6 @@ impl Config {
|
||||
Some(&constrained_sandbox_policy),
|
||||
);
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||
let memories_root = memory_root(&codex_home);
|
||||
std::fs::create_dir_all(&memories_root)?;
|
||||
let memories_root = AbsolutePathBuf::from_absolute_path(&memories_root)?;
|
||||
if !writable_roots
|
||||
.iter()
|
||||
.any(|existing| existing == &memories_root)
|
||||
{
|
||||
writable_roots.push(memories_root);
|
||||
}
|
||||
for path in additional_writable_roots {
|
||||
if !writable_roots.iter().any(|existing| existing == &path) {
|
||||
writable_roots.push(path);
|
||||
@@ -3166,56 +3156,6 @@ trust_level = "trusted"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let memories_root = codex_home.path().join("memories");
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
sandbox_workspace_write: Some(SandboxWorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::from_absolute_path(&memories_root)?],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
match config.permissions.sandbox_policy.get() {
|
||||
SandboxPolicy::ReadOnly { .. } => {}
|
||||
other => panic!("expected read-only policy on Windows, got {other:?}"),
|
||||
}
|
||||
} else {
|
||||
assert!(
|
||||
memories_root.is_dir(),
|
||||
"expected memories root directory to exist at {}",
|
||||
memories_root.display()
|
||||
);
|
||||
let expected_memories_root = AbsolutePathBuf::from_absolute_path(&memories_root)?;
|
||||
match config.permissions.sandbox_policy.get() {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
assert_eq!(
|
||||
writable_roots
|
||||
.iter()
|
||||
.filter(|root| **root == expected_memories_root)
|
||||
.count(),
|
||||
1,
|
||||
"expected single writable root entry for {}",
|
||||
expected_memories_root.display()
|
||||
);
|
||||
}
|
||||
other => panic!("expected workspace-write policy, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) async fn clear_memory_root_contents(memory_root: &Path) -> std::io::Result<()> {
|
||||
match tokio::fs::symlink_metadata(memory_root).await {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"refusing to clear symlinked memory root {}",
|
||||
memory_root.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
tokio::fs::create_dir_all(memory_root).await?;
|
||||
|
||||
let mut entries = tokio::fs::read_dir(memory_root).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
let file_type = entry.file_type().await?;
|
||||
if file_type.is_dir() {
|
||||
tokio::fs::remove_dir_all(path).await?;
|
||||
} else {
|
||||
tokio::fs::remove_file(path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
//! - Phase 2: claim a global consolidation lock, materialize consolidation inputs, and dispatch one consolidation agent.
|
||||
|
||||
pub(crate) mod citations;
|
||||
mod control;
|
||||
mod phase1;
|
||||
mod phase2;
|
||||
pub(crate) mod prompts;
|
||||
@@ -17,7 +16,6 @@ pub(crate) mod usage;
|
||||
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
|
||||
pub(crate) use control::clear_memory_root_contents;
|
||||
/// Starts the memory startup pipeline for eligible root sessions.
|
||||
/// This is the single entrypoint that `codex` uses to trigger memory startup.
|
||||
///
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::storage::rebuild_raw_memories_file_from_memories;
|
||||
use super::storage::sync_rollout_summaries_from_memories;
|
||||
use crate::config::types::DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION;
|
||||
use crate::memories::clear_memory_root_contents;
|
||||
use crate::memories::ensure_layout;
|
||||
use crate::memories::memory_root;
|
||||
use crate::memories::raw_memories_file;
|
||||
@@ -64,72 +63,6 @@ fn stage_one_output_schema_requires_rollout_slug_and_keeps_it_nullable() {
|
||||
assert_eq!(rollout_slug_types, vec!["null", "string"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clear_memory_root_contents_preserves_root_directory() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let root = dir.path().join("memory");
|
||||
let nested_dir = root.join("rollout_summaries");
|
||||
tokio::fs::create_dir_all(&nested_dir)
|
||||
.await
|
||||
.expect("create rollout summaries dir");
|
||||
tokio::fs::write(root.join("MEMORY.md"), "stale memory index\n")
|
||||
.await
|
||||
.expect("write memory index");
|
||||
tokio::fs::write(nested_dir.join("rollout.md"), "stale rollout\n")
|
||||
.await
|
||||
.expect("write rollout summary");
|
||||
|
||||
clear_memory_root_contents(&root)
|
||||
.await
|
||||
.expect("clear memory root contents");
|
||||
|
||||
assert!(
|
||||
tokio::fs::try_exists(&root)
|
||||
.await
|
||||
.expect("check memory root existence"),
|
||||
"memory root should still exist after clearing contents"
|
||||
);
|
||||
let mut entries = tokio::fs::read_dir(&root)
|
||||
.await
|
||||
.expect("read memory root after clear");
|
||||
assert!(
|
||||
entries
|
||||
.next_entry()
|
||||
.await
|
||||
.expect("read next entry")
|
||||
.is_none(),
|
||||
"memory root should be empty after clearing contents"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn clear_memory_root_contents_rejects_symlinked_root() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let target = dir.path().join("outside");
|
||||
tokio::fs::create_dir_all(&target)
|
||||
.await
|
||||
.expect("create symlink target dir");
|
||||
let target_file = target.join("keep.txt");
|
||||
tokio::fs::write(&target_file, "keep\n")
|
||||
.await
|
||||
.expect("write target file");
|
||||
|
||||
let root = dir.path().join("memory");
|
||||
std::os::unix::fs::symlink(&target, &root).expect("create memory root symlink");
|
||||
|
||||
let err = clear_memory_root_contents(&root)
|
||||
.await
|
||||
.expect_err("symlinked memory root should be rejected");
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert!(
|
||||
tokio::fs::try_exists(&target_file)
|
||||
.await
|
||||
.expect("check target file existence"),
|
||||
"rejecting a symlinked memory root should not delete the symlink target"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
|
||||
@@ -74,7 +74,6 @@ pub(crate) struct TurnState {
|
||||
pending_user_input: HashMap<String, oneshot::Sender<RequestUserInputResponse>>,
|
||||
pending_dynamic_tools: HashMap<String, oneshot::Sender<DynamicToolResponse>>,
|
||||
pending_input: Vec<ResponseInputItem>,
|
||||
pub(crate) tool_calls: u64,
|
||||
pub(crate) token_usage_at_turn_start: TokenUsage,
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
|
||||
use crate::features::Feature;
|
||||
pub(crate) use compact::CompactTask;
|
||||
pub(crate) use ghost_snapshot::GhostSnapshotTask;
|
||||
pub(crate) use regular::RegularTask;
|
||||
@@ -201,13 +200,11 @@ impl Session {
|
||||
let mut pending_input = Vec::<ResponseInputItem>::new();
|
||||
let mut should_clear_active_turn = false;
|
||||
let mut token_usage_at_turn_start = None;
|
||||
let mut turn_tool_calls = 0_u64;
|
||||
if let Some(at) = active.as_mut()
|
||||
&& at.remove_task(&turn_context.sub_id)
|
||||
{
|
||||
let mut ts = at.turn_state.lock().await;
|
||||
pending_input = ts.take_pending_input();
|
||||
turn_tool_calls = ts.tool_calls;
|
||||
token_usage_at_turn_start = Some(ts.token_usage_at_turn_start.clone());
|
||||
should_clear_active_turn = true;
|
||||
}
|
||||
@@ -242,20 +239,6 @@ impl Session {
|
||||
}
|
||||
// Emit token usage metrics.
|
||||
if let Some(token_usage_at_turn_start) = token_usage_at_turn_start {
|
||||
// TODO(jif): drop this
|
||||
let tmp_mem = (
|
||||
"tmp_mem_enabled",
|
||||
if self.enabled(Feature::MemoryTool) {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
},
|
||||
);
|
||||
self.services.otel_manager.histogram(
|
||||
"codex.turn.tool.call",
|
||||
i64::try_from(turn_tool_calls).unwrap_or(i64::MAX),
|
||||
&[tmp_mem],
|
||||
);
|
||||
let total_token_usage = self.total_token_usage().await.unwrap_or_default();
|
||||
let turn_token_usage = crate::protocol::TokenUsage {
|
||||
input_tokens: (total_token_usage.input_tokens
|
||||
@@ -277,27 +260,27 @@ impl Session {
|
||||
self.services.otel_manager.histogram(
|
||||
"codex.turn.token_usage",
|
||||
turn_token_usage.total_tokens,
|
||||
&[("token_type", "total"), tmp_mem],
|
||||
&[("token_type", "total")],
|
||||
);
|
||||
self.services.otel_manager.histogram(
|
||||
"codex.turn.token_usage",
|
||||
turn_token_usage.input_tokens,
|
||||
&[("token_type", "input"), tmp_mem],
|
||||
&[("token_type", "input")],
|
||||
);
|
||||
self.services.otel_manager.histogram(
|
||||
"codex.turn.token_usage",
|
||||
turn_token_usage.cached_input(),
|
||||
&[("token_type", "cached_input"), tmp_mem],
|
||||
&[("token_type", "cached_input")],
|
||||
);
|
||||
self.services.otel_manager.histogram(
|
||||
"codex.turn.token_usage",
|
||||
turn_token_usage.output_tokens,
|
||||
&[("token_type", "output"), tmp_mem],
|
||||
&[("token_type", "output")],
|
||||
);
|
||||
self.services.otel_manager.histogram(
|
||||
"codex.turn.token_usage",
|
||||
turn_token_usage.reasoning_output_tokens,
|
||||
&[("token_type", "reasoning_output"), tmp_mem],
|
||||
&[("token_type", "reasoning_output")],
|
||||
);
|
||||
}
|
||||
let event = EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_artifact_presentation::PathAccessKind;
|
||||
use codex_artifact_presentation::PathAccessRequirement;
|
||||
use codex_artifact_presentation::PresentationArtifactError;
|
||||
use codex_artifact_presentation::PresentationArtifactToolRequest;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use serde_json::to_string;
|
||||
@@ -68,6 +72,19 @@ impl ToolHandler for PresentationArtifactHandler {
|
||||
};
|
||||
|
||||
let request: PresentationArtifactToolRequest = parse_arguments(&arguments)?;
|
||||
if request
|
||||
.actions
|
||||
.iter()
|
||||
.any(|action| action.action == "render_preview")
|
||||
&& !turn
|
||||
.model_info
|
||||
.input_modalities
|
||||
.contains(&InputModality::Image)
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"render_preview is not allowed because you do not support image inputs".to_string(),
|
||||
));
|
||||
}
|
||||
for access in request
|
||||
.required_path_accesses(&turn.cwd)
|
||||
.map_err(presentation_error)?
|
||||
@@ -85,6 +102,26 @@ impl ToolHandler for PresentationArtifactHandler {
|
||||
.await
|
||||
.map_err(presentation_error)?;
|
||||
|
||||
if let Some(preview) = response.rendered_preview.as_ref() {
|
||||
let summary = format!(
|
||||
"{} Artifact `{}` slide {}.",
|
||||
response.summary,
|
||||
response.artifact_id,
|
||||
preview.slide_index + 1
|
||||
);
|
||||
let image_url = format!(
|
||||
"data:image/png;base64,{}",
|
||||
BASE64_STANDARD.encode(&preview.png_bytes)
|
||||
);
|
||||
return Ok(ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::ContentItems(vec![
|
||||
FunctionCallOutputContentItem::InputText { text: summary },
|
||||
FunctionCallOutputContentItem::InputImage { image_url },
|
||||
]),
|
||||
success: Some(true),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::Text(to_string(&response).map_err(|error| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
|
||||
@@ -118,14 +118,6 @@ impl ToolRegistry {
|
||||
let mcp_server_ref = mcp_server.as_deref();
|
||||
let mcp_server_origin_ref = mcp_server_origin.as_deref();
|
||||
|
||||
{
|
||||
let mut active = invocation.session.active_turn.lock().await;
|
||||
if let Some(active_turn) = active.as_mut() {
|
||||
let mut turn_state = active_turn.turn_state.lock().await;
|
||||
turn_state.tool_calls = turn_state.tool_calls.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
let handler = match self.handler(tool_name.as_ref()) {
|
||||
Some(handler) => handler,
|
||||
None => {
|
||||
|
||||
@@ -57,7 +57,6 @@ pub(crate) struct ToolsConfig {
|
||||
pub js_repl_tools_only: bool,
|
||||
pub collab_tools: bool,
|
||||
pub artifact_tools: bool,
|
||||
pub request_user_input: bool,
|
||||
pub default_mode_request_user_input: bool,
|
||||
pub experimental_supported_tools: Vec<String>,
|
||||
pub agent_jobs_tools: bool,
|
||||
@@ -84,9 +83,8 @@ impl ToolsConfig {
|
||||
let include_js_repl_tools_only =
|
||||
include_js_repl && features.enabled(Feature::JsReplToolsOnly);
|
||||
let include_collab_tools = features.enabled(Feature::Collab);
|
||||
let include_request_user_input = !matches!(session_source, SessionSource::SubAgent(_));
|
||||
let include_default_mode_request_user_input =
|
||||
include_request_user_input && features.enabled(Feature::DefaultModeRequestUserInput);
|
||||
features.enabled(Feature::DefaultModeRequestUserInput);
|
||||
let include_search_tool = features.enabled(Feature::Apps);
|
||||
let include_artifact_tools = features.enabled(Feature::Artifact);
|
||||
let include_image_gen_tool =
|
||||
@@ -148,7 +146,6 @@ impl ToolsConfig {
|
||||
js_repl_tools_only: include_js_repl_tools_only,
|
||||
collab_tools: include_collab_tools,
|
||||
artifact_tools: include_artifact_tools,
|
||||
request_user_input: include_request_user_input,
|
||||
default_mode_request_user_input: include_default_mode_request_user_input,
|
||||
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
|
||||
agent_jobs_tools: include_agent_jobs,
|
||||
@@ -1880,12 +1877,10 @@ pub(crate) fn build_specs(
|
||||
builder.register_handler("js_repl_reset", js_repl_reset_handler);
|
||||
}
|
||||
|
||||
if config.request_user_input {
|
||||
builder.push_spec(create_request_user_input_tool(CollaborationModesConfig {
|
||||
default_mode_request_user_input: config.default_mode_request_user_input,
|
||||
}));
|
||||
builder.register_handler("request_user_input", request_user_input_handler);
|
||||
}
|
||||
builder.push_spec(create_request_user_input_tool(CollaborationModesConfig {
|
||||
default_mode_request_user_input: config.default_mode_request_user_input,
|
||||
}));
|
||||
builder.register_handler("request_user_input", request_user_input_handler);
|
||||
|
||||
if config.search_tool
|
||||
&& let Some(app_tools) = app_tools
|
||||
@@ -2120,17 +2115,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) {
|
||||
let names = tools
|
||||
.iter()
|
||||
.map(|tool| tool_name(&tool.spec))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
!names.contains(&expected_absent),
|
||||
"expected tool {expected_absent} to be absent; had: {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn shell_tool_name(config: &ToolsConfig) -> Option<&'static str> {
|
||||
match config.shell_type {
|
||||
ConfigShellToolType::Default => Some("shell"),
|
||||
@@ -2333,7 +2317,6 @@ mod tests {
|
||||
"report_agent_job_result",
|
||||
],
|
||||
);
|
||||
assert_lacks_tool_name(&tools, "request_user_input");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -156,7 +156,6 @@ rollout_summaries/2026-02-17T21-23-02-LN3m-weekly_memory_report_pivot_from_git_h
|
||||
- an empty `<rollout_ids>` section is allowed if no rollout ids are available
|
||||
- you can find rollout ids in rollout summary files and MEMORY.md
|
||||
- do not include file paths or notes in this section
|
||||
- For every `citation_entries`, try to find and cite the corresponding rollout id if possible
|
||||
- Never include memory citations inside pull-request messages.
|
||||
- Never cite blank lines; double-check ranges.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -198,3 +211,10 @@ Example preview:
|
||||
|
||||
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}}]}`
|
||||
|
||||
For model-visible previews, use `render_preview`. It renders a slide natively and returns inline image output instead of writing a file.
|
||||
|
||||
Example inline preview:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"render_preview","args":{"slide_index":0,"scale":1.0}}]}`
|
||||
|
||||
`render_preview` defaults to the active slide when `slide_index` is omitted. It supports `scale` and `include_background`.
|
||||
|
||||
@@ -96,6 +96,7 @@ mod permissions_messages;
|
||||
mod personality;
|
||||
mod personality_migration;
|
||||
mod plugins;
|
||||
mod presentation_artifact;
|
||||
mod prompt_caching;
|
||||
mod quota_exceeded;
|
||||
mod read_file;
|
||||
|
||||
285
codex-rs/core/tests/suite/presentation_artifact.rs
Normal file
285
codex-rs/core/tests/suite/presentation_artifact.rs
Normal file
@@ -0,0 +1,285 @@
|
||||
#![cfg(not(target_os = "windows"))]
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::features::Feature;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelVisibility;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::openai_models::TruncationPolicyConfig;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_models_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use image::GenericImageView;
|
||||
use image::load_from_memory;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use wiremock::BodyPrintLimit;
|
||||
use wiremock::MockServer;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn presentation_artifact_render_preview_returns_inline_image() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
session_configured,
|
||||
..
|
||||
} = test_codex()
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::Artifact);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
let call_id = "presentation-render-preview";
|
||||
let arguments = serde_json::json!({
|
||||
"actions": [
|
||||
{
|
||||
"action": "create",
|
||||
"args": {
|
||||
"name": "Preview",
|
||||
"theme": {
|
||||
"color_scheme": {
|
||||
"bg1": "F6F2E8",
|
||||
"tx1": "1B1B1B"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "add_slide",
|
||||
"args": {
|
||||
"background_fill": "#F1E6D6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "add_shape",
|
||||
"args": {
|
||||
"slide_index": 0,
|
||||
"geometry": "rectangle",
|
||||
"position": { "left": 72, "top": 120, "width": 160, "height": 80 },
|
||||
"fill": "#C95A3D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "render_preview",
|
||||
"args": {}
|
||||
}
|
||||
]
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(call_id, "presentation_artifact", &arguments),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, first_response).await;
|
||||
|
||||
let second_response = sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]);
|
||||
let mock = responses::mount_sse_once(&server, second_response).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![UserInput::Text {
|
||||
text: "render the deck preview".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_configured.model.clone(),
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let function_output = mock.single_request().function_call_output(call_id);
|
||||
let output_items = function_output
|
||||
.get("output")
|
||||
.and_then(Value::as_array)
|
||||
.expect("render_preview output should be content items");
|
||||
assert_eq!(output_items.len(), 2);
|
||||
assert_eq!(
|
||||
output_items[0].get("type").and_then(Value::as_str),
|
||||
Some("input_text")
|
||||
);
|
||||
assert_eq!(
|
||||
output_items[1].get("type").and_then(Value::as_str),
|
||||
Some("input_image")
|
||||
);
|
||||
let image_url = output_items[1]
|
||||
.get("image_url")
|
||||
.and_then(Value::as_str)
|
||||
.expect("preview image_url present");
|
||||
let (prefix, payload) = image_url
|
||||
.split_once(',')
|
||||
.expect("preview image contains data prefix");
|
||||
assert_eq!(prefix, "data:image/png;base64");
|
||||
|
||||
let decoded = BASE64_STANDARD.decode(payload)?;
|
||||
let rendered = load_from_memory(&decoded)?;
|
||||
assert_eq!(rendered.dimensions(), (720, 540));
|
||||
assert_eq!(rendered.get_pixel(20, 20).0, [0xF1, 0xE6, 0xD6, 0xFF]);
|
||||
assert_eq!(rendered.get_pixel(100, 150).0, [0xC9, 0x5A, 0x3D, 0xFF]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn presentation_artifact_render_preview_fails_for_text_only_model() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = MockServer::builder()
|
||||
.body_print_limit(BodyPrintLimit::Limited(80_000))
|
||||
.start()
|
||||
.await;
|
||||
let model_slug = "text-only-presentation-preview-test-model";
|
||||
let text_only_model = ModelInfo {
|
||||
slug: model_slug.to_string(),
|
||||
display_name: "Text-only presentation preview test model".to_string(),
|
||||
description: Some(
|
||||
"Remote model for presentation preview unsupported-path coverage".to_string(),
|
||||
),
|
||||
default_reasoning_level: Some(ReasoningEffort::Medium),
|
||||
supported_reasoning_levels: vec![ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Medium,
|
||||
description: ReasoningEffort::Medium.to_string(),
|
||||
}],
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
input_modalities: vec![InputModality::Text],
|
||||
prefer_websockets: false,
|
||||
used_fallback_model_metadata: false,
|
||||
priority: 1,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
supports_reasoning_summaries: false,
|
||||
default_reasoning_summary: ReasoningSummary::Auto,
|
||||
support_verbosity: false,
|
||||
default_verbosity: None,
|
||||
availability_nux: None,
|
||||
apply_patch_tool_type: None,
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
supports_parallel_tool_calls: false,
|
||||
context_window: Some(272_000),
|
||||
auto_compact_token_limit: None,
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
};
|
||||
mount_models_once(
|
||||
&server,
|
||||
ModelsResponse {
|
||||
models: vec![text_only_model],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let TestCodex { codex, cwd, .. } = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::Artifact);
|
||||
config.model = Some(model_slug.to_string());
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
let call_id = "presentation-render-preview-unsupported";
|
||||
let arguments = serde_json::json!({
|
||||
"actions": [
|
||||
{
|
||||
"action": "create",
|
||||
"args": { "name": "Preview" }
|
||||
},
|
||||
{
|
||||
"action": "add_slide",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"action": "render_preview",
|
||||
"args": {}
|
||||
}
|
||||
]
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(call_id, "presentation_artifact", &arguments),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, first_response).await;
|
||||
|
||||
let second_response = sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]);
|
||||
let mock = responses::mount_sse_once(&server, second_response).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![UserInput::Text {
|
||||
text: "render the deck preview".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: model_slug.to_string(),
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let output_text = mock
|
||||
.single_request()
|
||||
.function_call_output_content_and_success(call_id)
|
||||
.and_then(|(content, _)| content)
|
||||
.expect("output text present");
|
||||
assert_eq!(
|
||||
output_text,
|
||||
"render_preview is not allowed because you do not support image inputs"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user