Skip to content

feat(web): give image projects their own canvas - #6865

Open
open-design-crew[bot] wants to merge 1 commit into
agent/web-canvas-workflow-overhaulfrom
feat/web-canvas-image-opt
Open

feat(web): give image projects their own canvas#6865
open-design-crew[bot] wants to merge 1 commit into
agent/web-canvas-workflow-overhaulfrom
feat/web-canvas-image-opt

Conversation

@open-design-crew

Copy link
Copy Markdown
Contributor

Why

Driving a real image project end-to-end kept running into the same wall: the workspace has no idea an image project is a set. metadata.kind === 'image' fell through to the generic file viewer, so opening the project showed one picture with 下载 / 关闭 and nothing else — no way to see the nine things the run produced, no way to act on one of them, no way to move between them without going back to the Design Files list.

Meanwhile prototype and wireframe projects, off the same branch's canvas edit overhaul (#6814), get a full canvas: a dock, a structure rail, comments, a mode segment. The gap isn't that images need more; it's that they were the only output type still being handed a document viewer.

This is a scoped first pass driven against a live image project, aimed at product review of the image workflow. Two behaviours inside it are presentation-only and called out below.

What users will see

Opening an image project now lands on a canvas instead of a single picture.

  • 平铺 (default) — every picture in the project in justified rows: mixed portrait/landscape, each row filling the pane's width, the trailing row left at its natural height instead of stretched. Hovering a card shows what produced it (model / resolution / ratio); on a narrow portrait card that wraps to two lines rather than dropping the model name.
  • 详情 — one picture centred, with a thumbnail rail down the left to step through the set; the mouse wheel moves between pictures. Hovering the picture shows the same model / resolution / ratio line.
  • 平铺 / 详情 sits in the workspace toolbar row, in the leading slot the HTML canvas uses for 编辑 / 演示 — same segment control, so the two canvases read as one product.
  • 分享 / 导出 / Handoff in that row's trailing file-actions slot. 分享 copies the picture's link; 导出 downloads it; Handoff is the existing component, unchanged.
  • Action dock at the bottom of 详情, reusing the HTML canvas's own .canvas-dock shell: 引用该图 (drops the picture into the composer as an attachment), 评论, 再次生成.
  • Clicking any card in 平铺 opens it in 详情.

Nothing else moves: the top bar, the nav rail, the chat pane and the Design Files tab are untouched, and the Design Files tab still reaches the plain file viewer for any single image.

Surface area

  • UI — new image-project canvas in apps/web (grid, focus view, thumbnail rail, action dock), plus the view segment and file actions it portals into the shared workspace toolbar row
  • Keyboard shortcut — new or changed
  • CLI / env var
  • API / contract — no new endpoint or contract shape; comments reuse the existing onSavePreviewComment path
  • Extension point
  • i18n keys — none added; see "Known state" below
  • New top-level dependency
  • Default behavior change — only image projects (metadata.kind === 'image') render differently; no schema, network or setting changes
  • None

Screenshots

To attach: 平铺 (justified grid), 详情 (thumbnail rail + action dock), and the workspace toolbar row showing 平铺/详情 alongside 分享 / 导出 / Handoff.

Known state

Three things a reviewer should weigh, rather than discover:

  1. 再次生成 is presentation-only. It shows a placeholder card and then appends a new card; it does not call a generation backend. The interaction it demonstrates — every generation is a new picture, originals are never overwritten — is the part under review.
  2. campaign-mock.ts is fixture data. Per-image title / model / resolution are hard-coded because nothing in the project record carries them yet. The image files are real project files; only this descriptive layer is fixture.
  3. No i18n keys. Strings on this surface are Chinese literals. Adding keys means all 19 locales, which is not worth paying before the surface settles — but it does mean this cannot ship to non-zh-CN users as-is.

