ItemId wraps a u64 (item_p parses the `item` param with u64_p), so
reading activeSequence with as_i64 would silently return None for
any id above i64::MAX and make document()'s sequence go null instead
of inspecting the valid id project.inspect reported. Switches to
as_u64 and adds a regression test for that boundary (proven to fail
against as_i64 first).
Addresses a CodeRabbit review comment on #2480.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
document() composed project.inspect and sequence.inspect as two
separate round trips to the app (two lock scopes in headless mode,
two TCP round trips in bridge mode). sequence.inspect with no `item`
answers for whichever sequence is active *now*, not the one
project.inspect just reported, so if the active sequence changes in
that window (another client, or the app's own UI, switching tabs)
the pair comes back inconsistent: project.activeSequence names one
sequence while sequence describes another.
Reproduced against a running bridge-mode app: a real MCP client
hammering doc_inspect while an independent connection flipped
sequence.open between two sequences mismatched on ~300/300 calls, so
this is a wide window, not a rare edge case.
Fix: read project.inspect's activeSequence id first, then pass it
explicitly as sequence.inspect's `item`, so the second call always
describes the sequence the first one named, regardless of what
becomes active afterward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Exports and rendered frames now record which offline items (missing,
made offline, unreadable) they drew the placeholder slate for. The engine
reports them as `offlineMedia` in file.exportMedia (wait), jobs.list
export entries and export.queue items; the bridge and MCP long exports
pass it through. filmcraft-cli export and render name each item on
stderr and exit 1, unless --allow-offline accepts the placeholders.
Fixes#1054
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere offers JPEG, Targa and DPX as image-sequence export
formats; FilmCraft only wrote PNG, TIFF and BMP sequences (docs/gaps.md G13).
Fix: three new export formats (ids jpg, tga, dpx), numbered like the other
sequences, with presets, the export UI (JPEG quality slider, Targa alpha)
and es/ja/zh-cn strings. JPEG uses the image crate at the export quality;
Targa (TGA 2.0, uncompressed 24/32-bit) and DPX (SMPTE ST 268, 10-bit RGB
method A, BT.709) are written by a small hostile-input-safe writer in
crates/export/src/raster.rs. Format::is_image_sequence replaces the
hand-kept format lists. OpenEXR stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
remove_partial treated any `<stem><digits>.<ext>` file as a still of an image sequence, so
cancelling an export to `clip.mp4` also deleted a `clip1.mp4` written in the same second (or
during the export). Only PNG/TIFF/BMP sequence exports write numbered stills, and their frame
numbers have at least three digits (`image_sequence_path`); match only those.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- mxf: read_pcm bounds a request by the sample frames the file actually stores (plus 16M
samples of zero padding) instead of a fixed 16,777,216-sample cap, so whole-clip reads of
long stereo MXF (transcription) work again; PCM/AES3 chunk buffers are clamped to the source
length instead of a 256 MiB cap; decode_pcm's output is bounded by its input, so its
fixed cap goes (the AES3 >8-channel refusal stays; a request is also bounded by the source
length divided by the channel count). read_sample keeps only the source-length bound.
- mxf: index_entries no longer fails open() past 1,048,576 entries (the sparse BTreeMap
already bounds memory by real entries); long recordings open.
- project/format: new Sequence/Track/CaptionTrack::check_bounds() hold only the hostile-value
bounds (settings, marks, overflowing time ranges); project load and export ranges use it, so
projects that break only structural rules (overlap, zero duration) still open and export.
check() = check_bounds() + the structural rules, as before.
- project: frame-size cap raised to 268,435,456 pixels, 32768 per side (16K, 16384x8192).
- engine: checked_u32_p accepts integer-valued floats (1920.0); renderable_sequence returns
an EngineError (new Session::try_render_program_at, used by automation render) instead of
eprintln.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
file.exportMedia with wait: true was forwarded to the app unchanged. The app then encoded on its UI
thread: it showed no progress and answered no other control request until the encode ended, and the
control server's 60 s reply limit reported "timeout" for any longer export while it went on and
succeeded.
BridgeClient::execute now starts the export without wait and polls jobs.list until the job
finishes, returning the same {job, path, result} (or the export's error). filmcraft-cli --bridge
exec and MCP bridge mode both use it, and MCP command_run in bridge mode now gets the same progress
notifications and cancellation as headless mode.
Tested against a fake app (wait: true never reaches it, one export started, result and error
returned) and end to end against the desktop app: a blocking demo export returned its 642-frame
result with exit 0 while ui.inspect was answered in ~0.05 s throughout.
Fixes#91Fixes#92
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both tools are annotated readOnlyHint, but a call with `seconds` moved the active sequence's
playhead (and, with Selection Follows Playhead on, the selection) to that time, in headless and
bridge mode alike.
- headless: Session::render_program_at renders the frame at `seconds` (frame-snapped, as the
playhead would be) without touching the session.
- bridge: the Program monitor screenshot needs the live playhead there, so the tool records the
playhead and selection, takes the screenshot and puts both back (the selection only if it changed,
since selecting also leaves trim mode).
Regression test: rendering at 5 s and 8 s with Selection Follows Playhead on leaves playhead and
selection unchanged and still renders the requested frame (it moved the playhead to 8 s before).
Fixes#114
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A short export can finish inside one 100 ms poll, which left a single progress
notification and no time to answer the ping. The test now tries longer exports
until one outlasts a few polls, and cancels a longer one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
remove_partial removed every '<stem>[digits].<any ext>' file in the output folder modified since the export started, so cancelling an export to trailer.mp4 also deleted trailer.aep or trailer2.psd if they were saved during the export. It now removes only the output file, numbered stills with the output's extension, and the .srt/.vtt caption sidecar. Regression test with decoy files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
command_run file.exportMedia with wait: true (headless) runs as the
background export job and uses the MCP progress and cancellation utilities:
- with _meta.progressToken, notifications/progress reports frames done /
total with the job status, at most every 100 ms (none without a token);
- notifications/cancelled stops the job at the next batch (jobs.cancel),
deletes the partial output and sends no response;
- the session lock is only held for short polls, so ping, doc_inspect,
jobs.list etc. are answered while it encodes.
Without wait, and in bridge mode, nothing changes. Test
export_progress_and_cancel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- resources/list and resources/read serve the project (tree + active
sequence, as doc_inspect) and the command catalog (as command_list) as
JSON, for clients that attach context from resources.
- Results are built with rmcp's types, so resultType follows the negotiated
revision; for 2026-07-28 clients (current Claude Code) they also carry
ttlMs and cacheScope, like the generated tools/list. The resource list is
fixed (10 min), document reads are never cached (0).
- get_info now declares the resources capability; the instructions text
moves to a const, unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Every tool has a title and readOnly / destructive / idempotent / openWorld
hints, set in its #[tool] attribute, so clients can run the read-only
tools without asking and ask before edits.
- New tools: command_batch (several commands, stop at the first error by
default), doc_inspect (project tree + active sequence in one call) and
render_preview (= render_frame).
- An unknown tool argument is a -32602 error naming it and the accepted
ones, instead of being ignored.
- A line that is not JSON gets a -32700 reply (id null) instead of being
dropped without an answer: serve_io filters lines in front of rmcp.
- A panicking command no longer poisons the session lock for the rest of the
session ("session lock poisoned"); it is reported as "internal error: …"
(AGENTS.md §0.6).
- docs/agents.md § Conventions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- interchange: exporters (FCP7 XML, FCPXML, OTIO) skip a missing nested sequence or media with a
report warning instead of `expect`/`unwrap`; EDL event grouping and M2 lines, the XML writer
and the AAF/OMF dispatch arms have no panics. The WAVE header saturates instead of overflowing
for >4 GiB descriptors (found by fuzzing: a mutated EDL exported to OMF), regression test
`wav::tests::huge_wav_header_saturates`.
- captions: EBU STL frame rate read with `get()`; MCC rows, TTML text and the STL dispatch arm
without `expect`/`unreachable!`.
- project: bin moves, 5.1 pan lanes and text/shape layer constructors without `expect`.
- text: font resolution and line layout without `expect` (the bundled-face lookup keeps one
documented `#[allow(clippy::expect_used)]`: the faces are compiled in).
- automation: the MCP bridge returns an error instead of `expect` when not connected.
- Each crate denies unwrap/expect/panic/unimplemented/todo/unreachable outside tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>