Commit Graph

906 Commits

Author SHA1 Message Date
Nathaniel Ledford ea33301e6e
#323: a drag keeps the original its superseded drafts decoded; textures convert faster
A drag supersedes every request before it finishes, and the source a superseded
draft had decoded was thrown away with it, so a window cut from a full-size
original decoded it again for every tick. A view slot's superseded draft that is
newer than what the view shows now also hands its source to the session (late
results of other slots stay ignored, as before). Render results become textures
with a plain copy for the opaque case instead of egui's per-pixel premultiplying
conversion, on the UI thread.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 2f7d248d61
#323: a window does not keep the whole source uploaded with its stages
Profiling a slider drag at 100 % and 400 % found each tick of the window render
taking about 60 ms: the 24 MP source (288 MB) uploaded to the device was kept with
the view's stages, counted against the stage budget, and the stages cleared every
frame, so every tick uploaded and resampled again. A source over 96 MB is now
uploaded for the render only; the stages keep the window's own sampled pixels.
A tick costs 7-12 ms instead. equivalence.rs covers it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 35c9b67944
#323: test: no window over a picture that is not the frame (#280)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 6a07c35431
#323: hold the render sizes while a pinch or two-finger scroll runs
A continuous trackpad pinch changes the zoom every frame, and with it the sizes
the loupe plans, so it would have been a stream of re-renders of every size in
between. SizeHold (pure, tested) keeps the plan from before the gesture while it
runs and for a quarter of a second after; the view is drawn from the renders it
has, magnified by the GPU, and planned afresh when the gesture is quiet (a repaint
is asked for at that moment). Another photo resets the hold.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 72bab50c6e
#323: Before/After at 1:1 compares like with like
The Before image was a stand-in capped at 2560 px beside an After at full
resolution, so a 1:1 comparison misled. Both sides now get the same treatment:
the same whole-frame size and, once zoomed, a window each (Slot::RegionBefore
beside Slot::Region) cut from the same frame, in side by side, top/bottom, the
\ original view and both wipes (the Before window is clipped to its side of the
line). Each side's windows and tiles are tracked apart, a side is freed when it
is no longer shown, and ui.inspect reports regionBefore.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 136b2851ee
#323: engine: region_job_before renders a window of the photo without its edits
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 83d0304ce6
#323: the per-view stage caches are counted against the memory budget
The loupe's, the zoom window's and the before / hover renders' stage caches hold
decoded-image sized buffers that the engine's budget did not count. A quarter of
the memory budget is now theirs: each frame the renderer clears what exceeds it,
the least reusable first (hover, before, compare, second window, the zoom window)
and the open photo's own view last (stage_trim_order, pure and tested). The
inspector's memory report shows the budget and how often caches were trimmed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 3255cda67e
#323: the loupe renders the canvas, plus the visible window at no more than 100 %
At 8:1 on a 24 MP photo every drag frame rendered the whole 24 MP frame, and a
Retina fit view (about 2800 px) decoded the original of every photo stepped to.
A new render plan (region::plan, pure and tested) makes the cost follow the
canvas, not the zoom:
- the whole-frame render is canvas-sized and never above the preview source
  level, the preview size limit or the texture side (a drag drafts that);
- once the view is zoomed past it (by more than 25 %) the visible window is
  rendered, cut from a frame that is at most the photo's own pixels, so beyond
  100 % the GPU magnifies it instead of a bigger render being made; this also
  ends the duplicated second render above 100 %;
- a window is drafted during a drag and a draft of an older value still draws;
- a user who capped the preview size keeps the whole-frame render at fit;
- no window where the picture on screen isn't the frame's own aspect (an
  unsupported raw's embedded JPEG cropped differently, #280).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford b7f632f5c0
#323: no loupe texture is larger than the GPU allows
The Automatic preview size let the loupe texture reach 8192 px on every
platform. The browser build draws with egui_glow, which panics on a texture
larger than the GPU takes, and many WebGL devices allow only 2048 or 4096.
Every render size the loupe asks for (loupe, neighbour prefetch, hover and
Before stand-ins, compare tiles, the second window) and every zoom window is now
clamped to the context's max_texture_side (2048 when the host reports none,
512 at the least). Headless drivers can pretend a small GPU for tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 63cfc6ab70
#323: parity, GPU and control docs: sharp zoomed views by window render
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 930d14aacd
#323: fix the preview_limit doc comment (largest, not lowest)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford ff9c3fb2ec
#323: windows: refuse what cannot fit, grow Auto Mask windows by the dabs near them
Review findings: window growth for spots was capped silently and then rendered
wrongly; Auto Mask growth took the whole stroke's bounding box; hidden masks grew
windows; the wasm window memory cap was not enforced. window_for_reads_checked
returns None when what a window reads would need more than 8192 px (4096 on
wasm) along an axis, Session::region_job returns None then (the loupe keeps the
whole-frame render), Auto Mask growth is per dab near the request (once, not
chained along the stroke) and ignores hidden masks. Docs say what the limits are.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford a1d907bf0f
#323: a tight crop at its own pixels reads the source level its pixels need
An adversarial review found that sizing the loupe render from the cropped frame
(previous commit) left the source level chosen from the output edge alone: a
crop whose own long edge is under 2560 px was drawn from the 2560 px preview of
the whole photo, soft though sized right. The level now follows the pixels the
output needs along the source's long edge (output edge / crop fraction); grid
thumbnails keep their cheap level.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 05618b2ad4
#323: windows on the GPU; Auto Mask strokes grow the window; dab references at f32
The GPU renderer renders windows (no more CPU fallback): the vignette gets the
window's offset and the whole output's size (VIG_VIEW), the airlight and noise
reduction scale come from the whole frame, and a window grown for spots or
strokes is cut back after the readback. equivalence.rs renders every settings
case as a window plus a corner vignette and a spot reading from outside, within
the existing 1 LSB. Auto Mask strokes join spots in growing the window (they read
the pixel under each dab). An Auto Mask dab's reference pixel is chosen from its
position at f32 precision, like the GPU kernel: an f64 rounding error that
depended on the window's start could put a boundary dab on either pixel.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 4b83f8bb5e
#323: windows cross the Web Worker boundary
WireJob carries the request's window (an old job without one has none), and the
browser build asks for window renders like the desktop; windows there are capped
at 3072 px a side to respect wasm memory.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford eebea486d5
#323: windows: spots render in a window; deep-zoom regression tests for the wide stages
Spot removal reads outside the spot and its automatic source depends on the
pixels at hand. A windowed render now fixes automatic sources from a fixed-size
render of the whole frame and grows the window it works on to hold every spot
that touches the requested one and the patch it reads (bounded at 8192 px),
cutting the result back to the request, so photos with spots get sharp windows
too. The wide stages (dehaze, clarity, highlights/shadows) are checked at depth
on hard-edged blocks: the cut kernels cost at most a few levels, so they are not
computed from a whole-frame proxy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford dc5bff6947
#323: 100 % zoom is the photo's own pixels as shown (after crop and rotation)
fit_rect measured Percent zoom against the uncropped, unrotated photo, so a
cropped photo at 1:1 showed stretched and a rotated one used the wrong axis. The
loupe, its neighbour prefetch and the compare view now use the frame's
cropped-and-rotated size, which also stops a small crop being rendered above its
own pixels.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford f8f091436a
#323: review fixes: no window renders during drags, current-look tiles only, margin by zoom
An adversarial review found that a slider drag at any zoom started a window
render (the 0.6-scale draft looked stretched), that a tile made for an earlier
look kept being drawn and that the tile table was cleared mid-drag, that the
fixed 256 px margin was far below the wide stages' kernels at depth, that spot
removal reads outside the window, and that the window texture stayed resident
after zooming out. Window renders now wait for the drag to end, a tile is drawn
only while photo, zoom and look match, the margin grows with the zoom (768 px
cap) and the window is capped at 6144 px, photos with spots keep the whole-frame
render, the texture and stages are freed when unneeded, and wasm never asks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 244088498d
#323: windows: noise reduction and mask edge refinement follow the whole frame's size
An adversarial review found local::denoise and Refine Edges scaled with the
window's own size, so their strength changed with pan and zoom. Frame::output_long
gives the whole output's long edge to both; tests on a noisy source cover them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford e0539db3f5
#323: the loupe renders the visible window at the zoom scale
When the whole-frame render is smaller than the zoomed view (a 1:1 or 8:1 view
of a large photo, or a capped Preview size) the loupe also requests a window
render (Slot::Region) of the pixels on screen plus a margin, and draws it over
the whole-frame render at its place of the frame. Stateless, grid-snapped
windows mean small pans reuse the render. Not used in the browser build, while
comparing, hovering a look, soft-proofing or showing an overlay. ui.inspect's
loupe reports the window.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 2079e8e08d
#323: engine region_job and the window geometry for zoomed views
Session::region_job renders one PixelWindow of the loupe's frame from the source
level that frame size needs, with a key of its own and no cache writes.
ui-egui::region decides when a window is needed (the whole-frame render is
stretched) and which one: the visible pixels plus a margin, snapped to a 256 px
grid so small pans reuse the render, bounded in size, clamped into the frame.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford b358022f14
#323: pipeline: render a window of the output (RenderRequest::window)
A zoomed view needs the pixels it shows at the zoom scale, not a whole-frame
texture stretched over them. RenderRequest gains an optional PixelWindow of the
virtual full output that max_w x max_h describes; only the window is
materialized. It is Frame:🪟 the crop rectangle narrows to the window
(flips as drawn), so sampling, masks, spots, red eye and the lens warp need no
change. The post-crop vignette keeps the whole frame's geometry (Frame::view)
and dehaze uses the whole frame's airlight (estimated on a 384 px render), so a
window matches the same pixels of the full render. The GPU renderer declines
windowed requests (CPU for now).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford ca5c212d03
#323: review fixes: prefetch and stand-ins stay at preview level; render sizes move in 64 px steps
An adversarial review found that with Automatic the neighbour prefetch asked for
the full source level, whose single cache slot then evicted the open photo's
source, and that hover / Before renders and every window-resize pixel produced
full-size renders. Prefetch and stand-ins are now capped at the preview source
level and the loupe size is rounded up to a 64 px step.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
Nathaniel Ledford 14570cfdf3
#323: Detail view renders at the size it is shown, not at a hidden 2560 px cap
The loupe capped every render at the "Preview size" setting (default 2560 px),
then stretched the texture over the on-screen rect, so 1:1 and Retina fit views
of larger photos looked soft or pixelated.

