Skip to content

fix(feedback): optimize oversized screenshots without losing detail - #23664

Merged
nwparker merged 24 commits into
stablyai:mainfrom
AmoabaKelvin:shrink-oversized-feedback-screenshots
Oct 6, 2026
Merged

nwparker merged 24 commits into
stablyai:mainfrom
AmoabaKelvin:shrink-oversized-feedback-screenshots

Conversation

@AmoabaKelvin

@AmoabaKelvin AmoabaKelvin commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​279 $\color{#cf222e}{\Huge{\mathbf{−}}}$​25 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​254
Prod 9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​181 $\color{#cf222e}{\Huge{\mathbf{−}}}$​53 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​128

ELI5

Feedback screenshots must fit the existing 4 MB attachment budget. Orca now tries a full-resolution lossless PNG re-encode when an image is too large, and attaches it only if that fits. It never downsizes the screenshot or adds JPEG compression. Images that still cannot fit receive the existing size warning. The original file is unchanged.

What Changed

Before: oversized screenshots were refused. This branch's earlier compression ladder could fit more images by reducing resolution or using JPEG, which could make small text harder to read.

After: an oversized PNG or JPEG gets one full-resolution PNG encode. If the result fits its remaining capped budget, it appears as an attachment and a toast reports the size reduction. If it cannot fit, the image is refused rather than losing detail. Images already within the budget are attached byte-for-byte.

  • The existing 4 MB total limit and four-image limit remain. Each optimized image receives at most half the total budget, leaving room for another image.
  • GIF, WebP and animated PNG are never flattened. The existing 32 MB source cap, dimension checks and minimum remaining budget guard bound work before decoding.
  • Image batches read one at a time against committed attachments. Pending reservations conservatively cover every file that could attach, including a GIF that fits only after an earlier optimization frees space. Refused files and removed drafts cannot make a queued batch under-reserve.
  • Mixed text plus an oversized image keeps the text in the textarea while preparation proceeds. The pending-batch regression also preserves text, caret position and subsequent typing when the image later has no room.
  • The attachment hint shows “Preparing attachments…” after 250 ms; Send waits for pending reads. Bitmaps and canvas backing stores are released, and a thrown settle callback cannot poison the queue.
  • Main-process limits, server submission and the existing text-only fallback are unchanged.

Why

Feedback screenshots often contain small text. Full-resolution PNG preserves displayed detail without introducing JPEG artifacts. A single encode bounds the work more tightly than the old multi-step ladder. This deliberately leaves some oversized photographs or already efficiently encoded PNGs refused, matching the previous main behavior instead of attaching a less readable image.

Preparation stays in the dialog so the user sees the result or warning before sending. Browser clients use the same path. Conservative pending reservations can leave co-pasted text in the textarea even when an earlier batch later frees space; that is preferable to silently discarding the report.

Linked Issue

Related to #22776: automatic lossless optimization only; resolution reduction and lossy JPEG fallback are intentionally excluded.

Visual Proof

Before:
before

After:
after

The existing proof shows the attachment/preparation flow; the final compression policy now uses full-resolution PNG only.

Testing

  • 84 tests passed across image optimization, file validation/budgeting, read queue, drop handling and the feedback dialog.
  • Restoring the previous reservation logic makes three new regressions fail, including a real userEvent paste that loses report text. Restoring the previous compression ladder makes the two PNG/JPEG full-resolution policy tests fail.
  • Official web typecheck, changed-code quality gate and the workflow's standalone React Doctor check passed.
  • Tests run with ORCA_BACKGROUND_LAUNCH=1. No visible Electron app, native clipboard, live feedback submission or Linux/Windows manual validation was run for this repair. Fresh CI must pass on the final head before merge.

Review

The earlier Medium risk caused by downscaling/JPEG detail loss is removed by the full-resolution lossless-only policy. Both open bot findings have concrete code fixes and regression coverage. Remaining size refusals and animated-image restrictions match baseline safeguards; pending reservations err toward preserving report text.

Agent skill upstream boundary

  • Not applicable; no upstream skill-installer content copied.

Checklist

  • User-facing before/after, mechanism and rationale described
  • Focused correctness, resource cleanup and paste regressions tested
  • Existing cross-platform browser path and attachment safeguards preserved
  • Typecheck and changed quality/React Doctor checks passed
  • Fresh required CI passes on the final committed head before merge

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 580571aa-0105-4bf2-93c3-affacb33a141

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed3b1e8d78d3c4f2f18db295b802de1bc3182a1 and 3bbd62b2cad4a15d2c7f4477e8ac5f6a5381f557.

📒 Files selected for processing (2)
  • src/renderer/src/lib/feedback-image-attachments.test.ts
  • src/renderer/src/lib/feedback-image-attachments.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

Feedback image attachment processing now shrinks eligible oversized images and returns compression notices with images and errors. The shrink process tries PNG and JPEG encodings in a defined order. Sidebar image batches are read sequentially and use the committed image count and byte total when each read starts.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 3bbd6

Oversized screenshots are now shrunk to fit the attachment limit, and the user is told when this happens. Animated GIF, WebP and APNG images are still refused rather than flattened. No merge-blocking risk was identified.

Architecture Summary

Architecture risk: 🟡 Medium · up to 3bbd6

The change affects 1 system.

Changed systems: src

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — src (service) was modified; 8 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx: The image-reader fixture adds an empty notices array alongside errors.
  • observed — Modified behavior in src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx: The paste-rejection test changes its oversized file from PNG to GIF and updates the expected error accordingly. Its mocked result now includes notices, and the reader-call assertion waits for the asynchronous call.
  • observed — Modified behavior in src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx: The queued-batch test now returns fixtures with notices and models the second committed image as 150 bytes; the old test only returned the first image and left later reads pending.
  • observed — Modified behavior in src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx: The test now waits for all three reader calls and expects the third batch’s byte budget to be 1,150, combining the first image’s 1,000 bytes with the second image’s 150, instead of expecting 1,200.

Reliability and maintainability

  • inferred — Risk-relevant change factors for src: blast_radius_1; direct_dependents_1
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue [#22776] requires recompression at the main-process submission boundary before postFeedback. The current implementation performs recompression in renderer-side readFeedbackImageFiles. The im… Move recompression to the main process before postFeedback, or update issue [#22776] to accept renderer-side recompression. Preserve the current notices, limits, image-type handling, fallback behavior, and tests.
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The renderer attachment logic, shrink helper, notices, translations, paste handling, batch accounting, and tests directly implement or support issue [#22776]. No unrelated product behavior is shown.
Title check ✅ Passed The title clearly summarizes the main change: optimizing oversized feedback screenshots while preserving image detail.
Description check ✅ Passed The description covers the user impact, implementation, rationale, linked issue, visual proof, testing, review, and checklist. It also states which checks and manual tests were not completed. Some tem…
Full details: Linked Issues check

Explanation

Issue [#22776] requires recompression at the main-process submission boundary before postFeedback. The current implementation performs recompression in renderer-side readFeedbackImageFiles. The implementation satisfies the other stated coding requirements: it preserves fitting images, tries PNG before JPEG, reports compression, enforces the 4 MiB and safety limits, retains GIF/WebP refusals, handles paste and batch accounting, and adds automated tests. The optional report-text budget change is not required.


Comment @coderabbitai help to get the list of available commands.

@AmoabaKelvin
AmoabaKelvin force-pushed the shrink-oversized-feedback-screenshots branch from 22c4e73 to 0a75606 Compare September 28, 2026 19:11

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one rough edge inline.

Reviewed changes

  • Attach-time shrink pipeline — new feedback-image-shrink.ts decodes once (createImageBitmap) and re-encodes down a PNG-then-JPEG ladder, returning the first blob that fits the space left.
  • Budget accounting — readFeedbackImageFiles admits an oversized file only when there is a plausible shrink target, subtracts the shrunk size from the budget, and returns compression notices alongside errors.
  • Serialized read queue — use-sidebar-feedback-images.ts now reads batches one at a time against committed drafts, so a later add is measured against the earlier batch's shrunk size rather than its raw file size; notices surface via toast.info.
  • Gates & copy — canAttachWithin lets the paste and attach gates admit shrinkable images; en.json gains the compressed string; four test suites updated/added.

I verified the main-process handoff is safe (shrunk contentType can only be image/png/image/jpeg, both accepted; bytes/data derive from the same Blob and main validates data.byteLength), and that en-runtime-required.json does not need the new key because its call-site default matches en.json.

ℹ️ Animation guard excludes only GIF, but image/webp is an accepted animation-capable type

canAttachWithin protects animation only by rejecting image/gif, yet image/webp is in SUPPORTED_FEEDBACK_IMAGE_TYPES and can be animated. An oversized animated WebP therefore takes the shrink branch, where createImageBitmap yields a single frame, so the animation is silently flattened — the exact "degrade an attachment without telling the user" outcome this PR exists to prevent. The compression notice only reports a size change.

Technical details
# Animated WebP is silently flattened by the shrink path

## Affected sites
- `src/renderer/src/lib/feedback-image-attachments.ts:62` — `file.type !== 'image/gif'` is the only animation guard; `image/webp` passes.
- `src/renderer/src/lib/feedback-image-shrink.ts:56` — `createImageBitmap(image)` is spec'd to decode only the default/first frame of an animated image.
- `src/renderer/src/lib/feedback-image-attachments.test.ts:198` — the only animation test is GIF-specific; there is no WebP case.

## Required outcome
A file whose animation would be lost must not be silently re-encoded: exclude animation-capable accepted types (`image/webp`, and ideally animated PNG) from shrink, or refuse them with the existing error path as GIF already does.

## Suggested approach
Extend the guard to `image/webp` and detect APNG (or simply refuse `image/png` files that sniff as APNG) before shrinking, mirroring the GIF rationale. `createImageBitmap` frame semantics: https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#dom-createimagebitmap; animated WebP support: https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ New commit is clean — no new issues; the earlier animated-WebP suggestion remains open.

Reviewed changes

Reviewed the single commit added since the prior pullfrog review, against a rebase of the original head.

  • Lazy read-queue seed — readQueueRef now initializes as useRef<Promise<void>>(undefined!) and seeds with readQueueRef.current ??= Promise.resolve(). This satisfies the renderer useRef lazy-initialization ratchet (src/renderer/src/lazy-use-ref-ratchet.test.ts rejects useRef(Promise.resolve()) because the argument runs on every render) and is behaviour-neutral: current is always seeded before handleAddFiles can run, and the ??= guard keeps the same promise for the component's life. The undefined! marker is explicitly allow-listed by that ratchet.

Nothing else changed in the delta. The previously flagged animation guard (only image/gif excluded, image/webp still shrinkable) is unaffected by this commit and remains a minor, non-blocking suggestion.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@AmoabaKelvin
AmoabaKelvin force-pushed the shrink-oversized-feedback-screenshots branch 2 times, most recently from 1911db6 to 5761297 Compare September 28, 2026 19:39

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the net delta since the prior pullfrog review (0a75606), centered on the new animation guard, against the full PR.

  • Byte-level animation detection — hasAnimationFrames reads the VP8X animation flag (0x02) for WebP and walks PNG chunks for acTL before the first IDAT, so an oversized animated WebP or APNG is refused with the size error instead of being flattened to one frame by createImageBitmap.
  • Guard placement — the check runs in readFeedbackImageFiles only for files larger than the space left, after the dimension check and before shrinking; the by-type GIF guard still short-circuits before any read.
  • Coverage — new unit tests exercise the chunk/flag sniffing (APNG, animated and static WebP, truncated PNG) and the refusal path; the existing animated GIF/WebP read test now also asserts shrinkFeedbackImage is never called.

I verified the VP8X animation bit against the WebP RIFF container spec (|Rsv|I|L|E|X|A|R|, MSB-first, so animation = 0x02) and the PNG acTL-before-IDAT rule, and ran the four feedback suites locally — 60 tests pass.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor suggestion inline.

Reviewed changes

Reviewed the commit that superseded 1911db6 on this branch (the branch moved while the previous review was posting).

  • Swapped byte sniffing for a type guard — the read-time hasAnimationFrames check is gone; canAttachWithin now refuses image/webp by type alongside image/gif, so an oversized WebP never reaches shrinkFeedbackImage.
  • Removed the sniffing helpers — hasAnimationFrames/hasAscii and their unit tests are deleted; the GIF and WebP refusal cases are folded into one read-path test.
  • Net effect — oversized WebP (static or animated) is refused outright; only PNG and JPEG are still shrunk.

The animation guard now matches the PR description's stated rule for GIF and WebP. The one gap left is APNG, noted inline. All four feedback suites pass locally (56 tests).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@AmoabaKelvin
AmoabaKelvin force-pushed the shrink-oversized-feedback-screenshots branch from 5761297 to 5ed3b1e Compare September 28, 2026 22:57
@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Changes how feedback screenshots are processed and attached.

The PR appears safe to merge, with a non-blocking screenshot-clarity concern still open.

Findings

  1. P2 JPEG screenshots lose clarity ▶

Summary

The PR shrinks oversized feedback screenshots before attachment, reserves capacity for queued images, and shows a delayed preparation hint.

  • Rechecks found no new actionable issue in the changes since the previous review.
  • The earlier JPEG-clarity concern remains outstanding; the other previous threads are resolved.

Reviews (7) · Last reviewed commit: "merge: origin/main into shrink-oversized..."

Comment thread src/renderer/src/i18n/locales/en.json Resolved
@AmoabaKelvin
AmoabaKelvin force-pushed the shrink-oversized-feedback-screenshots branch from 5ed3b1e to 3bbd62b Compare September 28, 2026 23:10

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor suggestion inline.

Reviewed changes

Reviewed the delta since the prior pullfrog review (5761297): the acTL APNG refusal.

  • Refused oversized animated PNG — isAnimatedPng sniffs the acTL chunk in the first 64 KB and routes an oversized image/png to the existing size error instead of the shrink path, matching the by-type GIF/WebP rule; a read-path test and an updated paste test come with it.

The APNG gap raised in the prior review is closed and its thread resolved. The remaining note is that the new refusal is not mirrored in the paste gate, which cannot sniff bytes synchronously.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@AmoabaKelvin
AmoabaKelvin force-pushed the shrink-oversized-feedback-screenshots branch 2 times, most recently from 66b2dda to 49e2581 Compare September 29, 2026 17:00

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the net delta since the prior pullfrog review (3bbd62b): the localized compression notice plus the narrowed paste-gate contract.

  • Added compressed translations — Spanish, French, Japanese, Korean, and Simplified Chinese now carry the compression notice, each preserving all three placeholders ({{fileName}}, {{originalSize}}, {{size}}). pnpm run verify:localization-catalogs passes (exit 0).
  • Narrowed the hasAttachableFeedbackImage contract — the doc comment now scopes the mirroring invariant to the type/size limits the gate can evaluate and states that byte-dependent checks (dimensions, APNG, whether a shrink fits) cannot run synchronously, resolving the last inline thread.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the three commits added since the prior pullfrog review (49e2581): the shrink-ladder split, the per-step canvas release, the read-queue settle hardening, and the pending-attachment hint.

  • Split the shrink ladder by source type — feedbackImageShrinkSteps(sourceType) returns the JPEG-only ladder for an image/jpeg source (skipping both lossless PNG encodes, since a full-size PNG of decoded JPEG noise runs larger than the source it must undercut), while PNG still tries PNG sizes before the lossy ones.
  • Released each shrink step's canvas — encodeBitmap is now async and zeroes canvas.width/height in a finally after toBlob has answered, so full-size canvases no longer accumulate against the renderer's canvas memory budget (which hands back a blank canvas, silently uploading an empty screenshot).
  • Hardened the serialized read queue — the queue tail is now settled.catch(...), so a throw inside either settle callback can no longer leave the tail rejected and make every later attach report "Could not read the attached images".
  • Added a "Preparing attachments…" hint — the attachment box shows it while pendingImageReadCount > 0, so a slow shrink does not read as a dropped file; preparing was added to every locale (no placeholders, so catalog parity holds).
  • Tests — new coverage for the PNG/JPEG step split, batch sizing against the shrunk size, the compression notice, and the pending hint.

The JPEG-source shortcut is a deliberate resolution/time trade-off documented in feedback-image-shrink.ts and the PR body (greptile's open P2 raises the clarity side of it). Feedback suites (60 tests), oxlint on the changed sources, and verify:localization-catalogs all pass locally at this head.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor

Thanks, Kelvin. Shrinking at attach time in the dialog is the right call, and the reasoning against doing it in main is solid.

We tested a real build with several full-screen Retina screenshots and pushed some changes on top (plus a merge of the latest main, no conflicts):

  • Budget per screenshot. Each shrunk screenshot is now capped at half the 4 MB (2 MB). Before, the first one took about 3.8 MB and the second came out as a ~110 KB, 756 px JPEG you couldn't read. Now three full-screen screenshots attach at 1.7 MB / 1.5 MB / 0.75 MB, and all are readable. Screenshots that already fit are still sent untouched.
  • Refusal wording. It now says "would bring the attachments over 4.0 MB in total" when the space is used up, instead of "is larger than 4.0 MB".
  • Corrupt files. An undecodable image is reported as not a valid image, not as too large.
  • Animated PNGs. Detected by reading the PNG's chunks instead of searching the first 64 KB for text, which could miss real animations and refuse ordinary screenshots.
  • Second paste. A second screenshot pasted while the first is still shrinking is now recognised as an attachment.
  • Hint and closed dialog. "Preparing attachments…" only appears if a read takes longer than 250 ms. Queued work stops if the dialog closes.
  • Tests. They now cover the real decode, downscale and release path.

Could you update the description to match? Specifically:

  • "Two full-screen Retina screenshots now fit together" → each shrunk screenshot is capped at 2 MB, so three full-screen Retina screenshots fit and stay readable.
  • Replace the "a second one is squeezed harder than the first" trade-off paragraph with the per-screenshot cap.
  • Drop "the browser (web) version runs the same code": the web client can't submit feedback today (it has no feedback API).
  • Drop the "toBlob is asynchronous… measured 50 ms" claim, or restate it as measured: no long tasks; 0.3 s for a 6 MP shrink, 0.85 s for 32 MP.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new paste-gate reservation can under-count a mixed batch, so a paste is swallowed and its text dropped even though the reader then refuses the image. Details inline.

Reviewed changes

Reviewed the 14 commits added since the prior pullfrog review (22d2722) — the half-budget shrink cap and its accounting, the paste-gate reservation, the chunk-walk APNG sniff, the delayed preparing hint, the unmount skip, and supporting tests/docs.

  • Capped every shrunk screenshot at half the total budget — MAX_FEEDBACK_IMAGE_SHRINK_TARGET_BYTES = TOTAL/2; an image that already fits is still never re-encoded, so the first re-encode leaves room for a second.
  • Reserved a shrinking batch's capped size for the paste gate — maxFeedbackImageBatchBytes replaces the hook's raw file-size sum so a screenshot still shrinking no longer turns the next paste away.
  • Blamed the shared budget vs. the file size — addSizeError(fitsWholeBudget) now picks "would bring the attachments over 4.0 MB in total" only when the whole budget could not have taken the file.
  • Walked PNG chunks for APNG — isAnimatedPng follows chunk headers, so acTL past a large metadata chunk is found and bytes that merely spell acTL are not misread; a corrupt length ends the walk.
  • Reported an undecodable oversized image as invalid — a shrinkFeedbackImage rejection now yields "is not a valid supported image" instead of a size message.
  • Delayed the preparing hint — SidebarFeedbackImageAttachments uses useDelayedStatus with a 250 ms show delay so an image that reads at once does not flash it; queued reads are skipped once the dialog unmounts.
  • Tests/docs — coverage of the real decode/downscale/release path, the cap, the APNG walk, and the paste race; corrected toBlob/canvas comments.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ New commit is clean — no new issues. The earlier maxFeedbackImageBatchBytes finding remains open and untouched by this commit.

Reviewed changes

Reviewed the single commit added since the prior pullfrog review (d202596): a revert of e2272b825e.

  • Reverted the direct read-queue seed — readQueueRef is back to the lazy form (useRef<Promise<void>>(undefined!) plus readQueueRef.current ??= Promise.resolve()). The direct useRef(Promise.resolve()) form evaluates its argument on every render and trips the renderer useRef lazy-initialization ratchet (src/renderer/src/lazy-use-ref-ratchet.test.ts). Behavior is unchanged: current is seeded before any handler can run, and ??= keeps the same promise for the component's life.

I ran lazy-use-ref-ratchet.test.ts, use-sidebar-feedback-images.test.tsx, and the three feedback suites (73 tests) at this head — all pass. The one open pullfrog thread (maxFeedbackImageBatchBytes under-reserving a mixed batch) anchors to feedback-image-attachments.ts, which this revert does not touch, so it stays open.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@nwparker
nwparker force-pushed the shrink-oversized-feedback-screenshots branch from 72304c5 to 5a33db9 Compare October 5, 2026 05:46

@nwparker nwparker left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repaired removed-hook integration, silent loss of text co-pasted with oversized images and render-time queue-ref mutation found by fresh CI. 80 tests, web typecheck, changed-quality and standalone React Doctor pass; paste preservation tests verified red/green. Remaining tradeoff: resizing/JPEG can reduce screenshot detail, disclosed in UI.

Required PR Checks verify passed on this exact commit: 72a1761bf4f98e3b68ae9c44ee739f890ffb663c.

@nwparker nwparker changed the title fix(feedback): shrink oversized screenshots instead of refusing them fix(feedback): optimize oversized screenshots without losing detail Oct 5, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the net delta since the prior pullfrog review (72304c5): the mixed-paste gate option and read-queue seed (72a1761), then the full-resolution lossless-only rewrite and the independent batch reservation (b47911c).

  • Replaced the downscale/JPEG ladder with a single full-resolution PNG encode — feedback-image-shrink.ts draws the decoded bitmap at native size and encodes once as image/png; an oversized image that cannot fit is refused with the existing size message, and an image already within budget stays byte-for-byte. GIF/WebP stay refused by type, APNG by the acTL chunk walk.
  • Made the paste-gate reservation an upper bound — maxFeedbackImageBatchBytes now sums independent per-file bounds (file size when it fits, else the capped shrink target) and stops at the total budget, so a shrink's freed slack, a rejected file, or a removed draft can no longer make a queued batch under-reserve and swallow co-pasted text. The old retina.png + anim.gif counterexample now reserves the full 4 MiB and is pinned by a regression.
  • Preserved mixed-paste text — the gate only consumes a clipboard carrying text when an image fits as-is (allowShrinking: clipboardData.getData('text/plain').length === 0), while handleAddFiles still runs; the read queue also skips queued batches after unmount.
  • Tests — new regressions for the GIF-admitted-by-slack reservation, the budget-capped reservation, the full-resolution PNG policy for both PNG and JPEG sources, and a userEvent paste that preserves text, caret, and subsequent typing when the attached image is refused.

I traced the new reservation as a true upper bound on what the reader can commit (the predictor always uses the largest fitBytes, and each reader commit is <= its predicted per-file bound), re-checked the addSizeError budget-vs-file-cap attribution across gif/APNG/capped-target cases, and ran the four focused feedback suites — 77 tests pass at b47911c. The PRRT_kwDORpCz1s6oPcAl thread is already resolved by the author's reply, and no new issue replaces it.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

AmoabaKelvin and others added 3 commits October 5, 2026 17:18
… a JPEG

Two defects in the attach-time shrink ladder:

- Every step allocated a fresh full-size canvas and left it for the garbage
  collector, so a 32-megapixel source could keep six of them alive at once.
  Past the renderer's canvas memory budget Chromium hands back a blank canvas,
  which would upload as a blank screenshot with nothing to notice it. Each
  step's canvas is now released once toBlob has answered.
- A JPEG source ran two full-size PNG encodes first. A PNG of decoded JPEG
  noise is many times larger than the source it has to undercut, so those
  steps could only ever burn time and a large buffer before the first JPEG
  attempt. A lossy source now starts at the JPEG steps.
…ck throws

The queue tail is what the next batch awaits. A throw in either settle callback
left it rejected, so every later attach short-circuited into "Could not read the
attached images" without reading anything — and the rejection went unhandled.
nwparker and others added 18 commits October 5, 2026 17:18
Shrinking a full-screen capture takes long enough that the gap between picking
the file and its thumbnail appearing reads as a dropped attachment: the only
other signal was the Send button quietly staying disabled. The attachment hint
now reads "Preparing attachments…" while a batch is in flight.
A shrink kept the largest step that fit everything left, so the first
full-screen screenshot took a 3.8 MB PNG, the second fell to a barely
readable JPEG and the third was refused. Re-encodes now target at most
2 MB; an image that already fits is still attached untouched. The
"larger than 4.0 MB" refusal now applies whenever the capped target an
empty budget would give was the one that refused it.
…file size, for the paste gate

A 6 MB screenshot still shrinking reserved 6 MB, more than the whole budget,
so a second paste during the shrink fell through to the textarea even though
it attached. Each pending file now reserves what the reader can commit under
the same fit and shrink-target rules. Also corrects the minimum shrink target
comment to the measured smallest step.
This reverts commit e2272b825eb2d26286aee3a99ca28f3ea984508e.
Optimize oversized attachments only as full-resolution lossless PNG; refuse them with existing warnings when that cannot fit. Bound each pending batch independently so earlier optimization slack, rejected files, or removed drafts cannot under-reserve bytes and swallow co-pasted text. Regression tests demonstrate the old mixed-batch text loss.
@nwparker
nwparker force-pushed the shrink-oversized-feedback-screenshots branch from b47911c to 91e7548 Compare October 6, 2026 00:21
@nwparker

nwparker commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Addressed the older review’s two summary warnings on the current head, 725ee664516efe7a47aed217a30ea5d59a9ea47f:

  • The final scope is full-resolution lossless PNG optimization, using the existing renderer attachment-ingestion path and browser bitmap/canvas APIs. Preparation happens before sending, so the dialog can show the resulting attachment or size refusal, and browser clients share the same path. The existing source-size and raster-dimension checks bound decoding; processing remains serial and releases bitmap/canvas resources. This reuses the current flow without adding a native image-processing dependency. The main process independently validates supported types, nonempty bytes, count and per-image/total byte limits before submitting (validateFeedbackImages, called at the IPC boundary and again in submitFeedback). It does not trust renderer validation. Images that cannot fit still receive the existing warning; the source file is unchanged. The PR body already says Related to Recompress oversized Send Feedback screenshots instead of refusing them #22776, explicitly excluding downscaling and lossy JPEG fallback; it does not claim to implement every backend proposal in that issue.
  • The docstring-percentage warning is a bot policy, rather than this repository’s requirement. AGENTS.md asks for concise, non-obvious WHY comments, ideally one line. The implementation documents its resolution/detail policy and resource cleanup where useful; adding repetitive docstrings solely to reach 80% would contradict that guidance. The repository’s changed-code quality checks pass without suppressions.

The two concrete inline findings (JPEG detail loss and pending-batch under-reservation) are separately fixed, regression-tested, replied to and resolved. Latest-head CI remains the merge gate.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the delta since the prior pullfrog review (b47911c): a rebase onto newer main plus three test-only commits. The feedback production code is unchanged from the previously approved head; the new commits add test coverage and one test-only dependency.

  • De-duplicated the journal provider-handle import — journal-submission-positions.test.ts keeps a single codexProviderHandle import (moved to alphabetical position); no behavioural change.
  • Covered the encoded Windows launch line — startup-line-prompt-carry.test.ts adds a win32 case asserting a short multi-line prompt rides the launch line with no raw \n and a `n escape, and moves the "not carried" control to darwin (a short win32 prompt is now carried). The encoding lives in buildAgentStartupPlan, not the gate under test — the distinction the new control relies on is correct.
  • Pinned the released journal parser dependency — @streamparser/json@0.0.26 is added to devDependencies with a comment above CHECKOUT_FORMAT = 4. release-checkout-tree.ts archives only src/** and runs no install, so the extracted released sources resolve the bare import from the repo root's node_modules; no format bump is needed because extraction is unchanged.

I ran the two changed suites at 725ee66 (35 tests) — both pass. All prior pullfrog threads remain resolved.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@nwparker nwparker left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low merge risk after the final repairs. Oversized screenshots are optimized only through full-resolution lossless PNG encoding. The original attachment limits remain enforced before decode, and independent paste reservations preserve eligible images and accompanying text. 156 focused tests, both typechecks, and quality gates pass. Fresh CI on this exact head has passed, and review findings are addressed.

@nwparker
nwparker merged commit a47e0f5 into stablyai:main Oct 6, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants