Skip to content

fix(jira): render issue description/comment images with lightbox - #8938

Merged
AmethystLiang merged 6 commits into
stablyai:mainfrom
lifefloating:fix/jira-issue-inline-images
Jul 27, 2026
Merged

AmethystLiang merged 6 commits into
stablyai:mainfrom
lifefloating:fix/jira-issue-inline-images

Conversation

@lifefloating

@lifefloating lifefloating commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Jira issue screenshots no longer disappear in the Tasks drawer. Inline images in descriptions and comments are resolved from ADF media/attachments and can open in a viewport-centered lightbox (Esc/X closes only the preview, not the issue sheet).

Closes #8934

Screenshots

  • Description with inline screenshots rendered in the issue drawer
  • Click-to-expand lightbox above the drawer
  • Comment-thread screenshots opening the same lightbox
  • Esc/X closing only the lightbox while the issue sheet stays open

(Please attach captures from a real Jira issue with pasted screenshots.)

Testing

  • pnpm lint (0 errors; existing unrelated warnings only)
  • pnpm typecheck
  • pnpm test (branch-related suites green; full suite had one unrelated flaky pty-connection case that passed on re-run)
  • pnpm build
  • Added or updated high-quality tests that would catch regressions, or explained why tests were not needed
    • adf-markdown / attachment-images / issues unit coverage for media → data URL conversion
    • MarkdownImageLightbox + Sheet interaction
    • CommentMarkdown expand-control regression for document variant

AI Review Report

Reviewed the ADF media path, authenticated attachment download, data-URL embedding limits, and lightbox/Sheet dismissal interaction.

Main risks checked:

  • Media UUID ≠ attachment ID mapping (rendered HTML order + filename fallback)
  • Memory growth from inlining many/large images (5MB / 12-image caps)
  • Esc/X dismissing the parent issue Sheet instead of only the lightbox
  • Comment bodies still on compact markdown (would show images but block expand)

Cross-platform: no new shortcuts, shell commands, or path separators. Image fetch uses existing authenticated Jira REST client (main process). Lightbox is renderer UI over Sheets; behavior is the same on macOS/Linux/Windows Electron. No platform-specific keyboard accelerators added (Esc only).

Security Audit

  • Attachment bytes are downloaded in main with existing Jira auth, then embedded as data: image URLs for trusted markdown rendering only.
  • SVG attachments are excluded from image download to avoid scriptable content.
  • Size/count caps limit IPC payload and renderer memory.
  • No new shell execution, secrets logging, or untrusted remote image auto-fetch for arbitrary URLs in compact cards.
  • Lightbox portals re-enable pointer events under modal Sheets carefully so controls work without leaking dismiss through to the drawer.

Notes

  • Comment threads now use variant="document" so they share the description lightbox path.
  • MarkdownImageLightbox locale keys were synced for en / es / ja / ko / zh.
  • Full pnpm build not run in this pass; worth running before merge if CI does not cover it.
image image

ELI5

Screenshots in Jira issue descriptions and comments vanished in the Tasks drawer. Images load again and open in a lightbox without closing the whole issue panel.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Jira ADF rendering now supports media resolvers, external image URLs, unresolved placeholders, and inline media. Jira attachment helpers parse metadata, extract referenced IDs, download eligible raster images, and map them to Markdown. Issue and comment loading passes attachment-aware options into Markdown conversion. Renderer markdown images now open in a portal lightbox, coordinate active-lightbox state, prevent underlying sheet dismissal, and include localized labels. Task-page Escape handling and related tests were updated.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements Jira image resolution, attachment downloading, data URLs, and lightbox behavior requested in #8934.
Out of Scope Changes check ✅ Passed The changes stay focused on Jira media rendering, attachment handling, and lightbox/sheet interaction.
Title check ✅ Passed The title clearly matches the main change: Jira issue and comment images rendered with a lightbox preview.
Description check ✅ Passed The description includes all required template sections and gives concrete summary, testing, review, security, and notes details.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/renderer/src/components/sidebar/MarkdownImageLightbox.tsx (2)

78-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded colors instead of design tokens.

This portal uses raw Tailwind arbitrary-value classes (bg-black/80, border-white/20, bg-black/55, bg-black/75) rather than tokens from src/renderer/src/assets/main.css. As per coding guidelines, "All UI work must follow docs/STYLEGUIDE.md, use tokens from src/renderer/src/assets/main.css, and use shadcn primitives from src/renderer/src/components/ui/." The custom portal is understandable given the documented Radix pointer-events conflict, but the color/opacity values themselves could still reference existing tokens (or a themed overlay token) instead of hardcoded black/white mixes so dark/light theme changes propagate consistently.

