[package] name = "op-host-desktop" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true description = "OpenPencil desktop host — winit + skia-safe binary that drives the Rust editor on macOS / Linux / Windows" # The shipped executable keeps the stable `openpencil-desktop` name so # release artifacts + external CLI integrations are unaffected by the # Phase 7.3 crate rename. [[bin]] name = "openpencil-desktop" path = "src/main.rs" # File-association metadata for `cargo-bundle` (`cargo bundle`). This # declares OpenPencil as the OS-level handler for `.op` / `.pen` # documents: macOS writes `CFBundleDocumentTypes` into the `.app` # Info.plist, Windows registers the extensions, Linux emits a # `.desktop` MimeType entry. Once the association is registered the # OS launches this binary with the document path in argv, which # `main.rs` opens at startup (see `src/main.rs` argv handling). [package.metadata.bundle] name = "OpenPencil" identifier = "com.zseven-w.openpencil" [[package.metadata.bundle.file_associations]] ext = "op" name = "OpenPencil Document" role = "Editor" [[package.metadata.bundle.file_associations]] ext = "pen" name = "OpenPencil Document" role = "Editor" # Desktop-only binary. The op-host-native lib is multi-platform (mobile # stubs land in Step 1f) but the runner here pulls winit + glutin via # op-host-native's desktop dep set, so the whole crate is gated to # macos / linux / windows. [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] # Phase 7.3 reorg: openpencil-shell-core dissolved — widget facade / # render-backend / layout scene paths resolve through op-editor-ui. op-editor-ui = { path = "../op-editor-ui" } op-host-native = { path = "../op-host-native", version = "0.1.0" } # Phase 5 strangler reorg: the stdio MCP server (`mcp_serve.rs`) drives an # `op_editor_core::EditorState` (canonical `.op` document) instead of the # old shell-core `Document`. op-editor-core = { path = "../op-editor-core" } # Locale tables — git dialog strings go through `op_i18n::translate` # so they are localized like the rest of the chrome. op-i18n = { path = "../op-i18n" } # Phase 7 strangler reorg: the MCP tool registry + JSON-RPC stdio layer # (`mcp.rs` + `mcp/*`) was extracted out of the dissolved shell-core into the # op-mcp crate; `mcp_serve.rs` registers tools through `op_mcp::*`. op-mcp = { path = "../op-mcp" } # Phase 7 strangler reorg: the transport-free AI chat data shapes # (`ChatProvider` trait + model catalog + agent-settings state) were # extracted into op-ai; the `src/chat_*.rs` real transports + the # model-discovery path import them through `op_ai::*`. op-ai = { path = "../op-ai" } # AI skill engine — the BuiltIn agent provider resolves # generation-phase prompt skills through it (`chat_runtime.rs`). op-ai-skills = { path = "../op-ai-skills" } # ACP client — the `AcpProvider` chat backend (`chat_acp.rs`) drives # third-party Agent Client Protocol agents through it. op-acp = { path = "../op-acp" } # Canonical `.op` (PenDocument) → shell `Document` loader / adapter. # Extracted out of this binary into a shared library crate so library # crates (op-host-native) can reuse the conversion; # `persistence.rs` keeps only the desktop-only `rfd` Save/Open + error # dialogs. op-pen-loader = { path = "../op-pen-loader" } # In-app Git — `git_session.rs` binds an `op_git::GitRepo` to the # currently-open document so the Git panel can show branch / status # / history and drive commits. op-git = { path = "../op-git" } # Structured node-level 3-way merge for `.op` documents — the in-app # Git merge orchestrator feeds it the conflicting file's three index # stages so a merge conflict resolves per PenNode, not as raw text. op-opmerge = { path = "../op-opmerge" } # Canonical `.op` / `.pen` schema — all persistence + interop goes # through `PenDocument` rather than the desktop's private DocPayload. # Falling back to the schema's `load_str` lets us open files saved by # the TS editor, Jian apps, or any tool that emits the canonical form. jian-ops-schema = { path = "../../vendor/jian/crates/jian-ops-schema" } # jian-core ships the taffy-backed `LayoutEngine` that is the # canonical flex solver for `.op` files. We reuse it (instead of # rolling our own) so layout stays bit-identical with what the TS # editor + future jian native runtime produce. jian-core = { path = "../../vendor/jian/crates/jian-core" } # jian-skia ships `SkiaMeasure` — a `MeasureBackend` impl that # defers to skia's paragraph shaper. Wiring it into the layout # engine replaces the ~10% character-count heuristic with real # glyph advances so text frames sized to `fit_content` agree # with what the canvas painter draws (TS parity for line-wrap + # fixed-width text growth). jian-skia = { path = "../../vendor/jian/crates/jian-skia", features = ["textlayout"] } skia-safe = { version = "0.97.0", default-features = false, features = [ "binary-cache", "textlayout", "gl", "pdf", ] } # `casement` — ZSeven-W's winit fork (vendored as a submodule under # `vendor/casement`). It adds `winit::platform::macos::drain_opened_file_urls`, # the macOS open-documents Apple-event hook upstream winit lacks. The # `package` key keeps the `winit` import name, so all `use winit::…` # stays unchanged. winit = { package = "casement", path = "../../vendor/casement", default-features = false, features = [ "x11", "wayland", "wayland-csd-adwaita", "rwh_06", ] } # Document Save / Open (.pen / .op). Serialization lives in # `src/persistence.rs` as a hand-rolled DTO so shell-core stays # serde-free (Color / Rect / Point2D come from glam + jian-core # which would otherwise need wrapper types). rfd pops the native # Save/Open file dialogs so the user picks the path. serde = { workspace = true } serde_json = { workspace = true } rfd = "0.14" # Base64-encodes chat image attachments for providers that take # inline image content blocks (Claude SDK) or a base64 body field # (HTTP-server transport). Same pure-Rust crate op-figma uses. base64 = "0.22" # 2D affine transforms used by PNG export's bounds walker so nested # rotated containers don't clip their children. Pinned to the same # version shell-core uses for `Point2D = glam::Vec2`. glam = { version = "0.29", default-features = false, features = ["std"] } # Resolves the platform-specific user-config dir for the auto-saved # settings file (TS parity with `agent-settings-store` localStorage): # macOS → ~/Library/Application Support/openpencil/settings.json # Linux → ~/.config/openpencil/settings.json # Windows → %APPDATA%/openpencil/settings.json dirs = "5" # Cross-product Rust agent runtime — provides `QueryEngine` + the # generic `Provider` trait + streaming `Event`s. Vendored as the # `vendor/agent` git submodule (the agent-rs repo) and referenced by # relative path so the BuiltIn ChatProvider in `src/chat_runtime.rs` # can drive a real LLM turn. The submodule keeps CI self-contained. # # Compiled with NO default features today — the `anthropic` feature # would drag in reqwest's TLS stack (rustls / icu / idna) which needs # rustc ≥ 1.86 while this workspace pins 1.85. The BuiltIn bridge ships # the trait + the engine wiring + `from_provider`; concrete # Provider impls (Anthropic / OpenAI-compat / Ollama) flip on once the # workspace rust-toolchain bumps. agent = { path = "../../vendor/agent/crates/agent", default-features = false } # Forked from bartolli/anthropic-agent-sdk — lives under # `vendor/anthropic-agent-sdk/` so OP owns the source and can diverge # from upstream. Provides the Claude Code CLI subprocess transport + # stream-json message parser that `src/chat_claude.rs` wraps as a # `ChatProvider`. Default features (no rmcp) keep the dep set tight. anthropic-agent-sdk = { path = "../../vendor/anthropic-agent-sdk" } # Official GitHub Copilot CLI SDK (github/copilot-sdk). Replaces # the former in-workspace fork. Drives the Copilot chat bridge in # `src/chat_copilot.rs` over the CLI's `--server --stdio` JSON-RPC # transport. Requires Rust 1.94 / edition 2024 (the workspace # toolchain was bumped for it). github-copilot-sdk = "0.1" async-trait = "0.1" # HTTP client for the Codex / OpenCode bridges. They run as local # HTTP servers (spawned via ` serve`) and we POST JSON requests # to them — `reqwest` is already in the dep graph via # anthropic-agent-sdk so adding it here costs no new deps. # Default-features-off + rustls-tls keeps the TLS choice consistent # with the SDK's. We still talk to 127.0.0.1, but rustls is a # small price for not having two parallel TLS stacks. reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", "json", "stream", ] } # Async runtime — agent-rs is async; the BuiltIn ChatProvider impl # bridges its `EventStream` into the sync `Iterator` # shape via a tokio task + std::sync::mpsc channel. tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "macros", "sync", "io-util", "process", "net", "time", ] } futures = { version = "0.3", default-features = false, features = ["std"] } # Native menu bar (`src/menu.rs`). winit owns the window but has no # menu primitive, so `muda` builds the NSMenu / Win32 menu and routes # selections through its global event channel. Gated to macOS / # Windows: `muda`'s Linux backend needs a GTK window, but this # binary's winit is built for the x11 / wayland backends (no GTK), so # Linux falls back to the in-canvas File menu. `default-features = # false` drops the Linux-only `gtk` / `libxdo` feature deps. [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] muda = { version = "0.17", default-features = false }