Files
fusion/packages
gsxdsm 203557e7ae feat(dashboard): attach issue images on GitHub and GitLab import (#2151)
## Problem

An imported issue whose bug report *is* a screenshot arrived at the
agent as an unfetchable link.

Everything needed to show an agent an image already existed — the
executor builds a `## Attachments` section pointing at
`.fusion/tasks/<id>/attachments/` (`executor.ts:18887`), triage inlines
image attachments as base64 vision blocks, and `agent-prompts.ts:171`
explicitly permits reading that directory. But the import routes only
ever called `createTask()` with the issue body as text and never called
`addAttachment()`, so **that directory was always empty for imported
issues**.

The images can't be fetched later by the agent: GitHub
`user-attachments` assets redirect to a signed CDN URL and 404 on
private repos without credentials, and GitLab `/uploads/...` needs the
instance token. Import time is the only point where those credentials
are known to be present.

## Change

New `packages/dashboard/src/issue-image-attachments.ts` extracts images
from an issue's **body and comments**, downloads them, and stores them
via `addAttachment` — which already bridges images into the artifact
registry, so they also surface in the UI gallery.

Wired into every import surface:
- `POST /github/issues/import`
- `POST /github/issues/batch-import`
- All four GitLab routes, via the shared `importItem` chokepoint

Provider differences sit behind an `ImageImportPolicy` rather than one
shared host list, because the forges disagree on what matters:

| | GitHub | GitLab |
|---|---|---|
| URL form | absolute | usually relative `/uploads/<sha>/f.png` |
| Resolution | n/a | **project**-rooted, not instance-rooted |
| Trust boundary | fixed host allowlist | the configured instance origin
(self-managed = any host) |
| Auth | `Bearer` (gh CLI token) | `PRIVATE-TOKEN` |

Notable decisions:

- **Extraction runs on the original body, not the translated one.** The
translation model can rewrite or drop URLs — the same reason the
existing code appends `Source:` *after* translating.
- **`resolve()` returning null is the SSRF guard.** It's the single
place deciding a URL is ours to fetch, so
`![](http://169.254.169.254/...)` in an issue body is never requested.
- **Best-effort.** A failed download or comment fetch never fails an
import that already produced the task.
- **Batch stays cheap.** The REST `comments` count (free on the payload)
skips the comment fetch for issues with none, so a 50-issue batch
doesn't pay 50 round trips to discover empty threads.
- Capped at 10 images / 5MB each (matching `MAX_ATTACHMENT_SIZE`) / 15s
timeout.
- `GitLabClient.listNotes` is new and **read-only** — the client's
existing "no comment side effects" rule governs writes.

## Verification

- **30 new/updated tests pass** (23 helper + route-level wiring on both
forges). Route tests drive the real Express routes through to
`addAttachment`; the helper tests alone wouldn't prove the wiring.
- Typecheck clean (exit 0), lint clean, `check:changesets` passes.
- **Pre-existing failures confirmed against the untouched baseline, not
caused here:** 5 in `routes-github.test.ts` (`engine-unavailable`,
conflict-reclaim) and the `test:gate` `chat.test.ts` mock-completeness
failure both reproduce identically on `main` with this branch stashed.

One incidental test fix: `routes-gitlab.test.ts` used
`mockResolvedValue(jsonResponse(...))`, handing the **same** `Response`
instance to every call. A `Response` body is single-use, so the added
notes fetch got a consumed body. Switched to `mockImplementation` to
build a fresh one per call, matching the neighbouring test.

## Notes for the reviewer

Images are attached but the body's markdown links are left as-is — the
agent reads the files, and rewriting URLs in operator-visible text
seemed worse than leaving them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* GitHub and GitLab issue (and merge request) imports now convert
embedded screenshots in issue descriptions and comments/notes into real
task attachments.
* Works across single-issue and batch import workflows, including
project/group import flows.

* **Bug Fixes**
* Attachment extraction/import is resilient: per-image failures,
comment/notes fetch issues, and problematic/unsafe/oversized links won’t
break the overall import.

* **Tests**
* Added comprehensive coverage for URL extraction, provider policies,
attachment downloading/limits, redirects, and route integration for both
GitHub and GitLab.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:58:46 -07:00
..
2026-07-13 10:32:12 -07:00
2026-07-13 10:32:12 -07:00
2026-07-13 10:32:12 -07:00
2026-07-13 10:32:12 -07:00