Repository navigation
MCP: headless open_file reads .pdf and PDF-compatible .ai, as File ▸ Open does - #5
Merged
Conversation
…Open does Headless `open_file` (and `file.open` through run_command) had its own loader that stopped at .vectorcraft and .svg, so an agent could not open a PDF or an .ai file without the desktop app, although the engine and `vectorcraft-cli` already import them through vectorcraft-pdf. The headless loader now imports .pdf, .ai and any file starting with %PDF through vectorcraft_pdf::import_with_report (default options, as File ▸ Open uses), and returns the import warnings like the SVG path. The open_file tool description lists the new types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
|
Thanks @storybored379, merged! Headless If you're not already there, come say hi in our Discord, we'd love to have you: https://discord.gg/artcraft |
apexmiguel9-hub
pushed a commit
to apexmiguel9-hub/vectorcraft-android
that referenced
this pull request
Oct 6, 2026
MEDIDO, y el motivo de fondo es que **abrir una segunda Activity da ANR en este port**.
No es SAF: es de abrir cualquier Activity. Traza del propio movil, textual:
"main" prio=5 tid=1 Native
native: storytold#1 <std::sys::sync::condvar::futex::Condvar>::wait+112
native: storytold#2 <WaitableNativeActivityState>::set_activity_state+160
native: storytold#3 try_with_waitable_activity_ref::<glue::on_pause::{closure#0}…>+240
native: storytold#5 android_activity::activity_impl::glue::on_pause+20
native: storytold#8 android.app.NativeActivity.onPause+16
Reason: Input dispatching timed out (…MainActivity is not responding.
Waited 10001ms for FocusEvent(hasFocus=false))
mLastPausedActivity: com.android.documentsui/…picker.PickActivity
`set_activity_state` (`android-activity-0.6.1/src/native_activity/glue.rs:511`) escribe
el comando en un pipe y **espera en un condvar** a que `activity_state` cambie, y solo el
hilo del bucle puede cambiarlo. Ese hilo es el de `android_main`, que es donde corre el
update de egui.
O sea, el ciclo cerrado: el update espera al selector -> el selector pausa la Activity ->
la pausa espera al update -> el selector nunca devuelve. **Mi suposicion anterior de que
"el despertar viene de `onActivityResult`, en otro hilo, y no hay bloqueo" era
incorrecta**: ignoraba ese handshake.
Un browser dibujado en egui no abre ninguna Activity, asi que el ANR desaparece de raiz.
## El navegador: `egui_file`, y no hay que escribirlo
MEDIDO, leyendo el crate:
* `grep -rniE 'android|jni|SAF' src/` **no devuelve ni una linea**. Es egui puro.
* `src/lib.rs:231` — `fs: Box::new(Fs {})`, y `src/fs.rs` es `std::fs` a pelo
(`std::fs::create_dir`, `read_dir`, `rename`). **No hay que escribir ningun `Vfs`.**
* Pide `egui ^0.36`, que es exactamente la del proyecto. Una dependencia (`dyn-clone`),
MIT, 4,1 M de descargas, actualizado 2026-08-17.
* Inmediate mode: `show(ctx)` cada frame y `state()` / `path()` / `selection()` para el
resultado. **Ninguna espera.** Ademas trae `multi_select`, asi que Place tambien sale.
## El permiso
`MANAGE_EXTERNAL_STORAGE` si hace falta, y se declara. MEDIDO, y no es un detalle de la
web: sin el, el navegador se queda en lo nuestro y no ve el telefono.
$ adb shell run-as ai.storyteller.vectorcraft ls /sdcard
ls: /sdcard: Permission denied
Y el otro lado, con un build de test de **ArmorPaint sin ningun** permiso de
almacenamiento: su browser muestra la **rejilla vacia** en `/Download`. Los nombres de las
carpetas de primer nivel si se ven; su contenido, no. Que es justo la linea que separa
las dos apps.
Decidido lo que **no** hace falta: bajar el `minSdk`. Lo que decide es el `targetSdk`, no
la version del telefono, asi que el permiso hace falta igual en Android 11 que en 16. En
los Android 7 a 10 que cubre `minSdk 24` no se pide y no hace falta.
Es un aviso con dos botones, no un dialogo modal: se puede saltar y la app sigue
sirviendo para lo que es suyo.
## Lo que gana el motor sin tocarlo
Con rutas de verdad en vez de `content://`:
* **Los ficheros linked vuelven a funcionar.** El motor usa `std::fs` para ellos
(`cmd/links.rs:627`, `cmd/swatchlib.rs:136,190`, `cmd/flatten.rs:417`,
`cmd/pdfcmds.rs:176`, `cmd/printpresets.rs:211`), asi que con SAF un `content://` les
hacia `stat` a una cadena rara.
* **`note_recent` guarda rutas de verdad**, asi que los recientes se abren.
## Lo que queda fuera, y por que
**Guardar y exportar no funcionan todavia.** No es un descuido: `pick_save`, `pick_open`,
`pick_folder` y `pick_open_multi` son sincronos (`FnMut(…) -> Option<String>`), y un
browser en immediate mode no puede devolver una ruta en la misma llamada en la que el
usuario la elige. Ademas `save` escribe en esa misma llamada (`io.rs:251-263`).
Open y Place si, con `open_async`+`inbox` y `place_async`+`place_inbox`, que **ya existen
upstream** (`lib.rs:159,168`) y se drenan cada frame (`lib.rs:676`). Cero cambios en
`crates/**`.
Para guardar hace falta una costura aditiva, `save_async`, con el mismo patron que
`open_async`. Es el paso siguiente, y medido antes de escribirlo.
## Estructura
* `browser.rs` — el navegador, los `Services` y el frame del browser.
* `permiso.rs` — lo unico que queda de JNI: pedir y leer el permiso.
* `saf.rs` — **borrado**. Con ella caen el base64, `onActivityResult` y el `Condvar`.
* `MainActivity.java` — de 552 a 118 lineas. Solo el permiso y `nativeListo`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Headless
open_file(andfile.openthroughrun_command) had its own loader that stopped at.vectorcraftand.svg, so an agent runningvectorcraft-cli mcp --headlessgot"headless mode can open .vectorcraft and .svg files, not .pdf" for a PDF or an
.aifile, althoughthe engine and
vectorcraft-clialready import them throughvectorcraft-pdf.What changed
Headless::openimports.pdf,.aiand any file starting with%PDFthroughvectorcraft_pdf::import_with_reportwith default options (the same call File ▸ Open makes inui-egui/src/io.rs) and returns the import warnings, like the SVG path does.vectorcraft-mcpdepends onvectorcraft-pdf; theopen_filetool description lists the new types.How it was found and tested
vectorcraft-cli mcp --headless), then fixed.headless_end_to_endnow exports the test document to PDF, copies it to.ai, and opens both headless.Verified it fails on current
mainwithout the change (open_file: headless mode can open .vectorcraft and .svg files, not .pdf) and passes with it.cargo test -p vectorcraft-mcp,cargo clippy -p vectorcraft-mcp --all-targets(no new warnings),cargo fmt --check,cargo xtask layerson currentmain.crates/mcp/Cargo.toml,crates/mcp/src/headless.rs,crates/mcp/src/tools.rs,crates/mcp/src/tests.rs,Cargo.lockFound and fixed with Claude Code (AI-assisted).
🤖 Generated with Claude Code