feat(#3996): fall back to an explicit user-prompt filename for one unnamed image - #4027
feat(#3996): fall back to an explicit user-prompt filename for one unnamed image#4027aheritier wants to merge 2 commits into
Conversation
25c9d9a to
c4127a5
Compare
c4127a5 to
9594ce2
Compare
|
👋 This PR has merge conflicts with the base branch. Please rebase or merge the latest base branch and resolve them. I've moved it to draft and added |
9594ce2 to
56fad2f
Compare
56fad2f to
0272095
Compare
0272095 to
ddcbca4
Compare
ddcbca4 to
9e83563
Compare
4968274 to
9e83563
Compare
9e83563 to
c7783d3
Compare
Explicitly image-output-capable Gemini gateway requests now carry a system instruction asking the model to emit one exact "[media-file: relative/path]" line per generated image, echoing an explicitly requested name or choosing a meaningful kebab-case one, and to produce a single image unless variations are requested. handleStream filters those marker lines out of both the live event stream and the persisted assistant text with a chunk-split-safe, strictly bounded line parser, then pairs the extracted paths positionally onto the accumulated media blobs via MediaDelta.RequestedPath. Naming precedence stays marker -> provider display name -> generated-N, and marker paths remain untrusted input: they flow through the existing workspacemedia classification, MIME extension correction, escape elicitation/redirect, and never-overwrite writer unchanged. Extra blobs still materialize under their fallback names; extra markers are stripped but ignored.
…named image A live image-output model can ignore the [media-file:] marker instruction entirely, leaving a prompt like "Generate an image as sunshine.jpg" to land as generated-1.png. Add a deterministic fallback: when a turn returns exactly ONE media blob that marker pairing left unnamed, parse a single unambiguous explicit output filename from the triggering user message. The cue grammar is strict, deliberately not NLP: save (it) as / save to / write to / output to / name it / call it / filename:=, plus bare "as" only inside a narrow imperative output context (generation verb + optionally-articled media noun, e.g. "Generate an image as sunshine.jpg"), and a companion of-phrase form (generation verb + media noun + "of <subject>" + "as <filename>") so "Generate an image of a red panda as assets/red-panda.jpg" extracts the intended name. The of-phrase subject cannot cross quotes, clause punctuation, or CR/LF, and any subject containing "as", "with", or "in" is refused because those prepositions introduce open-ended attribute phrases whose trailing "as" compares — a false reject only costs the generic generated-N name, while a false capture could engage the escape-confirmation policy for a merely referenced file. Bare "called" and unanchored "as" are NOT cues, so comparative references to existing files never extract a name. Candidates are keyed by the filename capture's position: one occurrence matched by both grammars counts once; distinct occurrences stay ambiguous and extract nothing. Names may be quoted, backticked, or unquoted with a known image extension; zero or multiple candidates extract nothing. Precedence stays marker -> user-prompt filename -> provider display name -> generic generated-N, and the extracted path only fills MediaDelta.RequestedPath, so the existing untrusted-path pipeline (MIME/extension correction, collision suffixing, workspace containment and escape confirmation) applies unchanged.
c7783d3 to
b18b517
Compare
aheritier
left a comment
There was a problem hiding this comment.
Reviewed the #4027 delta only (161aaa6f0...b18b517f7, 2 commits, both GPG-signed).
CI @ b18b517: build-and-test, lint, windows-tests, CodeQL, license-check, build-image ×2 all green.
Local validation (git-archive of the head, worktree untouched): go build ./... (darwin + GOOS=windows), go test -count=1 -race on pkg/runtime, pkg/chat, pkg/model/provider/gemini, golangci-lint run → clean. 9 -overlay mutations all killed: dropping the single-blob guard, dropping marker precedence, making bare as a cue, disabling comparativeCueRE, accepting the first of several candidates, marker EOF flush, marker pairing order, whitespace-tolerant marker prefix, non-stripping filter — each fails at least one targeted test.
Checklist
- Extraction bounded/safe: ≤256 bytes, valid UTF-8, no control chars, no edge whitespace, image-extension whitelist, non-empty stem (
generated_media_prompt_filename.go:141-158); applies only to exactly one blob left unnamed by markers (:165-173). ✅ - Marker precedence over prompt name (
:166,TestMarkerOverridesUserPromptFilename). ✅ - Prompt-derived path goes through the SAME untrusted-path gate as markers — it only fills
RequestedPath(:171) →writeGeneratedMedia→ClassifyRequestedPath(media_escape.go:62). Verified:../x.png,..\evil.png,~/evil.png,/tmp/out.png→ escape policy (redirect when non-interactive);CON.png→ "not a usable path" → generic name. ✅ - Collisions: O_EXCL dash-suffix;
ArtifactPath+Document.Namereflect the actually-written path (TestUserPromptOfPhraseFilenameCollisionSuffixPersists). ✅ - No prompt text in logs: no slog in the two new files; the redirect warning names only the final written path (
media_escape.go:126-128). ✅ - This PR is what populates
RequestedPath(base has zero writers; head:generated_media_markers.go:175,generated_media_prompt_filename.go:171) — so #4026's escape-confirmation path and the A2A/ACP elicitation-parking finding become reachable here, via both marker and prompt-derived names. Not re-reported; noting reachability only.
Findings (no blockers)
[optional]generated_media_prompt_filename.go:48-52— unquoted alternative anchors on the first image extension, sosave as archive.png.txt/sunshine.jpg.bakextractarchive.png/sunshine.jpg. Harmless (writer MIME-corrects anyway), but worth a test pin if intentional.[optional]loop.go:1370— prompt source issess.GetLastUserMessageContent(); an assistant message in a later tool-call iteration of the same turn (or any turn without a fresh user message) re-extracts the same filename. Bounded by the single-blob guard + collision suffix (sunshine-1.png), so not a correctness issue.
Ready from my side once the stack below is.
What
Uses an explicit filename from the user prompt when exactly one generated image has no filename, with marker parsing and streaming coverage.
Why
Produces predictable, user-friendly names without guessing when the prompt supplies a clear name.
Validation
task test; filename extraction, marker, Gemini instruction, and streaming tests.Test instructions
Run:
task testRequest exactly one unnamed generated image with an explicit filename in the user prompt. Expected: that filename is used. Repeat with an ambiguous prompt or multiple images; expected: the safe fallback naming behavior is retained.