git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitd599a15bbbc2c528f7b35a7d15cc740963db0edf
parent3a256784d2
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-27 21:30
feat: get_state reports the status line

The status line carries the load report, node errors and refused edits,
and nothing outside the window could read it. get_state now returns it as
`status`, beside `playbar` and `grid`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md     |  6 +++++-
 src/api.rs    |  2 +-
 src/main.rs   | 13 +++++++++++++
 src/window.rs |  6 +++++-
 4 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index bd4d841..5c13ed2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -79,7 +79,11 @@ The main window runs an embedded MCP server on `127.0.0.1:3001`
 `claude mcp add --transport http cce-designer http://127.0.0.1:3001/mcp`, or
 speak JSON-RPC directly with curl (`initialize` / `tools/list` / `tools/call`).
 There is one tool per `McpAction` variant (tool name = the variant's serde
-tag, dispatched in `apply_mcp_call` in `src/window.rs`) plus `get_state`. The
+tag, dispatched in `apply_mcp_call` in `src/window.rs`) plus `get_state`,
+which returns the project beside three app-state fields that are not part of
+the save: `playbar`, `grid` and `status` — the status line's text as shown,
+which is the way to read a load report, a node error or a refused edit when
+the window is off-screen. The
 tool list lives in `mcp_tools()` in `src/api.rs`; the protocol layer is
 `cce_ui::mcp` (tools-only Streamable HTTP). Keep the enum, the tool list, and
 the schemas in sync — `test_mcp_tools_map_to_actions` enforces the mapping.
diff --git a/src/api.rs b/src/api.rs
index e696af9..3633b14 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -32,7 +32,7 @@ pub(crate) fn mcp_tools() -> Vec<McpTool> {
     vec![
         tool(
             "get_state",
-            "Get the current project state (node tree with params, cameras, pan, current path, selection) as JSON.",
+            "Get the current project state (node tree with params, cameras, pan, current path, selection) as JSON, plus the playbar, the network grid and the status line's text (`status`).",
             no_args(),
         ),
         tool(
diff --git a/src/main.rs b/src/main.rs
index 65e7fa0..c487fde 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4089,6 +4089,19 @@ mod tests {
         assert!(s.last_status_text.contains("Not applied") && s.last_status_text.contains("Threshold"), "{}", s.last_status_text);
     }
 
+    /// get_state carries the status line, so it can be read from outside
+    /// whether or not the window is on screen.
+    #[test]
+    fn get_state_reports_the_status_line() {
+        let mut s = State::new(false);
+        s.update_status_text("Not applied — Threshold: 'abc' is not a number");
+        let (reply, _rx) = std::sync::mpsc::channel();
+        let call = cce_ui::mcp::McpToolCall { name: "get_state".into(), arguments: serde_json::json!({}), reply };
+        let v = s.apply_mcp_call(&call, &mut false).expect("get_state");
+        assert_eq!(v["status"], "Not applied — Threshold: 'abc' is not a number");
+        assert!(v.get("root").is_some(), "the project is still there beside it");
+    }
+
     /// A load says when a project holds a value that does not fit — kept,
     /// but worth knowing about.
     #[test]
diff --git a/src/window.rs b/src/window.rs
index a1f148d..005fd2b 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -629,7 +629,7 @@ impl State {
     /// An MCP tool call: `get_state` returns the project snapshot; every
     /// other tool name is an `McpAction` tag — injected into the arguments
     /// and run through the shared action path.
-    fn apply_mcp_call(
+    pub(crate) fn apply_mcp_call(
         &mut self,
         call: &cce_ui::mcp::McpToolCall,
         needs_redraw: &mut bool,
@@ -659,6 +659,10 @@ impl State {
                 "configured_pitch": [cfg.pitch_x, cfg.pitch_y],
                 "configured_node_size": [cfg.node_w, cfg.node_h],
             });
+            // The status line as shown — the load report, a node error, a
+            // refused edit. The window may be anywhere, or off-screen; this
+            // is how to read it from outside.
+            v["status"] = serde_json::Value::String(self.last_status_text.clone());
             return Ok(v);
         }
         let mut req = if call.arguments.is_object() {