The setting is now a limit: Automatic (default) renders at the drawn size, up to
the photo's own pixels and an 8192 px ceiling; 1600–5120 px still cap it. The
domain rule is AppSettings::loupe_edge (NaN/zero/huge inputs are clamped). The
saved key changes from previewEdge to previewLimit so existing installs, whose
2560 was the old default, move to Automatic. Build Standard-Sized Previews keeps
a numeric edge via standard_preview_edge().

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 18:28:45 -04:00
BFlat c395789026
Merge pull request #398 from storytold/claude/integrate-batch-8
Integrate #391 and #393 (undecodable raws import as preview only), with a work budget for the JPEG scan
2026-10-09 05:21:21 +07:00
Claude cb0e0cb2e5
Follow-up to #393: the embedded-JPEG scan shares a work budget
scan_for_jpeg walks every JPEG-like start in a container it can't parse;
each walk runs through the marker segments that follow (up to 4096) and
their entropy data. A crafted CRW/MRW/X3F (or unsupported TIFF raw) of
fake starts one after another makes every walk cover most of the file:
quadratic, about a minute for 40 MB, on the import's probe thread. The
walks now report how far they read and share a budget of eight times the
file's size (at least 1 MiB); real files' candidates fail within a few
bytes or are the preview itself. Test: 200,000 fake starts, 0.15 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 22:03:26 +00:00
Claude 0fb3351c91
Merge PR #393 2026-10-08 22:00:34 +00:00
Claude b9254242a2
Merge PR #391 2026-10-08 22:00:10 +00:00
BFlat ab9e8d372c
Merge pull request #394 from storytold/claude/integrate-batch-7
Integrate part of #374 (bounded folder walks, launch imports off the UI thread) and #385, #386, #387
2026-10-09 04:43:24 +07:00
Claude b11e5399c6
Merge PR #387 2026-10-08 21:26:34 +00:00
Claude 83058f717e
Merge PR #386 2026-10-08 21:26:34 +00:00
Claude 7962b66a06
Merge PR #385 2026-10-08 21:26:33 +00:00
Claude c140f63372
Merge branch 'fix-374-startup-drive-walk' into integ-b7 2026-10-08 21:19:42 +00:00
Claude 790a341776
Part of #374: launch imports run off the UI thread; folder walks are bounded and never take a drive root or home implicitly
Root cause (in 0.2.1 and still on main): files and folders named on the
command line were imported from the eframe app-creator closure with
`app.run("library.import", …)`, which runs the engine command synchronously:
`import::expand` walked the folder recursively (one `is_dir()` open per entry,
no depth or count bound, following links) and probed every file before the
first frame could be drawn. An argument that resolves to a drive root (`.` or
`..` with a launcher's working folder at `C:\`, `/` or `\` on Windows) or to
the home folder therefore walked the whole drive with the window stuck as a
black "not responding" box. The library-problem banner's pending import had
the same synchronous call. Every other recursive walk (Local's Include
Subfolders, Find Missing Photos, preset and LUT folders, MCP) was also
unbounded, which is the "Local folders with subfolders: not responding"
pattern of filmcraft#23, and a link loop recursed until the stack overflowed.

- engine `walk::files_in`: one bounded, iterative walker (32 folder levels,
  1,000,000 entries; `Walked::truncated` says it stopped), folder or file
  taken from the directory listing (no open per entry), links and junctions
  to a folder followed once. `import::expand` (+ `expand_within`), preset and
  LUT folder expansion and MCP `expand_paths` use it.
- engine `import::launch_path`: an empty launch argument is ignored; a
  relative one is resolved against the working folder; a drive or file-system
  root becomes its memory card's DCIM folder or is refused, and so is the home
  folder (a notice says why and to use File > Import Photos).
- desktop: launch arguments go through it and are imported with
  `import::start_paths` (the import worker), like dropped files; so does the
  library-problem banner's pending import.

Not verified on Windows (no machine here). The report says PhotoCraft 0.5.0
and `--memory` hang the same way with no arguments; nothing in LightCraft
walks the disk at startup without arguments (strace of a headless demo run
with HOME=/ and cwd /: no recursive listing), so that part may be outside
this code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 21:19:42 +00:00
BFlat 67b7bc8bc6
Merge pull request #389 from storytold/claude/integrate-batch-6
Integrate #373 (keymap editor), #218 (Lightroom catalog import), #367 (parallel import copies), #344 (import file types)
2026-10-09 04:19:01 +07:00
Claude 41e67e1edd
Batch fix: the CLI snapshot test settles before its screenshots
Under load (the full workspace suite) the photo grid was captured before
its thumbnails rendered, darker than the export dialog dimmed over them,
and the dimming comparison failed; it passes alone. Each screenshot now
follows ui.settle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 20:56:35 +00:00
Claude 5d437489b9
Merge branch 'fix-344-import-file-types' into integ-b6 2026-10-08 20:53:11 +00:00
Claude 1e6f0e28e1
Batch fix: the Lightroom catalog dialog filter goes through add_filter_nocase
#218's catalog picker called rfd's add_filter, which #362's guard test
(no_dialog_bypasses_the_helper) forbids: on Linux the filter would hide
CATALOG.LRCAT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 20:53:11 +00:00
Claude 4af709995f
Fix #344: the import review checks or unchecks a file type at once
With several file types among the candidates, the Import Photos review
shows a toggle per type (ARW · 120, JPG · 120): unchecking JPG leaves
the JPEGs kept beside the raws out of the import, checking a type checks
its importable files (duplicates stay unchecked). JPEG/JPG and TIFF/TIF
are one type; a file whose probe named no format goes by its extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 20:52:35 +00:00
Claude 9b0d0fd05f
Parity summary regenerated (cargo xtask parity --write)
Main's summary block had a duplicated pair of total lines left from an
earlier merge; the regenerated one counts #373 and #218's rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 20:50:04 +00:00
Claude b1d8ba4284
Merge branch 'fix-367-import-parallel' into integ-b6 2026-10-08 20:48:31 +00:00
Claude d8efa6a147
Spanish for the keymap editor (#373) and the Lightroom catalog import (#218)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 20:48:31 +00:00
Claude d4c0a7a3d4
Merge remote-tracking branch 'origin/main' into integ-b6
# Conflicts:
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant.json
2026-10-08 20:48:15 +00:00
BFlat 68e0827633
Merge pull request #383 from storytold/claude/test-376-masking-layout
Part of #372: painted-text layout tests from #376
2026-10-09 03:47:22 +07:00
Claude cad024fbff
Part of #372: painted-text layout tests from #376
The tests from @QuirkyTurtle94's #376, a duplicate fix for #372 opened
while #379 was in review: they read the painted text shapes (not the
registered rects) at 1x and 2x scale, in English, German and Brazilian
Portuguese, at panel widths 250-520, and check that tile labels fit
unelided inside their tiles, that a long mask name is elided before the
eye, and that a long Describe prompt keeps Select inside the panel.
They pass unchanged against main's implementation.

Co-Authored-By: Elliot Graham <elliotgraham94@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 20:47:07 +00:00
Claude 86deb5b004
Fix #367: import copies and verifies files on several threads
Import -> Copy (and Move, and Copy as DNG) handled one file after another
on the import worker: copy, fsync, read back and hash, optionally decode
and re-encode the raw as DNG. Every other CPU-heavy step of an import was
already parallel (the probes, up to 8 threads), so with the probes cached
by the review dialog's scan the transfer phase ran on one core.

ImportJob::prepare_files now plans each file in order, as before
(duplicates by path and content, sidecars, rename sequence numbers,
folders, Kept notes) and collects the copies/moves, which then run on up
to 8 threads (std:🧵:scope; sequential on wasm32). Outcomes keep
their places, so results, order, names and failure messages are those of
a one-by-one import:
- a file whose content matches a transfer still pending, or whose name
  could collide with one (same folder, same name family: IMG.jpg,
  IMG-1.jpg, img.dng), waits until that one has finished;
- cancel stops before the next file; transfers not started leave no item;
- a panic in one transfer fails that file instead of the batch;
- Copy as DNG holds a memory-gate permit (6x the file size) while it runs.
Copy verification and crash safety are unchanged (same copy_new / place).
The desktop's import worker prepares batches of 2 x workers files (8..16,
lightcraft_engine::import::batch_size) instead of 8, so a batch's slowest
file holds back fewer threads; the browser build keeps 8.

Measured (4-core shared VM, 200 procedural 3.7 MB JPEGs = 754 MB, warm
page cache, throwaway benchmark not committed):
- probe: ~80 ms CPU per file is the JPEG decode (DCT-scaled to 64 px),
  1.2 ms the content hash; the dialog's scan: 4.4 s, 3.6 cores busy.
- Copy, probes cached: copy+fsync+verify 1.59 s at 0.43 cores before,
  0.94 s at 0.81 cores after. Copy, not cached: 6.45 s -> 5.51 s.
- Add, probes cached: 2 ms prepare; nothing to parallelize.
- commit of a batch on the UI thread (journal append + fsync): ~4 ms,
  flat for libraries of 0, 5k, 20k and 50k photos (~0.7 s for 1300).
Copy as DNG could not be measured (no raw corpus here); it is the most
CPU-heavy per-file step and was sequential.

Test: copies_run_side_by_side_with_the_same_outcome (copies under the
test fault Slow run on more than one thread; names, order and in-batch
duplicates match a one-by-one copy; the outcome assertions also pass on
the old code, the thread assertion fails there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 20:47:04 +00:00
BFlat a673b2bb7a
Merge pull request #382 from storytold/claude/i18n-371-spanish
Fix #371: Spanish interface
2026-10-09 03:41:48 +07:00
Claude 305ff71474
Part of #371: Spanish for the SAM 3 install dialog (#366)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
(cherry picked from commit 73b22c8524ef17b209ff2fffbb16c248524e2e96)
2026-10-08 20:41:31 +00:00
Claude a0e49629d3
Merge remote-tracking branch 'origin/main' into i18n-371-spanish 2026-10-08 20:41:31 +00:00
BFlat c8f697a128
Merge pull request #381 from storytold/claude/fix-366-sam3-install-help
Fix #366: the SAM 3 dialog without a download location shows how to install by hand
2026-10-09 03:41:25 +07:00