Source: Coding guidelines


93-103: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No focus trap inside the modal.

Only the close button receives initial focus; Tab/Shift+Tab are never intercepted, so keyboard users can tab past the single focusable element into content hidden behind the opaque backdrop. Consider trapping focus on the close button (or a small focus-trap loop) while the overlay is mounted.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5d51eb89-5ffc-4ab4-b44b-4c22d12bfefe

📥 Commits

Reviewing files that changed from the base of the PR and between 1ba71d2 and 9868d9b39b5c445ec3cda36354027be310db1b55.

📒 Files selected for processing (20)
  • src/main/jira/adf-markdown.test.ts
  • src/main/jira/adf-markdown.ts
  • src/main/jira/attachment-images.test.ts
  • src/main/jira/attachment-images.ts
  • src/main/jira/client.ts
  • src/main/jira/issues.test.ts
  • src/main/jira/issues.ts
  • src/renderer/src/components/JiraIssueWorkspace.tsx
  • src/renderer/src/components/TaskPage.tsx
  • src/renderer/src/components/sidebar/CommentMarkdown.test.tsx
  • src/renderer/src/components/sidebar/MarkdownImageLightbox.test.tsx
  • src/renderer/src/components/sidebar/MarkdownImageLightbox.tsx
  • src/renderer/src/components/sidebar/comment-markdown-element-renderers.tsx
  • src/renderer/src/components/sidebar/markdown-image-lightbox-state.ts
  • src/renderer/src/components/ui/sheet.tsx
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json

