refactor(host): rename op-web-daemon crate to op-host-services

The extracted headless crate is consumed by BOTH op-host-desktop (the GUI binary, for its
embedded --serve-web/MCP/chat/export) AND op-host-web-server — so 'web-daemon' was misleading.
Renamed crate dir + package + lib (op_web_daemon -> op_host_services) across all consumer files
(114 refs in 24 files) + the 4 Cargo.toml deps + Dockerfile/guard comments; identity docs
rewritten (it's the GUI-free host backend — daemon/MCP/AI/export/persistence — not web-specific).
No behavior change. (Lock renamed accordingly; the concurrent actor's op-host-web serde line excluded.)
This commit is contained in:
Kayshen-X 2026-06-19 22:35:22 +08:00
parent b7951fc598
commit 88a571c028
85 changed files with 216 additions and 212 deletions

72
Cargo.lock generated
View file

@ -3232,13 +3232,13 @@ dependencies = [
"op-figma",
"op-git",
"op-host-native",
"op-host-services",
"op-i18n",
"op-mcp",
"op-opmerge",
"op-orchestrator",
"op-pen-loader",
"op-process-io",
"op-web-daemon",
"reqwest 0.12.28",
"rfd",
"serde",
@ -3275,6 +3275,40 @@ dependencies = [
"tracing-test",
]
[[package]]
name = "op-host-services"
version = "0.8.0"
dependencies = [
"agent",
"anthropic-agent-sdk",
"async-trait",
"base64",
"dirs 5.0.1",
"futures",
"github-copilot-sdk",
"glam",
"jian-ops-schema",
"op-acp",
"op-ai",
"op-ai-skills",
"op-design-lint",
"op-editor-core",
"op-editor-host-core",
"op-editor-ui",
"op-host-native",
"op-i18n",
"op-mcp",
"op-orchestrator",
"op-pen-loader",
"op-process-io",
"reqwest 0.12.28",
"serde",
"serde_json",
"skia-safe",
"tokio",
"zip",
]
[[package]]
name = "op-host-web"
version = "0.8.0"
@ -3302,7 +3336,7 @@ dependencies = [
name = "op-host-web-server"
version = "0.8.0"
dependencies = [
"op-web-daemon",
"op-host-services",
]
[[package]]
@ -3389,40 +3423,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "op-web-daemon"
version = "0.8.0"
dependencies = [
"agent",
"anthropic-agent-sdk",
"async-trait",
"base64",
"dirs 5.0.1",
"futures",
"github-copilot-sdk",
"glam",
"jian-ops-schema",
"op-acp",
"op-ai",
"op-ai-skills",
"op-design-lint",
"op-editor-core",
"op-editor-host-core",
"op-editor-ui",
"op-host-native",
"op-i18n",
"op-mcp",
"op-orchestrator",
"op-pen-loader",
"op-process-io",
"reqwest 0.12.28",
"serde",
"serde_json",
"skia-safe",
"tokio",
"zip",
]
[[package]]
name = "op-web-sdk"
version = "0.8.0"

View file

@ -47,7 +47,7 @@ ARG WEB_BUNDLE_SOURCE=build
ARG SERVE_PORT=3100
# binaryen -> wasm-opt -Oz ; nodejs -> the 0-env-import assert in the gate
# script. op-host-web-server is the headless raster daemon (links op-web-daemon
# script. op-host-web-server is the headless raster daemon (links op-host-services
# only — no winit/glutin/skia-GL under Approach Y), so NONE of the GL/X11
# link-time libs the desktop binary needed are required here; only
# freetype/fontconfig (+ CJK fonts) for skia's raster text shaping at export.
@ -68,7 +68,7 @@ WORKDIR /src
COPY . .
# Build the headless web/MCP server binary (the `--serve-web` host) — links
# op-web-daemon only, no winit/skia-GL. Always built; this is the runtime
# op-host-services only, no winit/skia-GL. Always built; this is the runtime
# binary regardless of how the web bundle is produced.
RUN cargo build -p op-host-web-server --release

View file

@ -65,8 +65,8 @@ op-editor-ui = { path = "../op-editor-ui" }
op-host-native = { path = "../op-host-native", version = "0.8.0", features = ["gl-host"] }
# Phase 2: the headless daemon crate. The `--serve-web` closure migrates here
# over Phases 2-5; op-host-desktop's serve-web dispatch + GUI residuals call into
# it via `op_web_daemon::*`.
op-web-daemon = { path = "../op-web-daemon" }
# it via `op_host_services::*`.
op-host-services = { path = "../op-host-services" }
# Accessibility (#67) — the platform-free `accesskit` core types
# (`TreeUpdate` / `ActionRequest` / `ActivationHandler` …) used by the
# desktop a11y adapter (`src/a11y.rs`). The OS-specific subclassing

View file

@ -1,7 +1,7 @@
//! Host pump for connect-time ACP agent probes.
//!
//! The job struct + probe fns live in
//! [`op_web_daemon::acp_agent_probe_host`] (codex Issue 5 — the job is
//! [`op_host_services::acp_agent_probe_host`] (codex Issue 5 — the job is
//! a `DesktopApp` field); this residual keeps only the `impl DesktopApp`
//! pump, which drives the job through its public API.
@ -10,7 +10,7 @@ use op_editor_core::agent_settings::AcpAgentConnectOutcome;
use crate::DesktopApp;
// Re-export so `crate::acp_agent_probe_host::AcpAgentConnectJob` (the
// `DesktopApp` field type in `main.rs`) still resolves with zero churn.
pub use op_web_daemon::acp_agent_probe_host::AcpAgentConnectJob;
pub use op_host_services::acp_agent_probe_host::AcpAgentConnectJob;
impl DesktopApp {
pub(crate) fn drain_acp_agent_connect(&mut self) -> bool {
@ -92,7 +92,7 @@ impl DesktopApp {
mod tests {
use super::*;
use op_editor_core::agent_settings::{AcpAgentConnectPhase, AcpConnectionType};
use op_web_daemon::acp_agent_probe_host::AcpAgentProbeOutcome;
use op_host_services::acp_agent_probe_host::AcpAgentProbeOutcome;
use std::collections::BTreeMap;
#[test]

View file

@ -255,7 +255,7 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
// background Figma import worker so the launch doesn't freeze
// on a multi-second parse.
if let Some(path) = self.initial_file.take() {
if op_web_daemon::doc_io::is_supported_figma_import(&path) {
if op_host_services::doc_io::is_supported_figma_import(&path) {
figma_import_session::cancel(&mut self.host, &mut self.current_figma_import);
self.current_figma_import = Some(figma_import_session::spawn(&mut self.host, path));
self.request_redraw(true);
@ -281,7 +281,7 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
// one-off `op start` never rewrites the user's saved settings.
let bootstrap_changed = self.bootstrap_mcp_runtime_from_settings();
if bootstrap_changed && self.force_live_mcp_port.is_none() {
op_web_daemon::settings_io::save(self.host.editor_state());
op_host_services::settings_io::save(self.host.editor_state());
}
// Publish (or clean up) the live MCP discovery file now that the
// launch-time server state is settled, so `op` can find this canvas.
@ -347,7 +347,7 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
// on the trackpad hot path.
let settings_before = match &event {
WindowEvent::CursorMoved { .. } => None,
_ => Some(op_web_daemon::settings_io::fingerprint(self.host.editor_state())),
_ => Some(op_host_services::settings_io::fingerprint(self.host.editor_state())),
};
let mcp_cli_before = match &event {
WindowEvent::CursorMoved { .. } => None,
@ -361,7 +361,7 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
// The unsaved-changes prompt can abort the close.
if self.confirm_close() {
self.host.flush_settings_input();
op_web_daemon::settings_io::save(self.host.editor_state());
op_host_services::settings_io::save(self.host.editor_state());
event_loop.exit();
}
}
@ -438,12 +438,12 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
// dashboards, so doing it inline would freeze the
// window). Anything else is ignored silently so a
// stray drop can't disrupt the current document.
if op_web_daemon::doc_io::is_supported_figma_import(&path) {
if op_host_services::doc_io::is_supported_figma_import(&path) {
figma_import_session::cancel(&mut self.host, &mut self.current_figma_import);
self.current_figma_import =
Some(figma_import_session::spawn(&mut self.host, path));
self.request_redraw(true);
} else if op_web_daemon::doc_io::is_supported_document(&path) {
} else if op_host_services::doc_io::is_supported_document(&path) {
if persistence::open_path(
&mut self.host,
path,
@ -872,7 +872,7 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
// — a bare `exit()` would drop work.
if self.confirm_close() {
self.host.flush_settings_input();
op_web_daemon::settings_io::save(self.host.editor_state());
op_host_services::settings_io::save(self.host.editor_state());
event_loop.exit();
}
}
@ -998,13 +998,13 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
// in-flight Figma import internally, so a
// stale worker can't overwrite the fresh
// document when its result lands.
op_web_daemon::doc_io::ActionOutcome::Saved => self.mark_document_saved(),
op_host_services::doc_io::ActionOutcome::Saved => self.mark_document_saved(),
// User picked a `.fig`; spin up the worker
// session and let `pump` apply the document
// once parsing finishes. Cancel any prior
// in-flight session first so two imports
// in quick succession don't race.
op_web_daemon::doc_io::ActionOutcome::FigmaImportStarted(path) => {
op_host_services::doc_io::ActionOutcome::FigmaImportStarted(path) => {
figma_import_session::cancel(
&mut self.host,
&mut self.current_figma_import,
@ -1013,7 +1013,7 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
Some(figma_import_session::spawn(&mut self.host, path));
self.request_redraw(true);
}
op_web_daemon::doc_io::ActionOutcome::Noop => {}
op_host_services::doc_io::ActionOutcome::Noop => {}
}
}
}
@ -1219,7 +1219,7 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
self.request_redraw(true);
}
if let Some(before) = settings_before {
op_web_daemon::settings_io::save_if_changed(self.host.editor_state(), before);
op_host_services::settings_io::save_if_changed(self.host.editor_state(), before);
}
// A Git-panel click or Enter may have queued an action
// (Commit / Refresh / Pull) — run it after the event.
@ -1246,7 +1246,7 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
// `CloseRequested`; flush MCP port draft before snapshotting so
// a focused-but-uncommitted edit isn't silently dropped.
self.host.flush_settings_input();
op_web_daemon::settings_io::save(self.host.editor_state());
op_host_services::settings_io::save(self.host.editor_state());
if let Some(mut server) = self.mcp_server.take() {
server.stop();
crate::mcp_port_file::remove();

View file

@ -49,8 +49,8 @@ use op_acp::{
use op_ai::chat_provider::{ChatDelta, ChatProvider, ChatRequest, EffortLevel, StopReason};
use tokio::sync::mpsc;
use op_web_daemon::chat_attachment::TempGuard;
use op_web_daemon::chat_runtime::{shared_runtime, BlockingRecvIter};
use op_host_services::chat_attachment::TempGuard;
use op_host_services::chat_runtime::{shared_runtime, BlockingRecvIter};
/// TS error surfaced when ACP is used with the MCP server stopped —
/// ported verbatim from `apps/web/server/api/ai/agent.ts:506-511`.
@ -178,12 +178,12 @@ impl ChatProvider for AcpProvider {
// paths. The thinking / effort knobs ride in-band either way
// (ACP exposes no dedicated reasoning channel).
let (mut prompt, guard) = if config.connection_type == ConnectionType::Local {
match op_web_daemon::chat_attachment::prompt_with_attachments(
match op_host_services::chat_attachment::prompt_with_attachments(
&request.user_message,
&request.attachments,
) {
Ok(pair) => pair,
Err(e) => return op_web_daemon::chat_attachment::attachment_error_turn(e),
Err(e) => return op_host_services::chat_attachment::attachment_error_turn(e),
}
} else {
let mut prompt = request.user_message.clone();
@ -197,7 +197,7 @@ impl ChatProvider for AcpProvider {
(prompt, None)
};
let mut directive = String::new();
if let Some(d) = op_web_daemon::chat_attachment::thinking_directive(request.thinking) {
if let Some(d) = op_host_services::chat_attachment::thinking_directive(request.thinking) {
directive.push_str(d);
}
if request.effort != EffortLevel::Low {

View file

@ -2,14 +2,14 @@
//!
//! The headless attachment helpers (base64, temp-file spill, prompt
//! building, the Claude image-Read flow) live in
//! [`op_web_daemon::chat_attachment`]; this module keeps only the
//! [`op_host_services::chat_attachment`]; this module keeps only the
//! native file picker that stages a chosen image on the chat state.
use std::fs;
use op_editor_core::chat::{ChatAttachment, MAX_ATTACHMENT_BYTES};
use op_host_native::WidgetHostNative;
use op_web_daemon::chat_attachment::media_type_for_path;
use op_host_services::chat_attachment::media_type_for_path;
/// Drain `chat.pending_attachment_pick` (raised by the attach
/// button): open a native image picker and stage the chosen file on

View file

@ -1,12 +1,12 @@
//! Host-coupled test for the CLI standard-mode design route (GAP #33).
//!
//! The bulk of `chat_intent`'s tests are headless and live beside the
//! module in `op_web_daemon::chat_intent` (accessing its `#[cfg(test)]`
//! module in `op_host_services::chat_intent` (accessing its `#[cfg(test)]`
//! internals via `super`). This one test is the exception: it drives the
//! desktop GUI design-session pumps (`design_session::{pump_commands,
//! pump_progress}`, which take `&mut WidgetHostNative` — orphan rule) to
//! prove the new-design route clears the agent frame indicators after the
//! turn finishes. `op-web-daemon` links `op-host-native` with
//! turn finishes. `op-host-services` links `op-host-native` with
//! default-features off, so `WidgetHostNative` (gl-host gated) is absent
//! there; the test therefore lives host-side.
@ -20,14 +20,14 @@ use op_orchestrator::DesignRequest;
use op_editor_host_core::design::DesignSession;
use op_host_native::WidgetHostNative;
use op_web_daemon::chat_canvas_tools::chat_tool_channel;
use op_web_daemon::chat_intent::{run_cli_turn, CliTurnPlan};
use op_host_services::chat_canvas_tools::chat_tool_channel;
use op_host_services::chat_intent::{run_cli_turn, CliTurnPlan};
use crate::design_session::{pump_commands, pump_progress};
// ---------------------------------------------------------------------------
// Scripted providers (copied from the headless chat_intent test sibling —
// `op-web-daemon` keeps its own copies for the cross-crate tests there).
// `op-host-services` keeps its own copies for the cross-crate tests there).
// ---------------------------------------------------------------------------
struct Scripted {

View file

@ -11,7 +11,7 @@ pub use op_editor_host_core::chat::ChatPoll;
pub use op_editor_host_core::chat::{apply_poll_to_message, ChatSession};
use op_host_native::WidgetHostNative;
use op_web_daemon::chat_canvas_tools::execute_chat_tool;
use op_host_services::chat_canvas_tools::execute_chat_tool;
// Turn launch + provider routing (split out at the 800-line cap).
// `launch_if_pending` and friends live in the sibling file; the
@ -58,13 +58,13 @@ fn drain_tool_requests(state: &mut EditorState, session: &mut ChatSession) -> bo
}
let mut changed = false;
for req in requests {
if req.name == op_web_daemon::chat_intent::APPLY_MODIFICATION_OP {
if req.name == op_host_services::chat_intent::APPLY_MODIFICATION_OP {
let nodes = serde_json::from_str::<serde_json::Value>(&req.args_json)
.ok()
.and_then(|v| v.get("nodes").and_then(|n| n.as_array().cloned()))
.unwrap_or_default();
let (count, mutated) =
op_web_daemon::chat_canvas_tools::apply_design_modification(state, &nodes);
op_host_services::chat_canvas_tools::apply_design_modification(state, &nodes);
if mutated {
changed = true;
}

View file

@ -14,14 +14,14 @@ use op_host_native::WidgetHostNative;
use op_orchestrator::{classify_intent, Intent};
use crate::chat_acp::AcpProvider;
use op_web_daemon::chat_builtin_http::ConfiguredBuiltinProvider;
use op_web_daemon::chat_canvas_tools::{chat_tool_channel, chat_tool_defs, ChatToolRequest};
use op_web_daemon::chat_claude::ClaudeCodeProvider;
use op_web_daemon::chat_copilot::CopilotProvider;
use op_web_daemon::chat_http_server::OpenCodeProvider;
use op_web_daemon::chat_provider_llm::ChatProviderLlmClient;
use op_web_daemon::chat_subprocess::SubprocessProvider;
use op_web_daemon::chat_system_prompt::{
use op_host_services::chat_builtin_http::ConfiguredBuiltinProvider;
use op_host_services::chat_canvas_tools::{chat_tool_channel, chat_tool_defs, ChatToolRequest};
use op_host_services::chat_claude::ClaudeCodeProvider;
use op_host_services::chat_copilot::CopilotProvider;
use op_host_services::chat_http_server::OpenCodeProvider;
use op_host_services::chat_provider_llm::ChatProviderLlmClient;
use op_host_services::chat_subprocess::SubprocessProvider;
use op_host_services::chat_system_prompt::{
build_agent_system_prompt, build_chat_system_prompt, chat_history_from_transcript,
};
use op_editor_host_core::design::DesignSession;
@ -91,10 +91,10 @@ pub fn launch_if_pending(
host.mark_editor_state_dirty();
}
let append_context =
op_web_daemon::chat_intent::detect_append_intent(host.editor_state(), &user_text);
op_host_services::chat_intent::detect_append_intent(host.editor_state(), &user_text);
let initial_state = host.editor_state().clone();
let request = build_design_request(user_text, &initial_state, append_context);
*current_design = Some(op_web_daemon::design_session::start(llm, request, initial_state));
*current_design = Some(op_host_services::design_session::start(llm, request, initial_state));
return true;
}
// Design intent but the selected agent has no ChatProvider
@ -248,8 +248,8 @@ fn launch_cli_standard_turn(
DEFAULT_MAX_CHARS,
);
let system_prompt = build_chat_system_prompt(state, user_text);
let modify_plan = op_web_daemon::chat_intent::build_modify_plan(state, user_text);
let append_context = op_web_daemon::chat_intent::detect_append_intent(state, user_text);
let modify_plan = op_host_services::chat_intent::build_modify_plan(state, user_text);
let append_context = op_host_services::chat_intent::detect_append_intent(state, user_text);
let initial_state = state.clone();
let design_request =
build_design_request(user_text.to_string(), &initial_state, append_context);
@ -292,7 +292,7 @@ fn launch_cli_standard_turn(
indicator_epoch,
));
let plan = op_web_daemon::chat_intent::CliTurnPlan {
let plan = op_host_services::chat_intent::CliTurnPlan {
user_text: user_text.to_string(),
page_children_empty,
classify_provider,
@ -308,7 +308,7 @@ fn launch_cli_standard_turn(
thread::Builder::new()
.name("op-chat-intent".into())
.spawn(move || {
op_web_daemon::chat_intent::run_cli_turn(plan, chat_tx, executor, delta_tx, cmd_tx);
op_host_services::chat_intent::run_cli_turn(plan, chat_tx, executor, delta_tx, cmd_tx);
})
.expect("spawn op-chat-intent thread");
true
@ -330,8 +330,8 @@ pub fn drain_new_chat_request(
// A fresh transcript must start a fresh provider conversation —
// forget any resumable Claude Code / Copilot session so stale
// context can't leak into the new chat.
op_web_daemon::chat_claude::reset_claude_chat_session();
op_web_daemon::chat_copilot::reset_copilot_chat_session();
op_host_services::chat_claude::reset_claude_chat_session();
op_host_services::chat_copilot::reset_copilot_chat_session();
host.mark_editor_state_dirty();
true
}

View file

@ -1,5 +1,5 @@
use super::*;
use op_web_daemon::chat_system_prompt::{build_chat_system_prompt, chat_history_from_transcript};
use op_host_services::chat_system_prompt::{build_chat_system_prompt, chat_history_from_transcript};
use op_ai::chat_history::{trim_chat_history, DEFAULT_MAX_CHARS, DEFAULT_MAX_MESSAGES};
use op_ai::chat_provider::{ChatDelta, ChatRequest, EchoProvider, StopReason};
use op_editor_core::{ChatMessage, ChatToolCall};
@ -491,7 +491,7 @@ impl Iterator for ToolLoopIter {
match self.step {
1 => Some(ChatDelta::ToolUse {
name: self.tool.into(),
args: op_web_daemon::chat_agent_loop::tool_card_envelope("create", self.args),
args: op_host_services::chat_agent_loop::tool_card_envelope("create", self.args),
}),
2 => {
// Blocks until the UI thread (the test's pump loop)
@ -536,7 +536,7 @@ fn pump_executes_scripted_tool_call_against_live_state() {
.push(ChatMessage::assistant_streaming());
let before = host.editor_state().active_children().len();
let (executor, tool_rx) = op_web_daemon::chat_canvas_tools::chat_tool_channel();
let (executor, tool_rx) = op_host_services::chat_canvas_tools::chat_tool_channel();
let provider = Box::new(ToolLoopProvider {
executor: std::sync::Arc::new(executor),
tool: "insert_node",

View file

@ -116,7 +116,7 @@ fn build_live_structure_bundle(
fn show_save_error(host: &WidgetHostNative, path: Option<&std::path::Path>, detail: &str) {
crate::persistence::show_error_dialog_public(
host,
op_web_daemon::doc_io::ErrorKind::Save,
op_host_services::doc_io::ErrorKind::Save,
path,
detail,
);

View file

@ -2,7 +2,7 @@
//! half of the design session.
//!
//! The worker spawn + viewport-fit math live in
//! [`op_web_daemon::design_session`]; this residual keeps the two UI-loop
//! [`op_host_services::design_session`]; this residual keeps the two UI-loop
//! pumps (`pump_commands` / `pump_progress`, which take `&mut
//! WidgetHostNative` — orphan rule) plus the progress-line renderer they
//! fold into the chat transcript.
@ -19,7 +19,7 @@ pub use op_editor_host_core::design::DesignSession;
use op_host_native::WidgetHostNative;
use op_orchestrator::Progress;
use op_web_daemon::design_session::fit_design_viewport_to_content;
use op_host_services::design_session::fit_design_viewport_to_content;
/// Drain every pending apply request from the in-flight design
/// session and execute it against the real `EditorState`. Each

View file

@ -1,5 +1,5 @@
use super::*;
use op_web_daemon::design_session::{
use op_host_services::design_session::{
active_content_bounds, design_canvas_size, fit_design_viewport_to_content,
};
use op_editor_core::{EditorCommand, EditorState};

View file

@ -25,7 +25,7 @@ use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::thread;
use crate::persistence::show_error_dialog_public;
use op_web_daemon::doc_io::ErrorKind;
use op_host_services::doc_io::ErrorKind;
/// Successful worker output. Keep this Skia-free: building
/// `LayoutScene` runs text measurement, which can contend with the

View file

@ -207,7 +207,7 @@ impl DesktopApp {
// can detect edits made *during* the async
// pull — those the confirm above did not cover.
self.git_pull_doc_baseline =
Some(op_web_daemon::doc_io::document_fingerprint(self.host.editor_state()));
Some(op_host_services::doc_io::document_fingerprint(self.host.editor_state()));
self.host.editor_state_mut().editor_ui.git_panel.pulling = true;
}
}
@ -290,7 +290,7 @@ impl DesktopApp {
} else if !message.is_empty() {
match self.git_session.tracked_file().map(|p| p.to_path_buf()) {
Some(path) => {
match op_web_daemon::doc_io::save_to_path(self.host.editor_state(), &path) {
match op_host_services::doc_io::save_to_path(self.host.editor_state(), &path) {
Ok(()) => {
self.mark_document_saved();
// Stage, then guard against an empty
@ -329,7 +329,7 @@ impl DesktopApp {
}
Err(detail) => persistence::show_error_dialog_public(
&self.host,
op_web_daemon::doc_io::ErrorKind::Save,
op_host_services::doc_io::ErrorKind::Save,
Some(&path),
&detail,
),
@ -337,7 +337,7 @@ impl DesktopApp {
}
None => persistence::show_error_dialog_public(
&self.host,
op_web_daemon::doc_io::ErrorKind::Save,
op_host_services::doc_io::ErrorKind::Save,
None,
"the open document is not tracked under this repository — \
save it into the repository folder first",

View file

@ -146,7 +146,7 @@ fn kit_to_persisted(kit: &UIKit) -> PersistedKit {
// ---------------------------------------------------------------------------
/// Hydrate imported kits + the browser-open flag onto a fresh
/// `EditorState` (call once at startup, after `op_web_daemon::settings_io::load`).
/// `EditorState` (call once at startup, after `op_host_services::settings_io::load`).
/// Best-effort: a missing / unreadable / malformed file leaves the
/// built-in kits alone.
pub fn load(state: &mut EditorState) {

View file

@ -152,7 +152,7 @@ struct DesktopApp {
/// Background AI-model discovery — probes the installed CLIs
/// on a worker thread; its result is drained into
/// `chat.available_models` on a later frame.
model_probe: op_web_daemon::model_discovery::ModelProbe,
model_probe: op_host_services::model_discovery::ModelProbe,
/// Background auto-search jobs that replace generated empty image
/// nodes with freely licensed remote images.
image_search: image_search_session::ImageSearchSession,
@ -237,7 +237,7 @@ struct DesktopApp {
/// external repository changes.
last_git_refresh: Instant,
/// Live in-process MCP HTTP server, started from Settings -> MCP.
mcp_server: Option<op_web_daemon::mcp_live::McpLiveServer>,
mcp_server: Option<op_host_services::mcp_live::McpLiveServer>,
/// When set (via the `--live-mcp[=port]` launch flag used by
/// `op start`), the editor force-enables the live MCP server on
/// this port during `resumed()`, regardless of the persisted
@ -261,7 +261,7 @@ impl DesktopApp {
let mut host = WidgetHostNative::new();
let fit_blank_frame = initial_file.is_none();
// Best-effort prefs restore onto the host's `EditorState`.
op_web_daemon::settings_io::load(host.editor_state_mut());
op_host_services::settings_io::load(host.editor_state_mut());
// Imported UIKits + browser-open flag (`uikits.json`). Skipped
// under test like the update / model probes — unit tests must
// not see a developer machine's kit store.
@ -277,7 +277,7 @@ impl DesktopApp {
host.mark_editor_state_dirty();
// Baseline for the unsaved-changes prompt — the fresh,
// empty document is by definition "saved" (nothing to lose).
let saved_doc_fingerprint = op_web_daemon::doc_io::document_fingerprint(host.editor_state());
let saved_doc_fingerprint = op_host_services::doc_io::document_fingerprint(host.editor_state());
let update_probe = if cfg!(test) {
update_check::UpdateProbe::idle()
} else {
@ -289,9 +289,9 @@ impl DesktopApp {
)
};
let model_probe = if cfg!(test) {
op_web_daemon::model_discovery::ModelProbe::idle()
op_host_services::model_discovery::ModelProbe::idle()
} else {
op_web_daemon::model_discovery::ModelProbe::spawn()
op_host_services::model_discovery::ModelProbe::spawn()
};
Self {
window: None,
@ -386,7 +386,7 @@ impl DesktopApp {
// silent no-op when it finishes.
figma_import_session::cancel(&mut self.host, &mut self.current_figma_import);
self.image_search.reset();
self.saved_doc_fingerprint = op_web_daemon::doc_io::document_fingerprint(self.host.editor_state());
self.saved_doc_fingerprint = op_host_services::doc_io::document_fingerprint(self.host.editor_state());
self.rebind_git_session_for_current_path();
}
@ -475,7 +475,7 @@ impl DesktopApp {
/// Whether the document carries edits since the last save / open
/// / new.
fn document_is_dirty(&self) -> bool {
op_web_daemon::doc_io::document_fingerprint(self.host.editor_state()) != self.saved_doc_fingerprint
op_host_services::doc_io::document_fingerprint(self.host.editor_state()) != self.saved_doc_fingerprint
}
/// Open documents macOS delivered through the open-documents
@ -490,8 +490,8 @@ impl DesktopApp {
{
let mut opened = false;
for path in winit::platform::macos::drain_opened_file_urls() {
let is_op = op_web_daemon::doc_io::is_supported_document(&path);
let is_fig = op_web_daemon::doc_io::is_supported_figma_import(&path);
let is_op = op_host_services::doc_io::is_supported_document(&path);
let is_fig = op_host_services::doc_io::is_supported_figma_import(&path);
if !is_op && !is_fig {
continue;
}
@ -552,8 +552,8 @@ impl DesktopApp {
};
let mut opened = false;
for path in paths {
let is_op = op_web_daemon::doc_io::is_supported_document(&path);
let is_fig = op_web_daemon::doc_io::is_supported_figma_import(&path);
let is_op = op_host_services::doc_io::is_supported_document(&path);
let is_fig = op_host_services::doc_io::is_supported_figma_import(&path);
if (!is_op && !is_fig) || !path.is_file() {
continue;
}
@ -727,7 +727,7 @@ impl DesktopApp {
// them. An unchanged document reloads silently.
let edited_during_pull = baseline
.map(|base| {
op_web_daemon::doc_io::document_fingerprint(self.host.editor_state()) != base
op_host_services::doc_io::document_fingerprint(self.host.editor_state()) != base
})
.unwrap_or(false);
if !edited_during_pull || self.confirm_document_reload() {
@ -857,7 +857,7 @@ impl DesktopApp {
/// window is up (see `DesktopApp::apply_initial_file`).
fn initial_file_from_argv() -> Option<PathBuf> {
std::env::args_os().skip(1).map(PathBuf::from).find(|p| {
(op_web_daemon::doc_io::is_supported_document(p) || op_web_daemon::doc_io::is_supported_figma_import(p))
(op_host_services::doc_io::is_supported_document(p) || op_host_services::doc_io::is_supported_figma_import(p))
&& p.is_file()
})
}
@ -925,7 +925,7 @@ fn main() {
// headless `--render-shots` rasterizer below, MCP — so they resolve
// simple-icons instead of the unknown-glyph fallback dot. Set-once /
// idempotent.
op_editor_ui::set_brand_catalog(op_web_daemon::web_static::ICONIFY_BRANDS_JSON);
op_editor_ui::set_brand_catalog(op_host_services::web_static::ICONIFY_BRANDS_JSON);
// `--mcp` / `--mcp-http` swap the GUI for an MCP server mode;
// when one of those ran, exit instead of opening a window.
if mcp_serve::run_cli_if_requested() {
@ -985,10 +985,10 @@ fn main() {
}
}
// chat_intent moved to op_web_daemon::chat_intent (its headless tests
// chat_intent moved to op_host_services::chat_intent (its headless tests
// moved alongside it as a `#[path]` sibling). Only the one host-coupled
// test stayed here — it drives the GUI design-session pumps, which need
// `WidgetHostNative` (absent from op-web-daemon's default-features-off
// `WidgetHostNative` (absent from op-host-services's default-features-off
// op-host-native dependency).
#[cfg(test)]
#[path = "chat_intent_host_tests.rs"]

View file

@ -2,7 +2,7 @@
//! `main.rs` to keep that file under the 800-line cap.
use super::*;
use op_web_daemon::mcp_serve::tool_text;
use op_host_services::mcp_serve::tool_text;
#[test]
fn cursor_only_redraw_without_visible_state_change_skips_present() {
@ -184,7 +184,7 @@ fn live_mcp_http_server_applies_write_requests_to_editor_state() {
use op_editor_core::PenNodeExt;
fn start_live_server() -> (op_web_daemon::mcp_live::McpLiveServer, u16) {
fn start_live_server() -> (op_host_services::mcp_live::McpLiveServer, u16) {
// `bind(0)` to grab an ephemeral port, then re-`start` on that port,
// has a TOCTOU window where the OS can reassign the port between the
// probe-listener drop and the server bind — so a single attempt
@ -195,7 +195,7 @@ fn live_mcp_http_server_applies_write_requests_to_editor_state() {
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind ephemeral port");
listener.local_addr().expect("local addr").port()
};
if let Ok(server) = op_web_daemon::mcp_live::McpLiveServer::start(port) {
if let Ok(server) = op_host_services::mcp_live::McpLiveServer::start(port) {
return (server, port);
}
}
@ -257,13 +257,13 @@ fn live_mcp_http_server_waits_for_split_http_request() {
use std::sync::mpsc;
use std::time::{Duration, Instant};
fn start_live_server() -> (op_web_daemon::mcp_live::McpLiveServer, u16) {
fn start_live_server() -> (op_host_services::mcp_live::McpLiveServer, u16) {
for _ in 0..20 {
let port = {
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind ephemeral port");
listener.local_addr().expect("local addr").port()
};
if let Ok(server) = op_web_daemon::mcp_live::McpLiveServer::start(port) {
if let Ok(server) = op_host_services::mcp_live::McpLiveServer::start(port) {
return (server, port);
}
}
@ -320,7 +320,7 @@ fn live_mcp_http_server_routes_file_path_requests_to_target_file() {
use std::sync::mpsc;
use std::time::{Duration, Instant};
fn start_live_server() -> (op_web_daemon::mcp_live::McpLiveServer, u16) {
fn start_live_server() -> (op_host_services::mcp_live::McpLiveServer, u16) {
// `bind(0)` to grab an ephemeral port, then re-`start` on that port,
// has a TOCTOU window where the OS can reassign the port between the
// probe-listener drop and the server bind — so a single attempt
@ -331,7 +331,7 @@ fn live_mcp_http_server_routes_file_path_requests_to_target_file() {
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind ephemeral port");
listener.local_addr().expect("local addr").port()
};
if let Ok(server) = op_web_daemon::mcp_live::McpLiveServer::start(port) {
if let Ok(server) = op_host_services::mcp_live::McpLiveServer::start(port) {
return (server, port);
}
}
@ -420,13 +420,13 @@ fn live_mcp_http_server_replaces_document_via_rest_document_sync() {
use std::sync::mpsc;
use std::time::{Duration, Instant};
fn start_live_server() -> (op_web_daemon::mcp_live::McpLiveServer, u16) {
fn start_live_server() -> (op_host_services::mcp_live::McpLiveServer, u16) {
for _ in 0..20 {
let port = {
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind ephemeral port");
listener.local_addr().expect("local addr").port()
};
if let Ok(server) = op_web_daemon::mcp_live::McpLiveServer::start(port) {
if let Ok(server) = op_host_services::mcp_live::McpLiveServer::start(port) {
return (server, port);
}
}

View file

@ -78,7 +78,7 @@ impl DesktopApp {
if let Some(mut server) = self.mcp_server.take() {
server.stop();
}
match op_web_daemon::mcp_live::McpLiveServer::start_with_wake(port, self.mcp_wake_callback()) {
match op_host_services::mcp_live::McpLiveServer::start_with_wake(port, self.mcp_wake_callback()) {
Ok(server) => {
let bound_port = server.port();
self.mcp_server = Some(server);
@ -88,7 +88,7 @@ impl DesktopApp {
Err(err) => {
eprintln!("openpencil-desktop mcp: failed to start on {port}: {err}");
if port != 0 {
match op_web_daemon::mcp_live::McpLiveServer::start_with_wake(0, self.mcp_wake_callback()) {
match op_host_services::mcp_live::McpLiveServer::start_with_wake(0, self.mcp_wake_callback()) {
Ok(server) => {
let bound_port = server.port();
eprintln!(

View file

@ -1,14 +1,14 @@
//! Argv dispatcher for the headless CLI server modes.
//!
//! The MCP stdio / HTTP server runners live in
//! [`op_web_daemon::mcp_serve`] and the headless web-canvas daemon in
//! [`op_host_services::mcp_serve`] and the headless web-canvas daemon in
//! `crate::web_canvas_server`; this residual just inspects `argv` and
//! routes `--mcp` / `--mcp-http` / `--serve-web` to them. `main` calls
//! it before opening the GUI window.
use std::path::PathBuf;
use op_web_daemon::mcp_serve::{run, run_http};
use op_host_services::mcp_serve::{run, run_http};
/// If argv requests a headless server mode, run it and return `true` —
/// the caller (`main`) should then exit. Returns `false` for normal
@ -59,12 +59,12 @@ pub fn run_cli_if_requested() -> bool {
if first == "--serve-web" {
// `--serve-web <port> [doc] [--host <addr>]`: doc optional (empty
// document otherwise); `--host` opts in to a non-loopback bind.
let (port, path, host) = op_web_daemon::web_canvas_server::parse_serve_web_args(args)
let (port, path, host) = op_host_services::web_canvas_server::parse_serve_web_args(args)
.unwrap_or_else(|e| {
eprintln!("openpencil-desktop --serve-web: {e}");
std::process::exit(2);
});
if let Err(e) = op_web_daemon::web_canvas_server::run_web_canvas(path, port, &host) {
if let Err(e) = op_host_services::web_canvas_server::run_web_canvas(path, port, &host) {
eprintln!("openpencil-desktop --serve-web: {e}");
std::process::exit(1);
}

View file

@ -25,7 +25,7 @@ impl DesktopApp {
&mut self.host,
&mut self.current_path,
self.window.as_ref(),
) == op_web_daemon::doc_io::ActionOutcome::Saved
) == op_host_services::doc_io::ActionOutcome::Saved
{
self.mark_document_saved();
}

View file

@ -3,7 +3,7 @@
//! The headless load / save core (the canonical serializer, the
//! `.opmeta` view-state sidecar, legacy-`DocPayload` detection, and the
//! `EditorState` preference-carry helpers) lives in
//! [`op_web_daemon::doc_io`]; this module keeps the rfd / winit dialog
//! [`op_host_services::doc_io`]; this module keeps the rfd / winit dialog
//! flow — the Save / Save-As / Open pickers, the file-menu
//! [`run_action`] router, and the native error dialog — and imports the
//! headless functions back.
@ -19,7 +19,7 @@ use std::path::PathBuf;
use op_editor_core::EditorState;
use op_host_native::WidgetHostNative;
use op_web_daemon::doc_io::{
use op_host_services::doc_io::{
ActionOutcome, ErrorKind, active_page_bbox, load_editor_state, preserve_app_preferences,
save_to_path, set_file_name_display,
};
@ -251,20 +251,20 @@ pub fn run_action(
None
}
};
let raster = |rf: op_web_daemon::export::RasterFormat| -> Result<(), String> {
let raster = |rf: op_host_services::export::RasterFormat| -> Result<(), String> {
match &single_node {
Some(id) => {
op_web_daemon::export::export_node_raster(scene, id, &path, rf, scale)
op_host_services::export::export_node_raster(scene, id, &path, rf, scale)
}
None => op_web_daemon::export::export_raster(scene, &path, rf, scale),
None => op_host_services::export::export_raster(scene, &path, rf, scale),
}
};
let result: Result<(), String> = match fmt {
Fmt::Png => raster(op_web_daemon::export::RasterFormat::Png),
Fmt::Jpeg => raster(op_web_daemon::export::RasterFormat::Jpeg),
Fmt::Webp => raster(op_web_daemon::export::RasterFormat::Webp),
Fmt::Svg => op_web_daemon::export::export_svg(scene, &path),
Fmt::Pdf => op_web_daemon::export_pdf::export_pdf(scene, &path),
Fmt::Png => raster(op_host_services::export::RasterFormat::Png),
Fmt::Jpeg => raster(op_host_services::export::RasterFormat::Jpeg),
Fmt::Webp => raster(op_host_services::export::RasterFormat::Webp),
Fmt::Svg => op_host_services::export::export_svg(scene, &path),
Fmt::Pdf => op_host_services::export_pdf::export_pdf(scene, &path),
};
if let Err(e) = result {
eprintln!("[export-image] {e}");
@ -398,7 +398,7 @@ mod tests {
use super::*;
// `save_to_path` arrives via `super::*`; `sidecar_path` is only
// needed for test cleanup, so import it directly.
use op_web_daemon::doc_io::sidecar_path;
use op_host_services::doc_io::sidecar_path;
/// A unique temp path under the OS temp dir for a round-trip test.
fn temp_op_path(tag: &str) -> PathBuf {

View file

@ -4,24 +4,24 @@
//! pattern: the press handler raises
//! `agent_settings.pending_provider_connect` (the request seam in
//! op-editor-core), the redraw pump drains it onto a worker thread
//! running [`op_web_daemon::provider_probe::connect_provider`], and a later
//! running [`op_host_services::provider_probe::connect_provider`], and a later
//! frame polls the outcome into `agent_settings` +
//! `chat.discovered_models`.
//!
//! The job struct + outcome normalization live in
//! [`op_web_daemon::provider_probe_host`] (codex Issue 5 — the job is a
//! [`op_host_services::provider_probe_host`] (codex Issue 5 — the job is a
//! `DesktopApp` field); this residual keeps only the `impl DesktopApp`
//! pump, which drives the job through its public API.
use op_editor_core::agent_settings::ProviderConnectOutcome;
use crate::DesktopApp;
use op_web_daemon::model_discovery::model_entry_to_ec;
use op_web_daemon::provider_probe::ProbeOutcome;
use op_web_daemon::provider_probe_host::normalize_provider_probe_outcome;
use op_host_services::model_discovery::model_entry_to_ec;
use op_host_services::provider_probe::ProbeOutcome;
use op_host_services::provider_probe_host::normalize_provider_probe_outcome;
// Re-export so `crate::provider_probe_host::ProviderConnectJob` (the
// `DesktopApp` field type in `main.rs`) still resolves with zero churn.
pub use op_web_daemon::provider_probe_host::ProviderConnectJob;
pub use op_host_services::provider_probe_host::ProviderConnectJob;
impl DesktopApp {
/// Pump: poll a finished probe into editor state, then start the

View file

@ -4,13 +4,13 @@
//! loads a canonical `.op`, runs the SAME jian layout pass the editor
//! canvas uses (`op_pen_loader::editor_state_to_layout_scene`), and
//! writes one cropped PNG per top-level node on the active page via
//! [`op_web_daemon::export::export_node_raster`] — node-only screenshots (no
//! [`op_host_services::export::export_node_raster`] — node-only screenshots (no
//! editor chrome) for the model-design benchmark. No GUI / GL window,
//! so it runs headless in CI / scripts.
use std::path::PathBuf;
use op_web_daemon::export::{export_node_raster, RasterFormat};
use op_host_services::export::{export_node_raster, RasterFormat};
/// When `--render-shots` is present on argv, render node PNGs and return
/// `true` so `main` exits 0; otherwise return `false` and let the normal

View file

@ -2,7 +2,7 @@
//!
//! The headless settings persistence (load / save / fingerprint /
//! save_if_changed + the on-disk payload DTOs + id allocators + locale
//! detection) lives in [`op_web_daemon::settings_io`]; this residual
//! detection) lives in [`op_host_services::settings_io`]; this residual
//! keeps only `touch_recent`, which writes through the live
//! `WidgetHostNative` (orphan rule — it takes the host type).

View file

@ -14,7 +14,7 @@ path = "src/lib.rs"
# `gl-host` (NON-default) enables the desktop winit/glutin interactive runner
# tier + skia-safe's `gl` feature + accesskit. Consumers that need the GUI host
# (`op-host-desktop`, `op-app`) opt in via `features = ["gl-host"]`. Left OFF —
# the default, used by `op-web-daemon` (via `default-features = false`) and by
# the default, used by `op-host-services` (via `default-features = false`) and by
# the mobile `cargo check` (default features) — it yields a raster-only library:
# no winit / glutin / jian-host-desktop / accesskit, and skia-safe WITHOUT `gl`
# (so it emits no native GL link directives). The raster `NativeBackend` +

View file

@ -1,13 +1,13 @@
[package]
name = "op-web-daemon"
name = "op-host-services"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
description = "OpenPencil headless web-canvas daemon — serves the wasm shell + REST/MCP, no winit/skia-GL"
description = "OpenPencil headless host services — the GUI-free backend (serve-web daemon, MCP, AI chat/orchestration, export, persistence) shared by op-host-desktop + op-host-web-server; no winit/skia-GL"
[lib]
name = "op_web_daemon"
name = "op_host_services"
path = "src/lib.rs"
[features]
@ -64,7 +64,7 @@ zip = "2"
glam = { version = "0.29", default-features = false, features = ["std"] }
dirs = "5"
# Raster + pdf skia for the export endpoint — NO `gl` (the headless server links
# no GL). Cargo keeps this gl-free in the isolated `-p op-web-daemon` build.
# no GL). Cargo keeps this gl-free in the isolated `-p op-host-services` build.
skia-safe = { version = "0.97.0", default-features = false, features = [
"binary-cache",
"textlayout",

View file

@ -0,0 +1,51 @@
//! OpenPencil headless host services — the GUI-free host backend.
//!
//! Everything the host does that doesn't need a window: the in-memory
//! `.op` document daemon (whole-document REST sync + SSE for the browser
//! `op-host-web` shell + external MCP/CLI clients), the stdio + HTTP MCP
//! servers, the AI chat providers + design orchestration, server-side
//! PNG/SVG/PDF export (skia raster), document/settings persistence, and
//! model/provider discovery. It links NO winit / glutin / muda /
//! accesskit adapters / skia-GL — the desktop GUI stack stays in
//! `op-host-native` / `op-host-desktop`.
//!
//! Both hosts depend on this crate: `op-host-desktop` embeds it for its
//! own `--serve-web` / MCP / chat / export, and the thin
//! `op-host-web-server` binary is just an argv dispatcher over it.
//!
//! Modules were migrated here out of `op-host-desktop` over Phases 2–5 of
//! the extraction (plan:
//! `openpencil-docs/superpowers/plans/2026-06-19-op-web-daemon-extraction.md`).
// Migrated headless modules (Phases 2-5), kept alphabetical.
pub mod acp_agent_probe_host;
pub mod ai_proxy;
pub mod chat_agent_loop;
pub mod chat_attachment;
pub mod chat_builtin_http;
pub mod chat_canvas_tools;
pub mod chat_claude;
pub mod chat_copilot;
pub mod chat_http_server;
pub mod chat_intent;
pub mod chat_provider_llm;
pub mod chat_runtime;
pub mod chat_spawn;
pub mod chat_subprocess;
pub mod chat_subprocess_quirks;
pub mod chat_system_prompt;
pub mod design_session;
pub mod doc_io;
pub mod export;
pub mod export_pdf;
pub mod mcp_live;
pub mod mcp_serve;
pub mod model_discovery;
pub mod pre_validator;
pub mod provider_probe;
pub mod provider_probe_host;
pub mod provider_probe_models;
pub mod settings_io;
pub mod web_canvas_server;
pub mod web_chat_standard;
pub mod web_static;

View file

@ -4,11 +4,11 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
description = "OpenPencil headless web/MCP server binary — links op-web-daemon only, no winit/skia-GL"
description = "OpenPencil headless web/MCP server binary — links op-host-services only, no winit/skia-GL"
[[bin]]
name = "op-host-web-server"
path = "src/main.rs"
[dependencies]
op-web-daemon = { path = "../op-web-daemon" }
op-host-services = { path = "../op-host-services" }

View file

@ -1,6 +1,6 @@
//! Headless OpenPencil web / MCP server binary.
//!
//! Links only `op-web-daemon` (the extracted headless daemon) — no
//! Links only `op-host-services` (the extracted headless daemon) — no
//! winit / glutin / muda / accesskit adapters / skia-GL — so the
//! container / server image stays GUI-free. It mirrors the `--serve-web`
//! / `--mcp` / `--mcp-http` argv arms the desktop binary's
@ -27,12 +27,12 @@ fn main() {
// `--serve-web <port> [doc] [--host <addr>]`: doc optional
// (empty document otherwise); `--host` opts in to a
// non-loopback bind.
let (port, path, host) = op_web_daemon::web_canvas_server::parse_serve_web_args(args)
let (port, path, host) = op_host_services::web_canvas_server::parse_serve_web_args(args)
.unwrap_or_else(|e| {
eprintln!("op-host-web-server --serve-web: {e}");
exit(2);
});
if let Err(e) = op_web_daemon::web_canvas_server::run_web_canvas(path, port, &host) {
if let Err(e) = op_host_services::web_canvas_server::run_web_canvas(path, port, &host) {
eprintln!("op-host-web-server --serve-web: {e}");
exit(1);
}
@ -42,7 +42,7 @@ fn main() {
eprintln!("op-host-web-server --mcp: missing <path> arg");
exit(2);
};
if let Err(e) = op_web_daemon::mcp_serve::run(PathBuf::from(path)) {
if let Err(e) = op_host_services::mcp_serve::run(PathBuf::from(path)) {
eprintln!("op-host-web-server --mcp: {e}");
exit(1);
}
@ -62,7 +62,7 @@ fn main() {
eprintln!("op-host-web-server --mcp-http: missing <path> arg");
exit(2);
};
if let Err(e) = op_web_daemon::mcp_serve::run_http(PathBuf::from(path), port) {
if let Err(e) = op_host_services::mcp_serve::run_http(PathBuf::from(path), port) {
eprintln!("op-host-web-server --mcp-http: {e}");
exit(1);
}

View file

@ -1,47 +0,0 @@
//! OpenPencil headless web-canvas daemon.
//!
//! The Rust analog of the TS web app's Nitro routes: it owns the canonical
//! `.op` document in memory and serves it (whole-document REST sync + SSE) to
//! the browser WASM shell (`op-host-web`) and to external MCP/CLI clients, plus
//! server-side PNG/screenshot export via skia raster. It links NO winit /
//! glutin / muda / accesskit / skia-GL — the desktop GUI stack stays in
//! `op-host-desktop`.
//!
//! Modules are migrated here from `op-host-desktop` over Phases 2-5 of the
//! op-web-daemon extraction (see
//! `openpencil-docs/superpowers/plans/2026-06-19-op-web-daemon-extraction.md`).
//! Both `op-host-desktop` (for its `--serve-web` mode) and a thin
//! `op-host-web-server` binary depend on this crate.
// Migrated headless modules (Phases 2-5), kept alphabetical.
pub mod acp_agent_probe_host;
pub mod ai_proxy;
pub mod chat_agent_loop;
pub mod chat_attachment;
pub mod chat_builtin_http;
pub mod chat_canvas_tools;
pub mod chat_claude;
pub mod chat_copilot;
pub mod chat_http_server;
pub mod chat_intent;
pub mod chat_provider_llm;
pub mod chat_runtime;
pub mod chat_spawn;
pub mod chat_subprocess;
pub mod chat_subprocess_quirks;
pub mod chat_system_prompt;
pub mod design_session;
pub mod doc_io;
pub mod export;
pub mod export_pdf;
pub mod mcp_live;
pub mod mcp_serve;
pub mod model_discovery;
pub mod pre_validator;
pub mod provider_probe;
pub mod provider_probe_host;
pub mod provider_probe_models;
pub mod settings_io;
pub mod web_canvas_server;
pub mod web_chat_standard;
pub mod web_static;

View file

@ -2,7 +2,7 @@
# Headless-boundary guard for the web / MCP server binary.
#
# The whole point of `op-host-web-server` (Approach Y) is that it links the
# extracted `op-web-daemon` and NOTHING from the desktop GUI stack: no winit /
# extracted `op-host-services` and NOTHING from the desktop GUI stack: no winit /
# glutin / casement (windowing), no muda (native menus), no accesskit platform
# ADAPTERS (a11y bridges), and skia-safe WITHOUT the `gl` feature (raster only).
# This guard fails the build if any of those leak into the isolated dep graph,