Skip to main content

agentmux_srv/server/app_api/
attachments.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! `attachments.ingest` / `attachments.cancel` / `attachments.info` — image
5//! attachments in the agent composer
6//! (docs/specs/SPEC_AGENT_PANE_IMAGE_ATTACHMENTS_2026_09_26.md §6). The work
7//! itself lives in `backend::attachments`; these only adapt it to the RPC
8//! engine. Typed registration, so `frontend/types/rpc/` gets the bindings.
9
10use super::*;
11use crate::backend::attachments;
12
13pub fn register(engine: &Arc<WshRpcEngine>, state: &AppState) {
14    let svc = attachments::init(state.broker.clone(), state.config_watcher.clone());
15
16    let ingest_svc = svc.clone();
17    engine.register_typed(
18        COMMAND_ATTACHMENTS_INGEST,
19        move |req: CommandAttachmentsIngestData, _ctx| {
20            let svc = ingest_svc.clone();
21            async move {
22                if req.batch_id.trim().is_empty() {
23                    return Err("attachments.ingest: batch_id is required".to_string());
24                }
25                // Classification stats files and reads a few header bytes
26                // each; a folder drop can touch thousands, so keep it off
27                // the async workers. Processing then runs in the background.
28                let handle = tokio::runtime::Handle::current();
29                tokio::task::spawn_blocking(move || {
30                    let _guard = handle.enter();
31                    svc.ingest(req)
32                })
33                .await
34                .map_err(|e| format!("attachments.ingest: {e}"))
35            }
36        },
37    );
38
39    let cancel_svc = svc.clone();
40    engine.register_typed(
41        COMMAND_ATTACHMENTS_CANCEL,
42        move |req: CommandAttachmentsCancelData, _ctx| {
43            let svc = cancel_svc.clone();
44            async move {
45                svc.cancel(&req.batch_id);
46                Ok(serde_json::Value::Null)
47            }
48        },
49    );
50
51    let copy_svc = svc.clone();
52    let mstore = state.mstore.clone();
53    engine.register_typed(
54        COMMAND_ATTACHMENTS_COPY_TO_WORKDIR,
55        move |req: CommandAttachmentsCopyToWorkdirData, _ctx| {
56            let svc = copy_svc.clone();
57            let mstore = mstore.clone();
58            async move {
59                let block = mstore
60                    .get::<Block>(&req.block_id)
61                    .map_err(|e| format!("attachments.copy-to-workdir: {e}"))?
62                    .ok_or_else(|| "attachments.copy-to-workdir: no such pane".to_string())?;
63                let cwd = obj::meta_get_string(&block.meta, "cmd:cwd", "");
64                if cwd.is_empty() {
65                    return Err("No working folder is set for this agent.".to_string());
66                }
67                let path = tokio::task::spawn_blocking(move || {
68                    svc.store()
69                        .copy_original_to(&req.id, std::path::Path::new(&cwd), &req.name)
70                })
71                .await
72                .map_err(|e| format!("attachments.copy-to-workdir: {e}"))?
73                .map_err(|e| format!("Couldn't copy the file into the working folder: {e}"))?;
74                Ok(AttachmentsCopyToWorkdirResult {
75                    path: path.to_string_lossy().into_owned(),
76                })
77            }
78        },
79    );
80
81    let info_svc = svc;
82    engine.register_typed(
83        COMMAND_ATTACHMENTS_INFO,
84        move |req: CommandAttachmentsInfoData, _ctx| {
85            let svc = info_svc.clone();
86            async move {
87                let items = svc.info(&req.ids).await;
88                Ok(AttachmentsInfoResult { items })
89            }
90        },
91    );
92}