A request sent just as the app expires an idle control connection crosses
the app's id-less idle-close notice: the client's pre-send check saw an
open connection, wrote the request, and read the notice as its reply, so
`--connect` and the MCP server failed a command the app never read.
The app reads nothing after its idle timeout, so such a request never ran.
The app now marks the notice `"closed": "idle"`, and `Remote::call` sends
a request whose reply is that notice once more on a new connection (it
also matches the notice's exact text, for apps from before the marker).
At most one resend per call, whatever the reason; a reply with an id or
any other error is returned as before.
`idle_close_reconnects_without_replaying_a_command` now forces both
orders (notice before the next request, and a request crossing it)
instead of racing the notice; with main's client it fails every run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewing #2952 turned up two more shapes of the same bug, both on the path this branch is about.
`effective_wb` ignores the temp / tint stored on an `As Shot` white balance — it answers the file's
as-shot white — and on a raw edited in Kelvin it also ignores a named preset mode's stored numbers,
through `WbMode::preset`. So a one-field partial that names its scale, `wb {temp: 5800, scale:
"kelvin"}`, merged into such a photo, was stored and then read by nothing: the panel shows 5800 K
while the render stays at as shot, and the stored tint no longer matches what renders. An unmarked
partial already materialized a Custom value through the relative-scale conversion; the named-scale
ones never did. The photo's preparation now writes out the white the photo really renders as Custom
values before the partial lands, for `As Shot` and for preset modes on a Kelvin source. `Auto` keeps
its mode — its gray is resolved and stored when the mode is set, not at render time — and a partial
that names a `mode` itself stays authoritative. Sources without the Kelvin scale (a raw whose camera
has no matrices, a rendered file) had the same swallow, and the number reaches their render now.
The issue asks for one photo-aware merge in every caller that writes settings onto a photo, and names
preset apply. `preset.apply` ran `Preset::apply` → `apply_partial` → `DevelopSettings::merged` and
never saw the photo's scale, so a preset authored by hand naming one white balance number still had
the original bug; measured on a photo at 5000 K / +12, it landed the photo on the relative scale and
moved the tint it never mentioned. The preparation and the normalize-back now wrap `Preset::apply`
per photo, with the amount blending and the mask merging untouched, which also puts an amount below
100 % on a single scale. Presets the app writes (`preset.create` → `extract_groups`) and the crs /
XMP importers name mode, temp, tint and scale together and so land in the branch that behaves
exactly as before.
Tests: the `As Shot` and preset-mode cases in the #2910 merge test, and the new
`preset.apply` test, fail before this commit (the first stores the number in a mode that renders the
photo's old white; the second demotes the photo to `Legacy`); the same test then pins that
`preset.apply` and `develop.merge` agree on a one-field white balance, which is the point of having
one merge.
Docs: the rule for an agent is in `docs/control-protocol.md` and `docs/mcp.md`, recorded on the
`LR-EDIT-COLOR-TEMP` row of `docs/parity-checklist.md`, and in `docs/whats-new.md`.
Co-Authored-By: Claude <noreply@anthropic.com>
`develop.merge`, `lightcraft-cli render --settings` and MCP `set_develop` merged a white balance
partial that names only `temp` or only `tint` straight into the photo's settings, so the field the
partial leaves out — stored on the photo's scale — is re-read on the partial's scale. On a raw
edited in Kelvin that moves the green/magenta balance of photos the partial never mentioned:
`wb {temp: 5800}` on a photo at 5000 K / +12 tint lands on the relative scale, a shift of ~2.5 tint
units.
Resolve a one-field white balance partial against the photo: bring its stored white balance onto
the partial's scale with the photo's own as-shot white, merge, then put the result back on the
scale that photo is edited on. The inverse of `legacy_to_kelvin` is the composition of the two
Bradford adaptations run backwards: `lightcraft_pipeline::local::kelvin_to_legacy`. It maps the
source's own white to the relative scale's 6500 / 0 exactly, and answers None for a white the
relative scale cannot hold. Partials naming both fields or neither, presets and the crs / Luminar
importers are untouched: they always write `temp`, `tint` and `scale` together.
Tests: the round trip over the slider's reach, in both directions, and hostile inputs
(`crates/pipeline/src/local.rs`); the merge cases both ways plus the unchanged ones, and a restated
value that must not move the white at all (`crates/engine/src/tests_color.rs`). Both fail before
the change.
Co-Authored-By: Claude <noreply@anthropic.com>
Conflicts:
- xtask/src/main.rs: upstream's runners (cargo test or nextest, no
examples, doctests apart, one wasm `cargo check --keep-going`) with
#2302's `ci --quick` on top: test_selected builds the same targets
(--lib --bins --tests), the wasm check takes the changed crates in one
call, the full test step is upstream's.
- apps/lightcraft/src/control_server.rs: upstream's `before` closure, with
the batch's no-delay sockets kept.
- docs/parity-checklist.md: LR-BEHAV-ACCESS from main (tests/it path),
LR-BEHAV-LOCALE / LR-BEHAV-LOCALIZE from the batch (Polish).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: dev profile keeps line tables for workspace crates and no debug info for dependencies
* Control server tests: persistent_requests_and_slow_replies_outlive_the_idle_read_timeout no longer races the idle timeout
Each request (and the closing half-shutdown) reaches the socket before the
worker reads, so on a slow machine only the reply waits outlast the 100 ms
idle timeout, as the test intends. The idle test sends its input the same
way, and the no-frames test allows 5 s instead of 2 s to give up.
* CI: one integration-test binary per crate
Each crate's tests/*.rs become modules of tests/it/main.rs (51 test files
into 13 binaries, plus 6 that stay their own). Inner #![cfg] attributes
move to the mod lines. Files that change state for the whole process keep
a binary of their own: codecs jxl_tail_heap and raw sr2_heap (dhat global
allocator), gpu shutdown, shutdown_first and memory, ui-egui shutdown.
Docs, comments and the GPU probe name the new paths and targets
(--test it <module>::); the CI report names an it binary's skips after
the crate.
* CI: tests under nextest, doctests apart, one wasm check
xtask ci runs the tests with cargo nextest run --lib --bins --tests when
cargo-nextest is installed, else cargo test with the same targets and a
one-line notice; doctests run after them with cargo test --workspace --doc.
The heif and hardware GPU steps use the same runner. Examples stay out of
the test build (clippy --all-targets still checks them). Concurrent tests
under nextest follow the RUST_TEST_THREADS cap (NEXTEST_TEST_THREADS).
The wasm check is one cargo check --keep-going over the same crates, and
clippy runs with --keep-going.
* CI: tests run under cargo test by default, nextest with LIGHTCRAFT_NEXTEST=1 (per-process scene cache)
Folder templates (and rename / export names) can now give month folders by
name: {date:%Y}/{date:%B} → 2026/January, {date:%b} → Jan. The names are
English in every interface language so a library's folders don't change
with the language; a month that doesn't parse gives nothing (a folder
level becomes "unknown").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightroom's Resize to Fit has two boxes: Dimensions (the larger number to
the long edge, whichever way round the photo is) and Width & Height (the
width limited to W and the height to H as the photo stands). LightCraft
had only the first, labelled "Width × Height". Add `ResizeMode::WidthHeight`,
`fit: widthHeight` beside `width` + `height` in app.export, the mode in the
Export dialog (the old one is now called Dimensions), and the docs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
Resolved against rb-base-36: Scanned keeps the base's rewrite flag next to magic and tag; Exif keeps the Panasonic lens fallback; locale catalogs merged (one duplicate key dropped).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The connect-mode MCP client accepted any newline-less or wrong-id reply as
the answer to its request. Require a complete newline-terminated reply of
at most 64 MiB, the matching integer id, boolean ok and a result or string
error; otherwise close the connection and report an unknown outcome without
resending. Idless pre-dispatch refusals from the control server still work.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Wake the serving loop after protocol output fails and preserve the actual writer error. Use one process-owned stdin worker so the CLI can finish normal backend cleanup without waiting for client EOF, and terminate the output worker even when a progress hook is retained.
Closes#1527
Co-Authored-By: OpenAI Codex <noreply@openai.com>
A raw imported while its format couldn't be decoded kept preview_only until a
manual Reload, so new decoders in a release never reached existing photos.
When a library opens in another app version, its preview-only photos whose
stamp differs are probed again in the background and get exactly what Reload
from Disk gives them (shared reload_ops): a file that decodes now loses the
flag, one that still doesn't gets the current reason and a fresh stamp. The
result is one journaled system batch, not an undo step; develop settings and
history are kept. Unreadable files are left for next time, photos changed
while probing are skipped, and a result for a library closed meanwhile is
dropped. A toast says how many raws now develop from their raw data.
- engine: cmd/recheck.rs (stale / probe / apply) and
library.recheckPreviewOnly {all?} -> {checked, updated, cleared}; refused
without file access, where every probe would clear every flag
- desktop: recheck::tick once per library generation, through tasks::spawn
("Checking raw files" in the activity stack)
- the preview-only notice (Edit, Info) gains a Reload from Disk button
- strings translated in all nine catalogs; docs/mcp.md and the parity row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Photos imported before focus points were read got one only through Reload from Disk, which
hashes whole files on the UI thread. Now:
- lightcraft_meta::focus::read_header reads only what the decoder looks at: IFD0 (Make,
Model), the Exif IFD, the maker note's IFD and the focus tags' values (a JPEG: its segments
up to the Exif one), walked round by round and decoded with the whole-file code from a
buffer of the file's length holding just those ranges. Bounded rounds, bytes (2 MB) and
size (1 GB). One 32 KB read per α1 / A7S III ARW; on 109 real files (83 with a point)
it gives the whole-file read's point for every one.
- A background job (crates/engine/src/focus_scan.rs) reads photos with no point from a
supported body (by the stored camera; nothing else is opened), ARW or JPEG, one at a
time in path order, with a "Reading focus points" activity row (Cancel::Flag). The UI's
per-frame focus.pump gives it the face scan's pace (paused while dragging or typing) and
starts it once per library opening. Commits every 200 photos or 3 s as system ops (no
undo step, no sidecar write), only meta.focus, only on photos that still have none at the
same path. "Looked, none" goes to focus-scanned.bin beside the library, headed by the
decoder's tag so a decoder knowing more bodies starts afresh; unreadable files are retried
at the next opening.
- photo.readFocusPoints {ids?, wait?} (Photo > Read Focus Points) runs it on demand.
Measured cold on a 5400 rpm USB drive: 200 α1 / A7S III ARWs in 1.9 s (~100 photos/s,
32 KB each), so 58,796 raws take about 10 minutes instead of re-reading 2.25 TB.
Read the autofocus point from Sony maker notes (FocusLocation 0x2027, FocusFrameSize
0x2037, FocusMode 0x201b, AFAreaModeSetting 0x201c, AFTracking 0x2021 and the area mode
used from the enciphered 0x9402 block), for the ILCE-1 and ILCE-7SM3, whose layout was
checked against ExifTool's output on 333 real ARWs. Manual focus, centre-only Multi
reports and unchecked bodies give no point. The box is stored on the upright photo as
Meta.focus (omitted when unknown; no catalog format change), filled at import and by
Reload from Disk.
View > Show Focus Point (Alt+F) draws it in Detail, Compare and Survey through rotation
and crop; View > Zoom to Focus Point (on by default) makes Toggle Zoom centre on it; the
Info panel shows "AF-C · Wide · Animal Eye Tracking". photo.inspect returns meta.focus.
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 <codex@openai.com>