From 6a82142d69b156a41e22a5cc96721bac2f47bad9 Mon Sep 17 00:00:00 2001 From: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:33:21 +0000 Subject: [PATCH] GH-251: document controller polling and edit interactions Document polling of selection/settings/control ranges and shared edit interactions. Clarify explicit-photo reads, empty selection and navigation, and correct photo-tool ID descriptions and MCP initialization guidance. Partial documentation work for issue 251; hardware feedback remains outside this change. Co-authored-by: Codex --- crates/mcp/src/server.rs | 2 +- crates/mcp/src/tools.rs | 4 +-- docs/control-protocol.md | 72 ++++++++++++++++++++++++++++++++++++++++ docs/mcp.md | 13 +++++++- docs/parity-checklist.md | 4 +-- 5 files changed, 89 insertions(+), 6 deletions(-) diff --git a/crates/mcp/src/server.rs b/crates/mcp/src/server.rs index ba64ef45..4565db76 100644 --- a/crates/mcp/src/server.rs +++ b/crates/mcp/src/server.rs @@ -25,7 +25,7 @@ const INTERNAL_ERROR: i64 = -32603; const RESOURCE_NOT_FOUND: i64 = -32002; const INSTRUCTIONS: &str = "LightCraft is a non-destructive photo library and raw developer (a Lightroom alternative). \ -Photos have integer ids; most editing acts on the *active* photo (tools that take `id` make that photo active first). \ +Photos have integer ids; most editing acts on the *active* photo (`set_develop`, `crop` and `export` with an explicit photo id end the current edit interaction, replace the selection with that one photo and make it active; `get_develop` and `render_photo` do not change the selection). \ Typical loop: query_photos → select_photos (or pass id) → list_controls → set_develop {values: {\"light.exposure\": 0.5}} → \ render_preview to look at the result → export. Positions (crop rects, mask points, brush strokes) are normalized image \ coordinates 0..1 with the origin at the top-left. Every action is a command: command_list shows ids and parameter \ diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index 93b0567d..993e692e 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -182,7 +182,7 @@ pub fn helper_tools(has_ui: bool) -> Vec { "get_develop", "Get develop settings", "The full develop settings JSON of a photo (white balance, light, color, curves, mixer, grading, effects, crop, masks, spots…).", - json!({"id": photo_id}), + json!({"id": {"type": "integer", "description": "Photo id (default: the active photo). Reads without changing the selection."}}), &[], ), tool( @@ -216,7 +216,7 @@ pub fn helper_tools(has_ui: bool) -> Vec { "Render photo", "Render a photo with its current develop settings (cropped) and return it as an image so you can look at the result.", json!({ - "id": photo_id, + "id": {"type": "integer", "description": "Photo id (default: the active photo); selection is unchanged"}, "size": {"type": "integer", "description": "Long edge in pixels (default 1024, max 4096)"}, "format": {"type": "string", "enum": ["png", "jpeg"], "description": "Image encoding (default png; jpeg is smaller)"}, "path": {"type": "string", "description": "Also save the rendered file here"} diff --git a/docs/control-protocol.md b/docs/control-protocol.md index da38cf64..cdc35b63 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -140,6 +140,78 @@ opens as one. With no library open (the library problem window) it is an error i places (`EngineError::LightroomCatalog`, so `lightcraft-cli --library` and `mcp --library` too). `library.importLightroom {status: true}` reports the catalog's `path` after the task ends as well as while it runs. +## External controllers + +External apps can use this channel to send commands and poll the running desktop app for feedback +(e.g. knob values or LEDs). Start the app with `--control PORT`, then keep a JSON-lines connection to +`127.0.0.1:PORT`. Discover command ids, parameters and current enablement with `engine.commands`; +use control ids and ranges from `develop.controls` rather than translated labels or widget positions. + +| Task | Command through `engine.execute` | Result / effect | +|---|---|---| +| Read selection and active photo | `library.state {}` | `selection: {ids, active}`; `active` is `null` when no photo is active | +| Read photo identity and settings together | `photo.inspect {}` | Active photo's `id`, `develop`, rating, flag and history in one reply | +| Read a specific photo without selecting it | `photo.inspect {id}` / `develop.get {id}` | Photo details / full develop JSON; omitting `id` reads the active photo | +| Read global slider metadata and values | `develop.controls {section?}` | Array of `{id, label, section, min, max, default, step, decimals, track, value}` for the active photo | +| Set absolute slider values | `develop.set {values: {"light.exposure": 0.7}}` | Undoable edit of the active photo; also accepts `{control, value}` | +| Move a relative control | `develop.adjust {control: "light.exposure", delta: 0.1}` | Adds to the current value, clamped to the control's range | +| Change photo | `library.select {ids: [photoId]}` / `library.next {}` / `library.previous {}` | Changes the active photo; next/previous follow the current view's order | +| Undo / redo | `edit.undo {}` / `edit.redo {}` | Restore settings; poll again for feedback | + +With Auto Sync enabled, `develop.set` and `develop.adjust` can also change the other selected photos. +Only changed settings propagate; spot removal, red eye and the rendering process stay per photo. +During an interaction, propagation occurs when it ends. Re-read affected photos after the edit. +`develop.autoSync {}` toggles the mode; it is not a state query. + +For example, send these requests one at a time and read each reply before sending the next: + +```json +{"id":1,"method":"engine.commands"} +{"id":2,"method":"engine.execute","params":{"command":"photo.inspect"}} +{"id":3,"method":"engine.execute","params":{"command":"develop.controls","params":{"section":"light"}}} +{"id":4,"method":"engine.execute","params":{"command":"develop.set","params":{"values":{"light.exposure":0.7}}}} +{"id":5,"method":"engine.execute","params":{"command":"photo.inspect"}} +``` + +Replies use `{id, ok, result}` or `{id, ok: false, error}`. In the photo snapshot, exposure is +`result.develop.light.exposure`; the controls array's matching `light.exposure` entry has `value`, +`min: -5`, `max: 5` and `step: 0.01`. `wb.temp` is Kelvin on raws with colour matrices and relative +(6500 = as shot) elsewhere, as described above. Re-read values after edits to reflect the stored result. +Local mask adjustments are in the photo snapshot's `develop.masks`; use the mask commands from the +registry (e.g. `mask.adjust`) for those, rather than treating global control values as mask values. + +Feedback uses polling: the channel sends replies to requests, with no unsolicited change events. +Compare successive `photo.inspect` snapshots for photo-id or develop-setting changes, including GUI +edits, photo navigation, presets and undo/redo. GUI slider updates are visible while a drag is in +progress; ending a changed interaction commits it as one undo step (unchanged drags add none). +A controller can group `develop.set` / `develop.adjust` updates between +`develop.beginInteraction {label}` and `develop.endInteraction {}`. The GUI and controllers share +one interaction: another `develop.beginInteraction` or `library.select` ends the current one. +Update controller feedback from reads without sending the same values back as new edits. + +Each request is a separate snapshot. `photo.inspect` keeps identity and settings together; a +`develop.controls` reply has no photo id, so do not associate it with an earlier cached selection +when the user is changing photos. Refresh the selection and retry those reads after navigation. +With no active photo, `develop.controls` returns default values and `photo.inspect` / `develop.get` +without `id` return an error; check `library.state` before treating defaults as photo feedback. +Photo ids are local to the current library/session. After reconnecting or opening another library, +discard cached photo mappings and re-read state; matching photo ids alone do not identify the library. +For persistent libraries, poll `library.info` for the current `path` as well. Polling observes the state +at each read, not every intermediate change. + +The same queries and edits are available through the connected CLI: + +```sh +lightcraft-cli run --connect 127.0.0.1:7980 library.state photo.inspect develop.controls section=light +lightcraft-cli run --connect 127.0.0.1:7980 develop.adjust control=light.exposure delta=0.1 photo.inspect +``` + +For MCP, use `lightcraft-cli mcp --connect 127.0.0.1:7980`: `run_command` reaches the commands above, +`list_controls` / `set_develop` provide slider helpers, and `resources/read` polls +`lightcraft://library`, `lightcraft://photo/active`, `lightcraft://develop/active` and +`lightcraft://controls` ([mcp.md](mcp.md#resources)). Headless CLI/MCP mode owns a separate engine +session; use connect mode for feedback from edits in the desktop app. + ## Headless rendering (no window, no GPU) The egui UI can be rasterized on the CPU (`crates/ui-egui/src/softpaint.rs`, driven by diff --git a/docs/mcp.md b/docs/mcp.md index e1fb0737..e09138cf 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -170,7 +170,11 @@ per-request `_meta` receive `resultType: complete` and list/read cache hints; re | `render_photo {id?, size?, format?, path?}` | Render with current settings → **image content** (PNG, or JPEG with `format: "jpeg"`), long edge `size` (default 1024) | | `export {path \| dir, id? \| ids?, format?, longEdge? \| shortEdge? \| width?/height? \| megapixels? \| percent?, dontEnlarge?, ppi?, quality?, colorSpace?, bitDepth?, …}` | Full-quality render to `.png` / `.jpg` / `.tif` / `.webp` / `.avif` / `.jxl` (`jxlLossless`, `jxlEffort`, `jxlProgressive`); `format: "original"` copies the file + an XMP sidecar with the edits, `format: "dng"` writes raw photos as DNG with the edits embedded. No size param = 3000 px long edge; `longEdge: 0` = full size (cropped, native resolution); `width` + `height` fit either orientation; `dontEnlarge` defaults to true | -Tools taking `id` make that photo active first; without it they act on the active photo. +`get_develop {id}` and `render_photo {id}` use that photo without changing the selection. +`set_develop`, `crop` and `export` with a supplied photo `id` end the current edit interaction, +replace the selection with that one photo and make it active. Without `id`, +`get_develop`, `set_develop` and `crop` use the active photo; `render_photo` also falls back to the first +selected photo, and `export` uses the selection for batch export, as above. ### Connect mode only @@ -208,6 +212,13 @@ still reaches every command. | `lightcraft://develop/active` | The active photo's develop settings (`develop.get`) | | `lightcraft://controls` | Every develop control with its current value (`develop.controls`) | +Read these resources again to poll current state; they are snapshots, and `initialize` advertises +`resources.subscribe: false`. `lightcraft://photo/active` includes the photo's `id` and `develop` +settings together; `lightcraft://controls` contains slider values for the active photo at that read. +Separate reads can observe different active photos. For controller feedback from GUI edits, use +connect mode; headless mode owns a separate session. See [External controllers](control-protocol.md#external-controllers) +for command mapping, live slider values and polling semantics. + ## Example session ```text diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 77ae26e4..18812b6d 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -53,9 +53,9 @@ cited here still exists. Formerly docs/parity.md. | Y. Menus | 84 | 0 | 4 | 8 | 47/47 (100%) | 23/23 (100%) | | Z. Keyboard shortcuts (desktop) | 76 | 3 | 3 | 1 | 49/52 (94%) | 23/24 (96%) | | Lightroom Classic extras | 34 | 16 | 35 | 8 | — | 24/26 (92%) | -| **Total** | 407 | 50 | 73 | 35 | 192/200 (96%) | 145/157 (92%) | +| **Total** | 408 | 51 | 72 | 35 | 193/201 (96%) | 145/157 (92%) | -Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **81.5%** of 530 in-scope rows — P0 98.0% of 200 · P1 95.2% of 157 · P2 49.7% of 172. +Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **81.6%** of 531 in-scope rows — P0 98.0% of 201 · P1 95.2% of 157 · P2 50.0% of 172. ## Top gaps and shortcut notes