Comment thread src/main/jira/attachment-images.ts Resolved
Comment thread src/main/jira/attachment-images.ts Outdated
Comment on lines +198 to +217
return (attrs: JiraAdfMediaAttrs): string | null => {
const alt = attrs.alt?.trim() || 'Image'
if (attrs.url && /^https?:\/\//i.test(attrs.url)) {
return `![${escapeMarkdownAlt(alt)}](${attrs.url})`
}

if (attrs.alt?.trim()) {
const matches = byFilename.get(attrs.alt.trim().toLowerCase())
if (matches && matches.length > 0) {
const stillQueued = matches.find((image) => queue.some((entry) => entry.id === image.id))
const resolved = take(stillQueued ?? matches[0])
if (resolved) {
return resolved
}
}
}

// Why: ADF media IDs are Media Service UUIDs, not attachment IDs. Pair remaining
// inline images to downloaded attachments in rendered/document order.
return take(queue.shift())

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cache inferred attachments by ADF media UUID.

The resolver ignores attrs.id after consuming an attachment. If the same media UUID appears again without alt text, it consumes the next queued attachment or returns null, producing a wrong or missing image. Reuse the first resolution through a Map<string, string> keyed by attrs.id.

Comment thread src/main/jira/issues.ts Resolved
@brennanb2025

Copy link
Copy Markdown
Contributor

Taking a look!

@brennanb2025
brennanb2025 force-pushed the fix/jira-issue-inline-images branch from ef919e0 to 0ab218c Compare July 21, 2026 00:46
@brennanb2025

Copy link
Copy Markdown
Contributor

Thanks for the contribution — I rebased this onto current main and pushed a maintainer follow-up.

I replaced the custom lightbox portal and Sheet-wide dismissal state with the shared modal dialog primitive, which provides focus trapping, Escape ordering, and focus restoration while using design-system surfaces. Attachment resolution now scans the full metadata set before limiting, fetches only IDs referenced by rendered Jira content, reuses repeated media UUID mappings, respects an aggregate byte budget, uses same-origin provider content URLs for Cloud and Server/DC, and streams image-bearing detail/comment payloads in bounded chunks for remote-runtime/SSH use. Jira comments also keep their compact typography and opt into image expansion explicitly.

Validation: 89 focused tests, full typecheck, touched-file lint, React checks, max-lines checks, and localization checks pass. The repo-wide lint still reports two existing switch-exhaustiveness errors outside this PR.

@lifefloating

lifefloating commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the contribution — I rebased this onto current main and pushed a maintainer follow-up.

I replaced the custom lightbox portal and Sheet-wide dismissal state with the shared modal dialog primitive, which provides focus trapping, Escape ordering, and focus restoration while using design-system surfaces. Attachment resolution now scans the full metadata set before limiting, fetches only IDs referenced by rendered Jira content, reuses repeated media UUID mappings, respects an aggregate byte budget, uses same-origin provider content URLs for Cloud and Server/DC, and streams image-bearing detail/comment payloads in bounded chunks for remote-runtime/SSH use. Jira comments also keep their compact typography and opt into image expansion explicitly.

Validation: 89 focused tests, full typecheck, touched-file lint, React checks, max-lines checks, and localization checks pass. The repo-wide lint still reports two existing switch-exhaustiveness errors outside this PR.

Thanks a lot for the rebase and the maintainer follow-up — really appreciate you taking the time to clean this up!
Happy to help with any further polish if needed.

@lifefloating

Copy link
Copy Markdown
Contributor Author

I wanted to ask when this PR can be merged — I’m waiting to use this Jira feature. If there’s anything I need to adjust, I can do it now. Thanks!
@brennanb2025

@AmethystLiang

Copy link
Copy Markdown
Contributor

Thanks for this — the ADF media parsing, the lightbox component, and the TaskPage Esc-guard fix are all solid, and the test coverage is real rather than decorative.

We did a deep review pass and found enough issues that round-tripping them as individual review comments would be slow. Summary:

Correctness

  • buildCommentMediaOptions hardcodes /rest/api/3/issue/... instead of apiBasePath(site). On Jira Server/DC (/rest/api/2) that 404s, the error is swallowed by the catch, and comment images silently never render.
  • attachment-images.ts:247 — resolved ??= take(stillQueued ?? matches[0]) falls back to an already-consumed image. With two media nodes sharing an alt filename, the first attachment renders twice and the second never renders. Reproduced locally.
  • External media URLs are interpolated into ![alt](url) after only a ^https?:// check. A URL containing ) closes the link early and the remainder is parsed as markdown. rehype-sanitize + urlTransform stop it short of XSS, but a Jira field can still inject arbitrary links/images into the rendered body. Needs encodeURI.
  • extractAttachmentContentIdsFromHtml is the single gate for the whole feature and matches only two URL shapes — it misses the thumbnail forms (/secure/thumbnail/<id>/…, /rest/api/3/attachment/thumbnail/<id>). When it misses, preferredIds is empty and no images are fetched at all, degrading silently to *[name]*. There's no filename↔alt fallback even though fields.attachment is already in hand.

Performance

  • Downloads are serial (for … await, up to 12) while holding a Jira concurrency slot. MAX_CONCURRENT is 4 and both getIssue and getIssueComments acquire one, so a single screenshot-heavy issue pins half the pool through up to 24 sequential authenticated fetches.
  • No caching. loadComments refetches on mount and after every comment post, re-downloading and re-base64-ing every referenced image each time. Description and comments fetch independently, so an image in both downloads twice per open.
  • 20 MB total × 1.33 for base64 ≈ 27 MB strings, held in React state and in jiraIssueCache — which is bounded at 500 entries by count, not bytes. Given our renderer-OOM history that's the wrong bound for multi-MB payloads.

Design note (not blocking this PR)

Inlining bytes as data: URLs is what forces the chunked streaming RPC, the 32 MB guard, and the retained strings. A custom protocol handler would avoid all of it — but there's no prior art for one in this repo and the remote-runtime/SSH path would still need solving, so we're not asking for that here. We'll file it as a follow-up.

Next step

To keep this moving we'll push the fixes directly to this branch rather than sending you round after round of comments. Expect that within 24 hours, and happy to walk through any of it afterwards. (If branch permissions block a direct push, we'll open a PR into your branch instead.)

One question in the meantime: has this been exercised against a real Jira issue with pasted screenshots? Everything routes through that renderedFields HTML regex and all the current tests use hand-written HTML, so if it doesn't match real Jira output the failure mode is silent.

@AmethystLiang

Copy link
Copy Markdown
Contributor

Follow-up for protocol-handler transport rewrite: #10724

@AmethystLiang AmethystLiang 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.

Approved after plan + code review loops.

Implementation addresses the original review findings:

  • media↔attachment pairing no longer re-emits consumed images
  • Server/DC comment attachment lookup uses apiBasePath
  • markdown-safe external URL destinations (not encodeURI alone)
  • wider HTML discovery + Option A multi same-filename alt fallback
  • downloads concurrent outside the Jira API semaphore
  • main-process attachment data-URL cache with singleflight, lower caps, clear on disconnect/clearToken
  • post-map resolution warn with attachment-only stats

Follow-up protocol-handler rewrite: #10724

@AmethystLiang AmethystLiang 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.

Approved after plan + code review loops.

Implementation addresses the original review findings:

  • media↔attachment pairing no longer re-emits consumed images
  • Server/DC comment attachment lookup uses apiBasePath
  • markdown-safe external URL destinations (not encodeURI alone)
  • wider HTML discovery + Option A multi same-filename alt fallback
  • downloads concurrent outside the Jira API semaphore
  • main-process attachment data-URL cache with singleflight, lower caps, clear on disconnect/clearToken
  • post-map resolution warn with attachment-only stats