Two implementation notes:

  • Image comments are percentage-anchored. PreviewCommentTarget is DOM-anchored (selector / elementId / htmlHint) with a re-anchoring ladder that a picture has no DOM to satisfy. Pins store percentage coordinates against the picture's own box and synthesise the selector fields the daemon's upsert validation requires. The composer, list and save path are all the existing ones; only the anchor semantics differ.
  • .chrome-action gained text-decoration: none. 导出 is the class's first <a> (a real download link) and arrived underlined. No other anchor uses the class today.

Validation

  • pnpm guard and pnpm --filter @open-design/web typecheck clean.
  • pnpm --filter @open-design/web test: 6 files / 11 tests fail — the same 6 files and 11 tests fail on this branch's base commit (996f4377c, verified by stashing this change and re-running those suites). These are the reconciliation failures feat(web): overhaul the canvas edit workflow #6814 already documents; this change adds none.
  • Driven manually against a seeded nine-image project: grid packing at several pane widths, focus navigation by rail and wheel, dock actions, and the toolbar row's alignment with the centred picture.

Base branch

Targets agent/web-canvas-workflow-overhaul (#6814), not main — this builds directly on that branch's canvas work, and basing on main would show its 95 files in this diff. Merge #6814 first.

An image project's artifact is the whole set of pictures, but the
workspace handed it the generic one-file-at-a-time viewer: a single
picture with 下载 / 关闭 and no way to see the set. Prototype and
wireframe projects, meanwhile, get a full canvas from the edit-workflow
overhaul. This closes that gap.

`metadata.kind === 'image'` now swaps the pane for an image canvas —
the same shape as a sketch file swapping in SketchEditor, but keyed on
the project rather than the active file:

- 平铺: justified rows (mixed portrait/landscape, each row filling the
  width, trailing row unstretched); hover shows model / resolution /
  ratio
- 详情: single picture with a thumbnail rail, wheel to step through
- Action dock (评论 / 再次生成 / 引用该图) reusing the HTML canvas's own
  `.canvas-dock`, so both canvases share one control language
- 平铺 / 详情 sits in the shared workspace toolbar row, and 分享 / 导出 /
  Handoff in its trailing file-actions slot — the row the other file
  surfaces already use

Comments run through the existing `onSavePreviewComment` path; an image
pin stores percentage coordinates rather than viewport pixels, since
there is no DOM to re-anchor against. 再次生成 is presentation-only for
now: it appends a card rather than calling a generation backend.

The per-image copy in `campaign-mock.ts` is fixture data for review —
nothing in the project record carries a title or model yet.
@lefarcen
lefarcen requested a review from nettee August 13, 2026 16:27
@lefarcen lefarcen added size/XL PR changes 700-1500 lines risk/medium Medium risk: regular code changes type/feature New feature labels Aug 13, 2026

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

@open-design-crew

The new image-canvas surface has blocking regressions in workspace routing, comment persistence/lifecycle, and read-only permissions. I verified the live diff and the existing viewer/comment contracts; the direct web TypeScript check and guard pass, while the full web suite still has the 11 base-branch failures documented in the PR. Please address the inline findings before merge.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

// the active file. The other workspace tabs (Design Files, browser, side
// chat, terminal, design system) still win, so nothing becomes unreachable.
const imageCanvasActive =
projectKind === 'image'

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.

[blocking] This condition ignores the active file and live-artifact surfaces. openFile() sets activeTab to a filename, so opening any item from Design Files makes this expression true again; if that item is text/code or a live artifact, the ImageCanvas branch below wins and the real viewer never renders. It also defeats the stated Design Files path for opening a single image. Derive this from an explicit image-canvas landing state and keep the active file/live-artifact render paths authoritative (or gate on the actual active surface) instead of only excluding a few tab-id classes.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.


// ---- comments ----------------------------------------------------------
const savedPins = useMemo(
() => (previewComments ?? []).filter((comment) => comment.filePath === current?.id),

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.

[blocking] previewComments is the project-wide list and includes attached, applying, needs_review, resolved, and failed rows. The existing canvas filters to status === "open", while this renders every historical/processed row and labels them i + 1; that resurrects resolved pins and renumbers/collides after a pin is resolved or deleted. The contract makes pinSeq permanent across all statuses. Filter the rendered list to open rows and use comment.pinSeq (with the documented creation-order fallback), while computing provisional numbers over all statuses.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

position: { x: pin.x, y: pin.y, width: 0, height: 0 },
htmlHint: '',
};
await onSavePreviewComment(target, pin.note.trim(), false);

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.

[blocking] savePreviewComment deliberately returns null on missing conversation, upload failure, or upsert failure after showing an error toast. Ignoring that result means a 401/500 on any pin still falls through to setDraftPins([]) and destroys every note in this batch; partial success also makes retry duplicate earlier comments. Track each result, retain failed pins (and only clear confirmed successes), and leave the composer open so the user can retry.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

<span>分享</span>
</button>
{current ? (
<a

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.

[blocking] The viewerOnly permission is not applied to the raw share/download actions here; the same missing guard also leaves the comment button/send path below (lines 616–644) enabled. Existing FileViewer gates raw share/download and annotation/send for read-only collaborators. As written, a viewer-only member can copy/download the asset and can attempt to POST comments despite read-only status. Gate or disable the image canvas share/export and comment entry/send controls with the standard read-only reason, rather than only hiding Handoff.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

title={comment.note}
onClick={(event) => {
event.stopPropagation();
if (onRemovePreviewComment) void onRemovePreviewComment(comment.id);

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.

[blocking] A normal click on an existing pin calls onRemovePreviewComment immediately. There is no open/edit affordance or confirmation, unlike FileViewer, where marker clicks open the composer and deletion is explicit. Users inspecting a comment can therefore permanently delete it with one click. Make marker clicks select/open the note, and expose deletion only behind an explicit control and confirmation.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

// against, so the picture's own box is the only stable frame.
const pinId = `pin-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
const target: PreviewCommentTarget = {
filePath: current.id,

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.

[blocking] Derived 再次生成 cards use synthetic item.id values while item.file.name remains the real project path. For a derived card, current.id here is rerun-…, so comments are saved under a non-existent filePath; they cannot be fetched/reopened, and the savedPins filter at line 251 will not find them. Use the real file path (current.file.name) for comment persistence/filtering, or disable comments on presentation-only cards and keep a separate display id.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@github-actions

Copy link
Copy Markdown
Contributor

Visual regression review

Head: bce647d · Base: 996f437

Baseline unavailable; PR screenshots are new visual cases and need baseline review.

0 changed · 0 unchanged · 46 new without baseline · 0 failed

New cases without baselines

PR PR PR
visual-avatar-local-agent-list
pr
visual-avatar-local-agent-list-panel
pr
visual-avatar-menu
pr
visual-avatar-menu-panel
pr
visual-avatar-open-design-model-picker
pr
visual-critical-settings
pr
visual-critical-workspace
pr
visual-critical-workspace-preview
pr
visual-design-system-detail
pr
visual-design-systems
pr
visual-home
pr
visual-home-catalog
pr
visual-home-context-picker
pr
visual-home-context-picker-popover
pr
visual-home-plugin-filter
pr
visual-home-plugin-use-staged
pr
visual-home-plugin-use-with-query
pr
visual-home-staged-attachment
pr
visual-integrations-use-everywhere
pr
visual-new-project-modal
pr

26 additional new case(s) omitted from this comment.

Visual diff is advisory only and does not block merging.

@lefarcen

Copy link
Copy Markdown
Contributor

@open-design-crew The blocking items on this head are already captured in @nettee's review and inline comments, and the current failing checks line up with that state, so the next step is to address those findings and push an update.

💡 To drive this PR to merge hands-free, paste this to your AI coding agent (Claude Code / Codex / opencode / Cursor …):
Take over nexu-io/open-design#6865 until it merges — read https://raw.githubusercontent.com/nexu-io/looper/main/skills/pr-takeover/SKILL.md and follow it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/medium Medium risk: regular code changes size/XL PR changes 700-1500 lines type/feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants