Files
image-studio/docs/audits/2026-07-09-product-ux-architecture-audit.md

315 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Image Studio product, UX, and architecture audit
Date: 2026-07-09
Status: Phase 1 audit complete — code, workflow, architecture, and automated verification. A screenshot-based interaction pass remains pending because the in-app browser automation bridge was unavailable during this audit.
Implementation update, 2026-07-09: the Phase 0 strict TypeScript baseline has been restored. `bun run check` now runs type-checking, all 130 tests, ESLint, and the production build as one passing validation gate.
## Executive summary
Image Studio has a substantially better technical foundation than its current interface suggests. The document model supports artboards, nested groups, image and raster layers, masks, transforms, assets, generation provenance, selection, command history, and non-destructive generation candidates. The command-only state write path and read-only renderer are especially valuable foundations for a serious editor.
The main problem is that the product is still expressed as a collection of prototype controls rather than a coherent workflow. Generation is treated simultaneously as a tool, a settings panel, a canvas preview mode, and a dense bottom action bar. Layers, masks, generation candidates, document actions, transform properties, shortcuts, and debug commands compete for attention without a stable hierarchy.
Further product work should retain the document, command, input, and renderer foundations and organize behavior around three durable concepts:
1. **Document** — artboards, editable layers, masks, groups, transforms, and provenance.
2. **Operation** — select, transform, paint, mask, remove background, inpaint, outpaint, and generate.
3. **Candidate session** — temporary model outputs that can be compared, refined, accepted, combined, or dismissed without prematurely mutating the document.
## Audit method and limits
The audit covered:
- Repository and domain-specific `AGENTS.md` guidance.
- The core document model, editor state, commands, input, renderer, application composition, and React view.
- Primary workflows: image import, selection and transform, layer and group management, masks, brush/eraser, chroma key, magic wand, generation, inpaint preparation, candidate comparison, candidate acceptance, and export.
- Current test, lint, type-check, and build results.
- Current Git status and recent architectural history.
- Runtime HTTP health on a local development server.
The audit could not include screenshots or direct browser interactions. Findings about structure, behavior, accessibility markup, responsiveness, and styling are code-confirmed; exact visual proportions, animation feel, pointer hit areas, focus rendering, and browser-specific behavior still need a live visual pass.
## Product thesis
The strongest version of Image Studio is not a smaller Photoshop with an AI dialog. It is an editor for turning uncertain model outputs into controlled, editable compositions.
A central loop should be:
> Select context → describe an operation → generate alternatives → compare → accept or combine → refine locally
That loop should work for full-image generation, image-to-image, inpainting, outpainting, background replacement, object cleanup, and future model-backed operations. The document remains authoritative; generated results remain provisional until accepted.
## What is already strong
### 1. The state architecture has a clear backbone
Persistent document state lives in `core/`, transient editor state lives in `editor/`, changes flow through deterministic commands, input resolves intent, and the renderer consumes state without owning it. This is a sound basis for undo, serialization, collaboration, macros, and future agent-driven operations.
### 2. Generated outputs are provisional candidates
`GenerationState` separates candidates from the document. Candidates retain settings, seed, source inputs, masks, placement, crop information, and inpaint preparation metadata. This is exactly the right conceptual direction for comparing uncertain model outputs before committing them.
### 3. Accepted generated assets retain provenance
Generated assets record their mode, prompt, negative prompt, seed, model configuration, acceptance path, and inpaint context. This is strategically important. The UI does not expose it yet, but the underlying data could support “reuse settings,” “make variations,” auditability, and workflow history.
### 4. Masks and nested groups are real document concepts
Groups are tree nodes rather than flat UI labels. Masks are backed by raster assets and participate in commands and rendering. This avoids the common prototype trap where layers look hierarchical but behave as a flat list.
### 5. Automated behavioral coverage is meaningful
The repository has 130 passing tests across commands, history, transforms, selection, nested layer operations, masks, input, rendering decisions, generation candidates, inpaint preparation, and ComfyUI workflow construction.
## Priority findings
Severity meanings:
- **P0** — blocks treating the project as safely maintainable.
- **P1** — breaks or obscures a primary product workflow.
- **P2** — significant friction, inconsistency, or scaling risk.
- **P3** — lower-priority product polish and cleanup.
### P0 — strict TypeScript validation fails — resolved 2026-07-09
`bunx tsc --noEmit` fails despite tests, ESLint, and the production build passing. Current failures include unsafe layer narrowing, possibly undefined mask access, generic select-menu handlers, canvas typed-array incompatibilities, a resize hook call, input test typing, and the two currently modified Comfy files.
This creates a false-green validation story: production assets can build while the architecture's type contracts are already drifting. Strict type checking should be a required validation baseline.
Resolution: the affected layer narrowing, optional mask access, generic select handling, typed-array inference, resize-hook initialization, input-test dispatch typing, and current Comfy adapter typing were corrected without weakening the domain unions. The new `typecheck` and aggregate `check` scripts make the baseline repeatable.
### P1 — there is no durable project lifecycle
The app can import images and export an artboard PNG, but there is no project save, project open, autosave, recovery, or recent-document flow. `ImageDocument` is already serialization-friendly, but imported assets use object URLs and there is no persistence adapter.
For a layer-based editor, this is not an optional feature. Without it, the product cannot fulfill “turn an AI image into an editable project” beyond a single browser session.
The architecture needs an asset persistence strategy before project import/export is presented as complete.
### P1 — generation mode is hidden while infrastructure settings are promoted
The primary user intent—text-to-image, image-to-image, inpaint, or outpaint—is inside an “Advanced” disclosure. Meanwhile backend architecture, model, seed, and sometimes text encoder and VAE are presented as essentials.
This hierarchy is backwards for the target audience. Users should choose an operation first; compatible models and defaults should follow. Model infrastructure belongs in a secondary model/settings layer unless a power user explicitly expands it.
### P1 — generation is modeled as four competing UI concepts
Generation appears as:
- A tool in the left rail.
- A duplicate sparkle action in the top-right toolbar.
- A right-side settings sheet.
- A bottom action island containing execution, status, candidate navigation, comparison, regeneration, acceptance, mask editing, and dismissal.
These surfaces do not form a legible sequence. “Generate” is closer to an application operation than a pointer mode. Treating it as a normal canvas tool also makes panel visibility and keyboard behavior artificially coupled to `activeTool`.
### P1 — accepting one candidate destroys the entire candidate session — resolved 2026-07-09
Both “accept as layer” and “replace pixels” call `clearCommittedGenerationPreview`, which clears all candidates, not only the accepted candidate. This conflicts with the core use case of combining multiple model outputs. A user who generates several alternatives and accepts one loses the remaining comparison set.
Accepting a candidate should mark or remove only that candidate by default while preserving the rest of the session.
Resolution: both layer acceptance and masked-pixel replacement now remove only the committed candidate, select the nearest remaining candidate, and preserve the active comparison mode while results remain.
### P1 — up to twelve candidates are stored but only six are selectable — resolved 2026-07-09
Generation state retains twelve candidates, while `CandidatePicker` renders only `candidates.slice(0, 6)`. Candidates seven through twelve have no visible selection path. This is a concrete interaction bug, not merely a styling concern.
Resolution: the picker now renders the complete bounded candidate set in a horizontally scrollable group.
### P1 — “variant” is only a renamed ordinary layer — resolved 2026-07-09
“Accept variant” creates a standard top-level layer and records `variant-layer` provenance. There is no document-level variant set, linked source, stack semantics, exclusive visibility, or comparison group. The label promises more structure than the product provides.
Either call this “Add as another layer” or introduce a real variant/result-set concept.
Resolution: the duplicate variant action and `variant-layer` provenance value were removed. The UI now describes the real operation as “Add as layer”; refinement layers use the same honest layer acceptance semantics.
### P1 — generation preconditions are not represented clearly
The Generate button only requires a non-empty prompt. Image-to-image can proceed without a selected source image. Inpaint configuration is available without explaining or enforcing the required source layer and mask. Outpaint and mode-specific settings coexist regardless of current mode.
The workflow should state its required context before execution and offer direct repair actions such as “Select a layer,” “Create mask,” or “Use active artboard.” Disabled buttons alone are insufficient.
### P1 — non-inpaint generated placement ignores the active context
Non-inpaint candidates default to document position `(0, 0)` at native scale. The default artboard spans negative and positive coordinates, so a 1024×1024 result can appear partly outside the active artboard and does not use the viewport center or selected artboard bounds.
Placement should be explicit: fit active artboard, use requested frame, place at viewport center, or preserve source-layer bounds depending on operation.
### P1 — asynchronous generation state is owned by a transient React control — resolved 2026-07-10
Busy state, elapsed time, and errors live inside `GenerateActionControls`. Switching away from Generate unmounts that surface while the request continues. The user loses status and error visibility, and remounting removes the local busy guard even if a request is still running.
Generation jobs are application state, not ephemeral component state. They need stable IDs, lifecycle status, cancellation where supported, error details, and persistence across panel changes.
Resolution: generation and candidate follow-up work now run through command-driven, bounded job state with stable IDs, lifecycle timestamps, and durable errors. The existing Generate controls consume that state, the top toolbar keeps activity visible across tool and panel changes, concurrent submissions are rejected authoritatively, and document undo/redo no longer rewinds job lifecycle state. Cancellation remains a future adapter capability because the current Comfy request path does not expose cancellation.
### P2 — tool semantics mix modes, operations, and effects
The rail places Select, Generate, Brush, Eraser, Chroma Key, Magic Wand, and Pan at the same level. These are not peers:
- Select and Pan are navigation/interaction modes.
- Brush and Eraser are paint modes whose meaning changes during mask editing.
- Magic Wand is a selection/mask operation.
- Chroma Key is an effect-to-mask workflow.
- Generate is an AI operation.
Tool behavior should be classified explicitly so interaction modes and application operations do not share accidental state semantics.
### P2 — panel state ownership is inconsistent — resolved 2026-07-10
Generate visibility is derived from authoritative `activeTool`, while Layers visibility is local React state. `App.tsx` then manually enforces mutual exclusion across buttons, shortcuts, effects, and command-palette callbacks.
This works today but does not scale to additional application surfaces or operation states. Meaningful application state should have one model and one transition path.
Resolution: workspace panel state and Generate/Layers mutual exclusion now live in `EditorState` and transition only through commands. React consumes the resulting snapshot without corrective panel effects or local application state.
### P2 — view code owns application workflows and side effects — resolved 2026-07-10
React/view modules directly orchestrate image decoding, object URLs, network requests, generation preparation, candidate acceptance setup, raster processing, download behavior, and Comfy model discovery. Important examples are `useImageImport.tsx`, `GenerateControls.tsx`, `GenerateActionControls.tsx`, `runGenerate.ts`, and the mask/chroma-key helpers.
These functions are testable only unevenly and blur the intended boundary that React should display state and capture intent. Explicit application services/jobs should own these workflows without weakening the command-only mutation rule.
Resolution: explicit `operations/`, `platform/`, and `server/` boundaries now separate application use cases, browser/runtime adapters, and backend integrations. View modules emit intent and retain only UI-local drafts/disclosures; operations are prevented from accessing browser globals by ESLint and continue to write state exclusively through commands.
### P2 — imported object URLs have no durable ownership — resolved 2026-07-10
Image import creates object URLs and revokes them only when no artboard exists. Successful imports keep the URL indefinitely and would not survive project serialization or browser restart.
Asset sources need a lifecycle: persisted blob/handle, data migration, load/release hooks, and garbage collection when unreferenced.
Resolution: imported files are decoded into serialization-safe data URLs before command submission, eliminating retained object URLs and making imported asset sources independent of browser-session URL lifetimes. Temporary paint-preview object URLs remain platform-owned and are explicitly released.
### P2 — primary document actions are either invisible or duplicated
Undo and redo exist only as shortcuts/commands. Export exists in both the global top bar and every artboard row. Generate exists in both the rail and top bar. Fit/reset/zoom actions are split between transient bottom controls and the command palette. The command palette also exposes debug commands in the normal product surface.
Document actions should have one canonical invocation path, with the command palette acting as an accelerator rather than a separate source of behavior.
### P2 — accessibility is inconsistent
The four icon-only top-bar buttons have no `aria-label` or visible label. Other icon buttons are labeled more carefully, but abbreviations such as “Contig,” “Sub,” “Tol,” “Hard,” and “Clean” assume specialist knowledge. Tooltips depend mainly on native `title` attributes. Focus styles and hit targets need live verification.
### P2 — large modules have become change hotspots — resolved 2026-07-10
Several files combine multiple responsibilities:
- `commands/document.ts` — 923 lines.
- `renderer/image-textures.ts` — 754 lines.
- `view/CommandPalette.tsx` — 617 lines.
- `view/LayersSheet.tsx` — 505 lines.
- `commands/tool.ts` — 378 lines.
- `view/bottom-controls/GenerateControls.tsx` — 338 lines.
- `commands/generation.ts` — 315 lines.
- `view/bottom-controls/ChromaKeyControls.tsx` — 313 lines.
Line count alone is not a defect, but these files are already coordinating distinct concepts. Modules should split by product responsibility rather than by arbitrary component size.
Resolution: document-tree mutation helpers, WebGL texture programs, command-palette item construction, layer mask controls, server routes, and browser raster adapters now have focused modules. The remaining larger files represent cohesive command or rendering orchestration rather than mixing those extracted responsibilities.
### P3 — prototype identity remains in project metadata
The package is still named `bun-react-template`, the document defaults to “Untitled” without displaying that identity, the app has no visible product title, and debug palette items ship beside user actions. These details reinforce the prototype feel.
## Architecture recommendations
### Preserve
- Core document and asset model direction.
- Command-only state mutation.
- Editor/document state separation.
- Input resolution layer.
- Read-only renderer architecture.
- Generation candidate and provenance concepts.
- Nested group and mask invariants.
### Refine
- Restore strict TypeScript cleanliness.
- Model workspace/panel state consistently.
- Introduce generation job state separate from candidates.
- Change candidate commitment to preserve unrelated candidates.
- Define placement rules per operation.
- Define project and asset persistence.
- Separate application services from React presentation.
- Add operation precondition selectors with user-facing reasons.
### Avoid
- Rewriting the renderer without a product or performance requirement.
- Encoding presentation concepts in the core document model.
- Adding more one-off local panel booleans.
- Treating every AI workflow as a distinct persistent interaction mode.
## Recommended implementation sequence
### Phase 0 — restore engineering baseline
- Make `bunx tsc --noEmit` pass.
- Add it to the standard validation script/CI.
- Preserve and finish the current Comfy changes separately.
- Add focused tests for candidate preservation and generation preconditions.
### Phase 1 — product model corrections
- Introduce durable generation job state.
- Preserve candidate sessions after acceptance.
- Make all candidates reachable.
- Rename or model variants honestly.
- Define operation-specific placement and prerequisites.
- Define project persistence and asset lifecycle.
### Phase 2 — operation contracts
- Add operation precondition selectors with actionable failure reasons.
- Define output placement for text-to-image, image-to-image, inpaint, and outpaint.
- Define the valid acceptance actions for each candidate type.
- Add focused tests for missing context and placement behavior.
### Phase 3 — project persistence
- Define a versioned project serialization format.
- Persist asset data with explicit ownership and cleanup rules.
- Implement save, open, autosave, and recovery behavior.
- Add round-trip and migration tests.
### Phase 4 — candidate-session capabilities
- Support explicit session clearing and individual candidate dismissal.
- Add pinning and multi-selection only if concrete workflows require them.
- Surface provenance and reuse-settings actions through application operations.
- Keep candidate state provisional until an explicit acceptance command runs.
### Phase 5 — workflow consolidation
- Remove duplicate invocation paths that implement the same action differently.
- Keep model compatibility and defaults in shared application logic.
- Ensure generation jobs remain observable independently of mounted React controls.
- Verify undo/redo boundaries around accepted results and asynchronous jobs.
### Phase 6 — persistence and production hardening
- Save/open/recovery.
- Asset storage and cleanup.
- Keyboard and screen-reader pass.
- Pointer, touch, and pen pass.
- Performance profiling with large images, masks, deep trees, and candidate sets.
## Verification snapshot
Run on 2026-07-09:
- `bun test`: **pass**, 130 tests.
- `bun run lint`: **pass**.
- `bun run build`: **pass**.
- `bunx tsc --noEmit`: **fail**, multiple strict type errors.
- Local HTTP server: **200 OK** on port 3017 when run outside sandbox isolation.
- Screenshot/browser interaction pass: **not run**, browser automation bridge unavailable.
## Immediate next deliverable
Define and test operation preconditions and output-placement rules for text-to-image, image-to-image, inpaint, and outpaint. Each operation should report missing context explicitly and produce candidates with deterministic document placement.