Follow-up protocol-handler rewrite: #10724

@nwparker nwparker added the bug Something isn't working label Jul 27, 2026
lifefloating and others added 6 commits July 27, 2026 15:29
Jira ADF media nodes were dropped when converting descriptions/comments
to Markdown, so screenshots never appeared in the Tasks drawer.

Download image attachments with authenticated Jira API access, embed them
as data URLs on issue/comment detail loads, and add a viewport-centered
lightbox. Closing with X/Esc only dismisses the preview, not the issue sheet.
Jira issue comments still used compact markdown, so screenshots rendered
but could not expand. Use the document renderer for comment bodies, add a
regression test for the expand control, and sync MarkdownImageLightbox
locale keys.
Address PR review findings: correct media-attachment pairing, Server/DC
attachment lookup base path, markdown-safe external URLs, wider HTML
discovery with gated alt fallback, concurrent downloads outside the API
semaphore, and a main-process attachment data-URL cache with lower caps.
Fix discovery so repeated alts (image.png) get distinct attachment ids,
flush resolution warns after ADF mapping using attachment-only stats,
clear attachment cache on clearToken with epoch-guarded singleflight,
and add Server comment path plus release-before-binary regression tests.
Only needingCount determines whether to skip the attachment metadata
request — htmlIds alone cannot produce a download without needing
media. Add type annotation for mediaAttrs for clarity.
@AmethystLiang
AmethystLiang force-pushed the fix/jira-issue-inline-images branch from d407be0 to bb30356 Compare July 27, 2026 22:29
@AmethystLiang
AmethystLiang merged commit cdd5ceb into stablyai:main Jul 27, 2026
42 of 44 checks passed
nwparker added a commit that referenced this pull request Oct 14, 2026
Load Jira attachment images separately from issue Markdown so descriptions and comments stay small while screenshots load lazily and open in the existing lightbox. Downloads begin when an image mounts, capture its owning host, require negotiated image support, and use bounded authenticated byte streams and caches. Older clients retain their inline-image handlers.

Keep legacy and cancellable downloads in separate in-flight domains so cancelling a new image reader cannot abort a legacy reader; completed bytes share the bounded cache. Preserve the landed assignee, board, transition/comment queue and request lifetimes. Move unchanged project metadata IPC handlers into their domain module and tighten two inherited focus-check ceilings without changing behavior.

Validation:410 tests/54 suites; all changed-source quality stages, lint, format, routing and contract checks; exact final node/web types; independent source review;48/48 fresh hidden Electron checks with226 stable source hashes and nine inspected screenshots. Synthetic transports exercise production clients and UI. Current CI static checks, mobile bundle, wire compatibility, Linux/Windows packaging and Windows serve pass; five test shards were pending at review.

Extends the existing attachment work credited to Frank (@lifefloating) and Brennan Benson in PR #8938; existing contribution credits and media remain in this PR.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…blyai#8938)

* fix(jira): render issue images and open them in a lightbox

Jira ADF media nodes were dropped when converting descriptions/comments
to Markdown, so screenshots never appeared in the Tasks drawer.

Download image attachments with authenticated Jira API access, embed them
as data URLs on issue/comment detail loads, and add a viewport-centered
lightbox. Closing with X/Esc only dismisses the preview, not the issue sheet.

* fix(jira): open comment images in the same lightbox as description

Jira issue comments still used compact markdown, so screenshots rendered
but could not expand. Use the document renderer for comment bodies, add a
regression test for the expand control, and sync MarkdownImageLightbox
locale keys.

* fix(jira): harden inline image handling

* fix(jira): harden inline image discovery, escaping, and downloads

Address PR review findings: correct media-attachment pairing, Server/DC
attachment lookup base path, markdown-safe external URLs, wider HTML
discovery with gated alt fallback, concurrent downloads outside the API
semaphore, and a main-process attachment data-URL cache with lower caps.

* fix(jira): Option A multi-same-name attachments and post-map media warns

Fix discovery so repeated alts (image.png) get distinct attachment ids,
flush resolution warns after ADF mapping using attachment-only stats,
clear attachment cache on clearToken with epoch-guarded singleflight,
and add Server comment path plus release-before-binary regression tests.

* fix(jira): simplify comment media request skip condition

Only needingCount determines whether to skip the attachment metadata
request — htmlIds alone cannot produce a download without needing
media. Add type annotation for mediaAttrs for clarity.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Jira issue images do not render in Tasks drawer

4 participants