Files
moteva/findings.md
T
root d424b56076 feat: add CanvasPathEditor and editable path functionality
- Implemented CanvasPathEditor component for editing paths on the canvas.
- Created editablePath module to handle path data manipulation and conversion to SVG.
- Introduced pathPen module for managing path drawing with a pen tool, including anchor management and path data generation.
- Added tests for editablePath and pathPen functionalities to ensure correctness of path editing and drawing behavior.
2026-07-17 11:28:29 +08:00

307 lines
58 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.
# Findings: Server-Backed Brand Kit Canvas Context
## 2026-07-17 Pen Tool and Shape Path Editing
- The request is frontend-only unless canvas persistence lacks a path-compatible node representation; the existing API-first go-zero workflow does not apply until a server contract change is proven necessary.
- The integration must preserve the existing workspace visual language and place a Pen icon immediately after Shape.
- Required behavior includes free path drawing, double-click entry/exit for path editing, automatic vertex/control-point editing through `leafer-x-path-editor`, and edit support for shapes created by the existing Shape tool.
- The toolbar already has a Pen icon immediately after Shape, but it currently activates a freehand brush that stores a flattened SVG data URL as an image node; those strokes are not vertex-editable.
- Shape-tool nodes are structured `frame` nodes whose JSON content describes rectangle, ellipse, polygon, star, line, or arrow geometry. They render through React SVG, while the existing Leafer adapter only owns successful image textures and manual frames.
- The integration can stay within the existing `CanvasNode` contract by persisting editable path data inside `content`; no REST or database field is needed.
- `leafer-x-path-editor@1.1.3` installed successfully, but its published peer dependencies are pinned to Leafer 1.1.0 while this project uses `leafer-ui@2.2.2`; the initial install pulled a second Leafer 1.x graph and reported peer conflicts.
- The plugin registers an inner editor on Leafer `Path`, emits `PathEditorEvent.CHANGE`, and expects an `App` with the editor plugin enabled. Runtime integration must ensure the plugin and project resolve the same Leafer core/editor classes.
- The plugin README matches the requested vertex/control behavior. Its current source uses Backspace for anchor deletion even though the requested interaction says Enter, so the application may need an explicit Enter-key bridge while editing.
- Runtime registration succeeds with the project's `@leafer-ui/core@2.2.2` and `@leafer-in/editor@2.2.2`; `@leafer-in/state` is overridden to 2.2.2 so only one Leafer generation is shipped. The package manifest still emits peer warnings because it declares exact 1.1.0 peers.
- Editable shape and Pen paths are normalized inside the existing opaque shape JSON `content`; Brush strokes remain SVG data URLs. Both survive the current frontend/server snapshot contract without adding fields or changing APIs.
- Path edits that cross the current node bounds expand the node while preserving vertex world coordinates, preventing clipping after editor exit.
- User clarification: the existing freehand brush must remain a separate tool. The new Pen is additive, sits immediately after Shape, and creates structured editable shape paths; Brush keeps its existing SVG image-node behavior and width/color controls.
## 2026-07-16 Canvas Performance and LeaferJS Evaluation
- The current canvas is a React/DOM renderer: every visual node is a `CanvasNodeCard`, and the viewport is applied through a transformed DOM content layer.
- Wheel zoom and canvas pan update React `viewport` state at pointer/wheel frequency. Node dragging rebuilds the full `nodes` array and calls `setNodes` on every pointer event, so high-frequency interaction can reconcile the workspace and every un-memoized node even when only one transform changed.
- Large CAD drawings are displayed as one external SVG `<img>`, not thousands of React elements. Their 9 MB SVG decode/paint cost can amplify frame drops, but React state churn is a separate engine-wide bottleneck that also affects normal images and shapes.
- LeaferJS exposes a retained scene tree, Canvas rendering, hit testing, dragging, zooming, and an editor plugin. Those capabilities align with the current visual/interaction layer, while project persistence, Agent Chat, CAD conversion/semantics, crop, toolbars, and backend contracts should remain application-owned.
- The LeaferJS README's million-element benchmark is vendor-provided and has not been reproduced for this application's image-heavy/CAD workload. The migration decision must use this repo's pan/zoom/selection frame-time baseline.
- A full rewrite would unnecessarily couple chat, persistence, and action panels to the engine. The likely boundary is a canvas renderer adapter driven by the existing `CanvasNode[]` domain model, with React retained for toolbars, panels, overlays, and text editing.
- Viewport changes invalidate more than the `.canvas-world` transform: every `CanvasNodeView` receives `selectionScale` and `viewportScale`, fresh inline event callbacks, a fresh transform object, and a newly built context-menu handler object. A shallow `React.memo` alone would therefore not stop node rerenders.
- `workspaceCropTarget` scans all nodes and recomputes screen bounds on every viewport update. Selection overlays and annotations also consume React viewport state, adding more work to every wheel/pan event.
- Node drag calls `moveNodesByDelta` for the whole document, filters moved nodes, computes alignment guides against the full node list, replaces the `nodes` array, and updates React state on every pointer event. This is a stronger predicted bottleneck than SVG decoding once the image is loaded.
- The latest `leafer-ui` package observed is 2.2.2, MIT licensed, with about 4.3 MB unpacked package contents. The base engine can render images and vector paths, but editor/plugin bundle size and exact SVG/image behavior require a prototype before dependency adoption.
- The separate `@leafer-in/editor` 2.2.2 package is also MIT and about 1.23 MB unpacked. It provides selection/transform events, but React DOM overlays and the existing tool model still need an adapter rather than direct component replacement.
- `useCanvasPersistence` depends on both `viewport` and `nodes`; each interaction update reruns its effect, clears the existing 1.2-second timer, and creates a new timer. It does not save over the network each frame, but it adds avoidable effect/timer churn to the render path.
- Alignment-guide computation is linear in all document nodes for a single active node (with nine edge/center comparisons per candidate), then React still renders the resulting guides and the full node list. The measured browser baseline should vary node count to separate algorithmic scaling from a single large SVG's paint cost.
- Playwright production baseline on a 120Hz browser with lightweight frame nodes:
- 100 nodes: zoom p95 16.1ms, drag p95 9.1ms, one drag frame over 20ms.
- 500 nodes: zoom p95 40.9ms and drag p95 41.4ms; more than 90% of measured frames exceeded 20ms.
- 1000 nodes: zoom p95 83.4ms and drag p95 82.8ms; almost every frame exceeded 34ms and many 50ms+ long tasks were observed.
- The decisive differential probe kept the same 1000 DOM nodes but bypassed React. Direct `.canvas-world` transform updates measured p95 9.2ms with no frame over 20ms; direct single-node position updates also measured p95 9.2ms. This confirms React reconciliation/state/effect work, not raw DOM compositing capacity, as the primary bottleneck.
- Ranked causes after measurement: (1) confirmed React full-tree high-frequency updates; (2) drag-only full-array/alignment/persistence amplification; (3) unmeasured large SVG decode/paint cost; (4) disproved raw DOM node count as the primary cause.
- The first exact-DWG paint probe served the raw `convertCadBufferToSvg` output directly and Chromium rejected it. This is expected for the internal boundary: the third-party writer emits anonymous `</>` closing tags, while the normal browser import path repairs and sanitizes them in `normalizeCadSvg` before upload. The valid CAD paint baseline must capture the final UI-uploaded SVG, not the worker's raw intermediate string.
- The real browser upload path converted `/Users/liangxu/Desktop/花语江南6-204方案.dwg` into a valid 8,868,302-byte SVG with a 1200 x 839 preview viewport.
- With that single CAD node, React zoom measured roughly 192ms p95 and even direct `.canvas-world` transform measured roughly 158ms p95, with nearly every frame over 34ms. This isolates a second bottleneck: Chromium repeatedly rasterizes the complex SVG during transform/zoom.
- Replacing only the mocked display source with a one-time 1200 x 839 transparent WebP preview (119,676 bytes) reduced both React and direct zoom to 9.2ms p95 with no frame over 20ms. The persisted node/source remained conceptually SVG; only the display surface changed in the probe.
- The engine therefore needs two independent mechanisms: retained/incremental scene updates for general node count, and cached raster display textures for complex SVG/CAD sources. LeaferJS is relevant because its Canvas image nodes naturally draw cached bitmap textures while the application can keep the original SVG URL and CAD semantics in `CanvasNode`.
- LeaferJS's React guide explicitly warns against directly binding Leafer elements to reactive data because proxy/reactive wrapping significantly reduces performance. The adapter must own mutable engine objects outside React and emit domain updates only at transaction boundaries.
- `App.zoomLayer` exposes imperative `x`, `y`, and `scale`, matching the existing `CanvasViewport` without requiring React rerenders. `App` also separates ground/tree/sky engine layers, which maps cleanly to grid, document content, and editor handles.
- Image paint supports SVG URLs and maintains an asynchronous high-performance image-layer cache by default (`sync: false`). The global default image cache ceiling is documented as 2560 x 1600, which covers the current 1200 x 839 CAD preview texture.
- The recommended dependency boundary is `leafer-ui` plus narrowly selected plugins such as `@leafer-in/editor`, not the full `leafer-editor` bundle, which also includes viewport, scrollbars, arrows, HTML, export, text editing, and other features already owned by the application.
- Editor selection/move/scale events expose world-coordinate deltas and selected UI objects. These can update the Leafer scene immediately and commit the resulting `CanvasNode[]` once on interaction completion, keeping existing persistence and Agent Chat contracts unchanged.
- A throwaway LeaferJS 2.2.2 browser benchmark with 1000 draggable rectangles measured 9.2ms p95 for both viewport updates and one-node movement, with no frame over 20ms. This validates the retained renderer against the same node count where the current React path measured about 83ms p95.
- LeaferJS does not automatically solve complex CAD SVG paint: the exact 8.87 MB SVG used directly as an image fill measured 275.4ms p95 during viewport updates. The same Leafer scene using the one-time transparent WebP preview measured 9.1ms p95.
- Recommended architecture:
- Keep `CanvasNode[]`, original SVG URLs, CAD metadata/semantics, crop/export/model-reference logic, persistence, and Agent Chat application-owned.
- Add a renderer adapter whose Leafer scene objects are mutable and never wrapped in React state. Use `App.tree` for document nodes, `zoomLayer` for viewport interaction, and engine hit testing/editor events for selection and transforms.
- Keep React for workspace chrome, Agent Chat, action panels, menus, and temporary DOM text editing. React receives selection/screen-bounds snapshots and committed transforms, not per-frame movement.
- Persist a separate optional render-preview URL for complex vector nodes. Generate transparent WebP levels from the SVG once, use the current level throughout interaction, and upgrade the level asynchronously after zoom settles. The source `content` remains SVG and is still used by crop/download/semantic/model boundaries.
- Proposed acceptance gates before replacing the default renderer: 1000 mixed nodes and the exact CAD fixture both at <=16.7ms p95 for pan/zoom/drag after resources are ready; zero 50ms+ interaction long tasks; source SVG and semantic context byte/contracts unchanged; crop and Agent Chat reference tests unchanged; pixel comparison for CAD previews at each texture level.
- Production implementation discovery: canvas-node persistence uses explicit field lists across the go-zero `.api` type, Go domain/API mappers, PostgreSQL schema/queries/SQLC models, and frontend snapshot normalization. A durable preview URL therefore requires an explicit `renderContent` field through every layer.
- The canvas upload flow currently batches one file/public URL per optimistic node. Vector preview support must upload the source SVG and transparent WebP together, then commit both URLs to the node only when the source upload succeeds; failed preview upload should fail the vector node rather than silently reverting to the known-stalling raw SVG renderer.
## 2026-07-16 CAD SVG Crop Fidelity
- The crop overlay's `1934 × 1671` label is the selected region size in canvas coordinates, not the generated sibling node size. `createVectorCropSiblingNode` still caps the sibling's longest displayed edge at 640.
- `cropImageNodeFile` currently loads the complete SVG as an `HTMLImageElement`, uses intrinsic-image source coordinates in the nine-argument `drawImage`, and scales that crop to a 2048px long edge.
- This path does not change the SVG `viewBox` before decoding, so the browser can rasterize the full drawing first and then magnify the selected pixels. That explains thick CAD strokes and degraded text/detail in the crop.
- The fix seam is to map the normalized selection into the source SVG `viewBox`, set the cropped SVG's width/height to the final WebP dimensions, decode that rewritten SVG, and draw it once with no source-rectangle resampling.
- Raster image crop behavior remains on the current source-rectangle path.
- A real Chromium pixel probe imported the production `createCroppedSvgRasterSource` function and used an SVG line with `vector-effect="non-scaling-stroke"`. The old full-SVG source-rectangle upscale expanded the line from 4px to 82px; the cropped-viewBox render stayed 4px before and after WebP encoding.
- The generated sibling display cap is independent of the crop overlay's canvas-coordinate label. The crop asset remains a 2048px-long-edge WebP for model readability while the canvas sibling stays bounded to a 640-unit long edge.
- CAD crop semantics are carried separately from the raster bytes: `canvasNodeToReferenceImage` copies `semanticContext`, the composer emits it as hidden `cad-context`, and the backend includes that context in model prompts while excluding it from visible chat text.
## 2026-07-15 Canvas Wheel Passive-Listener Bug
- The reported stack points exactly to `CanvasWorkspace/index.tsx:1242`, where the React `onWheel` handler calls `event.preventDefault()`.
- React delegates `wheel` as a passive event in this runtime, so the browser rejects `preventDefault()` even though the handler is correct to suppress page scrolling while zooming the infinite canvas.
- There is no competing native wheel listener in the workspace. The correct boundary is one native listener on `.canvas-stage` registered with `{ passive: false }`, with explicit cleanup and the existing toolbar/popover bypass guard preserved.
- DWG parsing does not directly cause the warning; the long import makes canvas wheel interaction more likely while the React passive handler is mounted.
- A first native-listener implementation still missed the canvas because its effect ran during the loading view. Binding on `project.id` attaches after `.canvas-stage` mounts; browser verification then changed zoom from 100% to 91%, returned `defaultPrevented=true`, and emitted zero passive-listener warnings.
- The persistent workspace Crop action now follows Text directly. It remains visible/disabled without a selected image, enables for ready raster/vector/CAD images, and opens the same normalized crop overlay used by the floating image toolbars.
## 2026-07-16 Enlarged Single-Image Crop
- The screenshot reproduces a different contract from selected-node cropping: one image fills or exceeds the viewport, no node is selected, and the workspace Crop button is disabled.
- Enabling Crop for every unselected multi-image canvas would be ambiguous. The deterministic fallback should apply only when exactly one visible, unlocked, ready image exists; explicit selection remains authoritative otherwise.
- Initializing an oversized image to the full `{0,0,1,1}` crop leaves resize handles and the crop panel offscreen. The initial normalized rectangle must be the intersection of the image's screen bounds with a safe visible stage area.
- Crop controls should anchor to the active selection's screen bounds, not the full oversized image bounds.
- Browser verification used an unselected 2000 x 1600 image extending beyond a 680 x 672 stage. Workspace Crop enabled automatically, selected the image, and initialized a 632 x 576 crop at the safe visible inset; the panel remained within the 1280 x 720 browser viewport.
- With two visible images and no explicit selection, Workspace Crop remains disabled as intended.
## 2026-07-15 CAD Semantics and Crop Follow-up
- The user explicitly declined legacy-project semantic recovery and will re-upload all DWG/DXF assets. Semantic guarantees therefore apply to newly converted CAD nodes; no heuristic reconstruction from old SVG snapshots is included.
- The requested feature has two coupled outputs: a visually small crop for efficient inspection and a bounded semantic payload that preserves CAD meaning beyond raster pixels.
- The source CAD-derived SVG must remain unchanged. Crop must create a sibling node and asset, enabling repeated crops from the same large drawing and independent Agent Chat follow-ups.
- Useful model semantics should prioritize layer names, block names, visible text, entity-type counts, units/extents, and crop bounds while enforcing size/count limits so large files cannot inflate every prompt.
- Existing crop infrastructure already renders any image source, including SVG, into transparent WebP, but `applyCropImage` currently replaces and repositions the selected node. CAD can reuse the overlay/rasterizer while branching to a new sibling-node save path.
- Vector nodes currently expose only the compact vector/download toolbar. Adding a crop icon there is sufficient to enter the existing crop overlay without exposing unrelated raster AI actions.
- `AgentContent` already supports `textSource`; a `cad-context` text part can carry hidden model context without a new chat payload type. Frontend optimistic display and backend user-message serialization must explicitly exclude that source while agent prompt assembly retains it.
- `CanvasNode` has no semantic field and PostgreSQL persists node columns explicitly. Durable semantics therefore require a spec-first optional node field plus domain/mapping/schema/query updates.
- The CAD converter still has the parsed `CadDocument` in the worker and can embed a structured `<metadata>` payload into the derived SVG. This preserves richer layer/block/text/entity data in the vector asset while the node stores only a bounded prompt-ready summary.
- Selected contract: the SVG metadata keeps version, source/format/units, drawing bounds, layer/block/entity counts, bounded visible text, and a bounded top-level entity index with visual-coordinate bounds. The node persists a compact human-readable `semanticContext` derived from it.
- Crop semantics can intersect the normalized crop rectangle with the SVG metadata's visual-coordinate entity bounds, producing a crop-specific layer/block/entity/text summary even though the sibling visual is WebP.
- CAD imports will use a distinct `cad-vector` layer role; crop outputs use `cad-crop`. Generic SVGs may reuse the crop action but do not claim CAD semantics.
- Existing `TextSource` behavior is ideal for model delivery: `cad-context` remains in prompt/reference-memory builders, while optimistic UI and persisted user-message rendering need a narrow visibility filter.
- PostgreSQL `canvas_nodes` uses explicit columns and SQLC-generated queries. Adding `semantic_context TEXT NOT NULL DEFAULT ''` requires schema/query regeneration plus repository/domain/API mapping.
- Exact R12 semantic verification exposed legacy GBK bytes decoded as Latin-1 by the CAD library (`¿î·âÑùÖ½°å`). The converter now repairs this high-confidence mojibake before both SVG writing and semantic extraction so labels remain meaningful.
- Exact R12 verification now completes in about 0.37 seconds with 15 model entities, 11,198 reusable block entities, corrected Chinese garment labels, and a 1,050-character bounded model context.
- Exact `花语江南6-204方案.dwg` verification completes in about 11 seconds with 4,376 model entities, 3,397 reusable block entities, 4,422 crop-indexed entries, meaningful Chinese layer/block/text names, and a 1,912-character bounded model context.
- Semantic metadata increases the large DWG-derived SVG from about 8.49 MB to about 9.05 MB while retaining the reusable-block performance model; the increase is bounded and materially smaller than the former 50 MB recursive output.
## 2026-07-15 Vector Reference Follow-up
- Raw CAD/SVG reference text is caused by the prompt-directive parser stopping at the first closing parenthesis in filenames such as `...2026-05-05(1) (DXF).svg`; the existing capsule and hover-preview UI is already shared with WebP references.
- GPT Image rejects SVG input bytes even when the canvas and browser can render them. The SVG source must remain unchanged on canvas and be rasterized to alpha-preserving WebP only when building model-bound image contents.
- Browser rasterization is the appropriate conversion boundary because the generated CAD SVG relies on reusable definitions, `<use>`, patterns, text, and browser SVG rendering semantics.
- `CanvasWorkspace.generate` is the shared Agent Chat and image-generator submission boundary. It currently uses the same `AgentContent[]` for optimistic display and the socket request, so model URL normalization must produce a separate request copy while the visible message retains the original SVG URL/name.
- SVG detection cannot rely only on public URL suffixes because uploaded asset URLs may not preserve the source extension; the `AgentContent` image name and fetched response MIME/type must also participate.
- `PromptComposer.toAgentContents()` already preserves `imageName`; `ImageGeneratorFloatingComposer` currently drops it and must retain the reference name so extension detection and user-facing labels remain reliable.
- Home creation uses serialized prompt directives rather than `AgentContent[]`, so it requires the same reference normalization before serialization if SVG attachments can immediately trigger generation there.
- The requested canvas scope has two model-bound submission sources, Agent Chat and the floating image-generator composer; both converge on `CanvasWorkspace.generate`, so one adapter at that boundary covers them without touching canvas persistence.
- The existing timeline capsule already routes canvas asset URLs through SVG-safe thumbnail/preview URL helpers and provides its copy behavior; once the directive parser recognizes the full filename, no separate vector-specific UI is needed.
## Requirements
- The previous implementation is frontend-only and insufficient for durable user ownership.
- The canvas header needs a compact Brand Kit selector beside the project name.
- The selector contains None plus the authenticated user's Brand Kits and shows the active selection.
- Agent Chat and every new session within that canvas must use the selected Brand Kit as a creative baseline.
- The Home prompt composer also needs a Brand Kit picker. Its empty state provides a direct `创建 Brand Kit` action.
## Initial Decisions
- Persist the project-to-kit relation on the server and inject context server-side so clients cannot silently omit it.
- Preserve the existing restrained Moteva canvas visual language and use the supplied interaction hierarchy without copying product identifiers.
## Repository Discovery
- The server is an existing go-zero REST service with one `img_infinite_canvas.api`, generated handlers/types, thin logic files, and a shared `ServiceContext`.
- PostgreSQL persistence is SQLC-based (`server/sqlc.yaml`, `schema.sql`, `query.sql`) while memory storage supports local development and tests.
- The local `mcp-zero` binary and `/Users/liangxu/go/bin/goctl` are both available; API generation can follow the required spec-first workflow.
- The frontend Brand Kit implementation is still uncommitted and browser-local, so it must be migrated carefully without discarding its editor/model work.
- Playwright prerequisites are available, and the user has an authenticated browser session for final integration testing.
- `design.Project` is the common object passed through chat planning, conversation, and image-prompt generation, so adding resolved Brand Kit context there gives one reliable injection point for all Agent Chat flows.
- `design.Repository` currently owns project CRUD; both memory and PostgreSQL implementations enforce user scope through `design.UserIDFromContext`.
- Projects currently persist title/canvas/thread state but no `brandKitId`; SQLC generates PostgreSQL accessors from `schema.sql` and `query.sql`.
- Agent messages already carry `thread_id`, and the request can start a new thread; the selected kit should therefore be loaded from the project before every chat operation rather than copied into individual messages.
- `DesignService.AgentChat` reloads the project before planning every turn, then passes project-scoped copies to planning, research, conversation, and background generation. Hydrating the Brand Kit immediately after that load covers existing and newly created threads.
- Creative-agent prompt builders already centralize project identity through `projectBriefContext`, while deterministic/fallback image generation reads `design.Project`; Brand Kit context should be added to both centralized context builders and agent memory.
- Project creation has synchronous and asynchronous paths. Both should accept an optional requested kit and otherwise bind the authenticated user's default kit server-side.
- An explicit Home selection should override the default kit; the server must still verify that the selected kit belongs to the authenticated user.
- Home's composer has a compact action row with existing Radix Popovers for model selection, making a palette-icon Brand Kit popover a native extension rather than a new interaction pattern.
- Home currently appends a browser-local default kit prompt. This must be removed once project creation sends `brandKitId`, preventing duplicated or client-controlled brand instructions.
- Frontend `Project` and canvas snapshot merge paths need to preserve `brandKitId`; the direct project-document endpoint remains the authoritative source.
- Canvas workspace already centralizes title rendering and project updates, so the selector can live beside `WorkspaceTitle` and update the same in-memory project after a binding API call.
- The existing Brand Kit document is already versioned and contains the requested grouped colors, uploaded fonts with optional sizes/descriptions, logos, cover, references, voice, and visual direction. The server can validate this exact version-1 shape and compile it into authoritative prompt context.
- The project repository abstraction has a single `Save` operation and user-scoped `Get`; adding `BrandKitID` to `design.Project` will naturally flow through memory/cache/realtime wrappers as long as PostgreSQL mapping and API mappers are updated.
- `ServiceContext` constructs storage-specific modules beside the project repository. A dedicated Brand Kit service/store should follow the existing auth/sharing module pattern and use the same configured memory/PostgreSQL driver.
- All `/api/*` routes except the explicit public allowlist already require a valid bearer session, so new `/api/brand-kits` routes automatically enforce login before logic runs.
- SQLC is installed locally. The existing PostgreSQL repository runs the idempotent schema at startup, making schema/query changes plus SQLC regeneration the consistent persistence path.
- Direct image mode currently sends the raw user prompt to image generation, bypassing the creative planner. Brand context therefore must also be added to the direct/fallback image prompt builders, not only long-term memory and `projectBriefContext`.
- Asynchronous create/follow-up jobs reload projects from persistence, so Brand Kit context must be re-resolved inside those job execution paths; hydrating only the initial HTTP turn would be insufficient.
- Uploaded Brand Kit logos, cover images, and reference images are resolved server-side as ephemeral project inputs. Image generation appends them after user-supplied references so existing inline reference numbering remains stable.
## Device Management Request (2026-07-11)
- The requested visual is a restrained account settings page headed `设备管理`, with a safety-limit explanation, a bulk removal action, and a table of active devices.
- The repository already contains `list_account_devices_logic.go`, `remove_account_devices_logic.go`, and an `AccountManagementDialog`, so the first implementation step is to verify and complete the existing vertical slice instead of creating a parallel settings system.
- The target interaction must cover loading, API failure, no secondary devices, current-device labeling, disabled destructive actions, confirmation, and responsive behavior.
- The account dialog already renders a device section and calls `authGateway.devices()` / `removeDevices()`, but it silently fabricates a current-device row whenever the API is empty or fails. That masks production errors and makes destructive-action state unreliable.
- The existing removal handler has no pending/error guard around the async request, so repeated confirmation clicks or a network failure can produce duplicate calls or an unhandled rejection.
- Existing localized copy says bulk removal invalidates the current session, while the visible button is disabled when only the current device remains. Backend behavior must be treated as the source of truth and the copy aligned to it.
- The server device service is currently a stub: `ListDevices` always synthesizes one unknown desktop row and both `RemoveOtherDevices` and `Logout` return zero. There is no persistent session/device registry.
- Access and refresh tokens currently identify only the user and token type. Production-grade selective revocation therefore requires a random session ID claim plus a server-side session record; user-only stateless tokens cannot distinguish the current device from other logins.
- The desired bulk action is already named `RemoveOtherDevices` and the UI disables it when only the current session exists, so the consistent contract is: preserve the active session, revoke every other session, and update confirmation copy accordingly.
- The authentication store supports users and verification codes only. A complete implementation must add session persistence to both memory and PostgreSQL stores, bind token validation to active session state, and capture request/device metadata without putting HTTP concerns into business logic.
- Selected architecture: signed access/refresh tokens receive an opaque random `sid`; active sessions are persisted with user, device, creation, expiry, last-seen, and revocation timestamps; every bearer authentication checks the session registry.
- Backward compatibility: an otherwise-valid legacy access token without `sid` is mapped to a stable digest-derived session only after confirming that its subject is an existing user. This avoids a forced logout during rollout and prevents signed state tokens from becoming user sessions.
- Middleware will attach sanitized request metadata for public login routes and authenticated requests, then attach the resolved session ID to request context. Business logic remains HTTP-agnostic.
- Last-seen writes will be throttled and active device rows sorted current-first, then most recently seen. Device metadata is informational only.
- PostgreSQL uses the repository's existing idempotent embedded schema; memory and PostgreSQL stores will implement the same atomic revoke-others contract.
- `github.com/mileusna/useragent` is now the selected parser. It exposes normalized browser, OS, device, desktop/mobile/tablet, and bot fields while keeping authorization independent from user-agent data.
- The Next.js API proxy forwards incoming headers after removing only hop-by-hop headers, so browser UA and Client Hint metadata reach the Go middleware without a new client contract.
- The current device table is rendered as three independent columns rather than semantic rows. This makes loading/error/empty states awkward, can misalign cells, and forces horizontal scrolling on mobile.
- The dialog already has restrained neutral styling consistent with the supplied reference. The right approach is to preserve that language while replacing only the device slice with a row grid, explicit async state machine, accessible status text, and mobile stacking.
- Current helper text fabricates an extra empty separator in the device string. The replacement will expose normalized primary metadata and a quieter client/last-seen line instead of leaking placeholder delimiters.
- Follow-up requirement: the desktop and mobile session counts must be configurable. The backend config will be the source of truth, `GET /api/account/devices` will return the effective limits, and localized UI copy will interpolate those values.
- Bug confirmation: `TestDesktopDeviceLimitRevokesOldestSession` fails because `upsertAndSign` creates sessions without consulting `DeviceLimits`; the values currently affect only API/UI presentation.
- Enforcement policy: limits apply independently to normalized desktop and mobile sessions. A new login succeeds and atomically revokes the oldest same-type session, so the latest login wins and active sessions never exceed the configured count.
- Existing sessions created before enforcement are reconciled when the authenticated device list is read, preserving the current session first and then the most recently created sessions.
- Follow-up display requirement: `lastSeenAt` must be a timezone-neutral instant over the API. The backend will emit RFC3339 UTC and the browser will format it with `Intl.DateTimeFormat` using its local timezone and the selected UI locale.
- No configured `mcp-zero` callable tool is available in this session, so API validation/generation will use the documented `goctl` fallback after the `.api` spec is updated.
## Visual Annotation Request (2026-07-11)
- The supplied reference adds `标注改图` as an image action with brush, circle, rectangle, and cross tools, a mark list, and generation controls.
- The repository already has general canvas point annotations for Agent Chat, but those annotations are not image-local drawing geometry and cannot represent marked regions.
- `CanvasWorkspace` owns image action mode state and already routes selected-image operations through a shared asynchronous generation path; the new mode should extend that path rather than add a parallel API.
- The requested `告诉 AI 怎么改` content needs to be an editable instruction per visual mark, not a fixed tool-name label as shown in the reference HTML.
- Existing unrelated user edits are limited to `frontend/src/app/layout.tsx`, `CanvasAgentComposer/index.css`, and `ImageDropOverlay/index.css`; the annotation work must preserve them.
- The existing node action contract already carries an open action string, prompt, selection, mask, and options map, but the backend rejects unknown normalized action names and only sends the original source image to the image model.
- A reliable annotated edit should send both the clean source and a browser-rendered annotated guide image; coordinate-only text is less robust for freehand marks and irregular regions.
- The browser can rasterize the selected node through the existing `imageNodeRasterBlob`, draw normalized annotation geometry and numbered badges, upload the guide with `designGateway.uploadImage`, and submit its URL through a dedicated spec field.
- The server should treat the guide as temporary visual context: include it as a second image in the action message, tell the model never to reproduce the marks, and keep the clean source as the output base.
- The selected canvas image model and credit metadata already exist in `CanvasWorkspace`; the annotation panel can expose the same model selection rather than invent a separate model state.
- Existing side panels use viewport-aware positioning and restrained neutral surfaces. The annotation UI should follow this dense operational style while keeping all drawing geometry directly on the selected image.
- The implemented contract adds `annotationImage` to the existing node-action request. The backend requires it for `visual-annotation`, keeps the clean source first, appends the guide second, and stores both URLs in generator-task metadata for traceability and asset cleanup.
- Brush, circle, rectangle, and cross marks use normalized coordinates, so the on-canvas overlay, uploaded guide raster, structured action payload, and resized canvas view stay aligned.
- Generation remains blocked until every mark has a non-empty editable instruction, preventing unlabeled regions from reaching the image model.
- Follow-up invariant: visual annotation never changes the source node. Submission creates a sibling copy to the source's right; generation state, failures, and the final image are isolated to that copy.
## CAD Import Request (2026-07-15)
- The infinite canvas should accept DWG and DXF drawings and display them through a derived SVG or canvas-compatible representation.
- The existing import, upload, persistence, and rendering architecture must be traced before selecting a converter or API boundary.
- The frontend is React 19/Next 16/Vite and currently has no CAD-specific dependency; `lucide-react` is the existing icon source.
- The backend is a single go-zero REST service. Any new REST conversion boundary must be added to `server/img_infinite_canvas.api` before generation.
- The working tree already contains a user-owned change to `frontend/next-env.d.ts`; CAD work must not modify or revert it.
- Canvas image upload is centralized through `designGateway.uploadImage`, with drag/drop filtering in `ImageDropOverlay` and an upload command in `WorkspaceToolbar`.
- The canvas already supports SVG image URLs and exports vector/image nodes to SVG or raster, making SVG the natural render target for CAD conversion.
- Existing client image upload preprocessing intentionally preserves SVG, while server asset uploads are generic `fileName` plus `contentType` requests.
- Image nodes render regular image URLs, including SVG, and persist their URL plus world-space dimensions in the existing canvas snapshot; no new canvas node type is required.
- `uploadImagesToCanvas` already performs optimistic node creation, parallel upload, selection, save, and failure cleanup. CAD import should convert first, then feed derived SVG `File` objects into this path.
- The current file input and drag/drop gates are image-only, so both explicit selection and DataTransfer parsing need CAD-aware file recognition and copy.
- No DWG/DXF converter CLI is installed in the local environment (`dwg2svg`, `dwg2dxf`, `dxf2svg`, LibreDWG tools, and Inkscape are absent).
- `dxf-parser` is available from npm and can cover browser-side ASCII DXF parsing, but it does not parse binary DWG; DWG needs a real LibreDWG/ODA/cloud conversion boundary rather than treating the binary file as SVG.
- The server configuration pattern supports adding a configurable CAD converter, but a default that depends on a missing host executable would leave local DWG import nonfunctional.
- Browser-capable packages now exist for DWG: `dwgdxf` provides an MIT-licensed WASM DWG-to-DXF converter, while several LibreDWG viewer/parser packages are GPL/AGPL and unsuitable as a casual dependency in a potentially proprietary app.
- A clean client pipeline is feasible: convert DWG bytes to DXF in a worker/WASM path, parse DXF, serialize supported entities into a sanitized standalone SVG, then reuse the existing image upload and canvas rendering flow.
- The canvas file input lives inside the stage and currently accepts `image/*`; the drag helpers are local functions in `CanvasWorkspace/index.tsx`, so CAD recognition can be scoped to this workspace without changing Home's reference-image drop behavior.
- `@node-projects/acad-ts` 2.3.0 is the strongest single dependency: MIT licensed, TypeScript-native, reads ASCII/binary DXF and DWG AC1014 through AC1032, and includes an SVG writer. Its 6.4 MB unpacked size argues for dynamic import so ordinary canvas startup is unaffected.
- `dwgdxf` is smaller and MIT but would still require a second DXF parser and a custom SVG renderer. The single `acad-ts` document model should preserve more entity/layer/text behavior with less application code.
- `@node-projects/acad-ts` ships as dependency-free ESM with first-class type declarations; its compressed tarball is about 969 KB, making a lazy-loaded import acceptable for CAD-only use.
- The frontend has no existing unit-test runner. Verification must rely on focused converter fixtures/manual scripts plus the existing TypeScript, Next, and Vite production builds unless introducing a runner becomes necessary.
- The selected package contains pure ESM/browser-neutral code with no Node built-in imports detected. It exports `DwgReader`, `DxfReader`, `SvgWriter`, and SVG converter internals from the main entry.
- The package's reader API accepts raw `ArrayBuffer` for DWG and `Uint8Array` for DXF, which aligns directly with browser `File.arrayBuffer()`.
- Source inspection found suspicious/incomplete closing-tag logic in the packaged `SvgXmlWriter`; the advertised SVG writer must pass a real fixture test before it is trusted. If it fails, retain its CAD readers but implement a small application-owned SVG serializer over the parsed document.
- The runtime fixture confirmed the writer emits invalid `</>` closing tags and maps default CAD color to white. The readers remain usable, but raw writer output cannot be uploaded directly.
- The writer's entity geometry is still valuable if its deterministic closing placeholders can be repaired and the root transform/color normalized before DOM validation; this is materially smaller than reimplementing arcs, hatches, inserts, and text.
- `SvgXmlWriter` can write to an `ArrayBuffer` sink and expose its complete string via `getOutput()`, avoiding fixed output buffer sizing.
- Final architecture: run the pure TypeScript DWG/DXF readers and geometry writer in a module worker; repair the writer's deterministic close placeholders; validate/sanitize with browser XML APIs; move the Y-axis flip into a bounded group; map CAD index-7 white to dark ink; size the derived SVG for canvas display; upload only that SVG.
- CAD files will be accepted only by the canvas asset input/drop path. Home/reference/clipboard image behaviors remain image-only.
- The installed package exposes `SvgXmlWriter` directly, so conversion can capture the generated string without guessing an output-buffer size. Text, groups, hatches, and inserts all use the same anonymous closing placeholder, enabling deterministic stack-based repair before XML parsing.
- Normalization will allowlist generated SVG elements/attributes, remove CSS/external-resource attributes, keep CAD text upright inside a drawing-wide Y flip, remap context-dependent CAD white to dark ink, and enforce readable non-scaling strokes.
- The implementation passes strict TypeScript compilation and locale JSON parsing. The worker uses the library collection's declared `count` API and preserves the existing image-only clipboard/reference paths.
- The full frontend build passes. Vite emits CAD conversion as a separate 1.02 MB worker chunk (201 KB gzip), so normal canvas JavaScript does not absorb the parser cost.
- Local preview dependencies are already listening on frontend port 5173 and backend port 8888. Playwright CLI prerequisites are available.
- Next's production output also includes worker-related chunks plus a `.ts` media asset; real-browser testing must confirm Turbopack serves executable worker code instead of treating the TypeScript source as a passive URL asset.
- The process currently occupying port 5173 belongs to an unrelated Electron/Vite project, not this repository. This app needs an isolated preview port.
- The selected CAD library repository provides real ASCII/binary DXF and DWG fixtures across AC1014-AC1032, including `samples/svg/export_sample.dwg`, enabling format-level browser verification without fabricated test data.
- Inspection confirmed the Turbopack media asset is raw TypeScript with a bare package import, so it would fail as a browser worker. Next must use a worker-aware build mode or the conversion must move off-worker.
- Next 16 officially supports `--webpack` for both dev and build. After switching scripts, the full Next+Vite build passes and the raw `.ts` worker media asset is gone; webpack emits the CAD dependency as a separate ~828 KB JavaScript chunk instead.
- Real-browser import of the selected library's own 116 KB DXF fixture fails before SVG generation because the parsed document exposes no model-space block. This invalidates the assumption that advertised DXF support is production-ready.
- The user's real failing file is a 915,957-byte AutoCAD R11/R12 (AC1009) DXF. R12 support is therefore a hard compatibility requirement, not an optional legacy case.
- Node verification shows the larger AC1015 upstream DXF does contain 161 model-space entities, while the geolocation fixture legitimately contains zero. The first browser failure still needs separation from the empty-fixture result before replacing all parsing.
- The exact R12 file parses successfully into 15 model-space entities referencing eight blocks and 11,205 nested drawing entities. The failure occurs later in `SvgXmlWriter`: its point/text/fill paths read raw ByLayer color index 256, whose RGB lookup is undefined, instead of using the resolved layer color.
- Resolving inherited entity colors before SVG serialization removes that library failure without changing the source file. The exact drawing now converts in about 0.6 seconds to a 2.24 MB SVG containing 10,518 points, 565 text elements, 114 polylines, and eight lines.
- Browser verification against the exact WeChat DXF completed the derived-SVG upload and project save with HTTP 200 responses, displayed nonblank paper-pattern geometry, emitted no console errors, and preserved the CAD node after reload.
- Next's production minifier renames `acad-ts` constructors, but the library uses `constructor.name` as its DXF metadata key; without repair, the parsed entities keep zero-valued geometry. The worker now restores stable names from the package's export keys before parsing, preserving normal production minification.
- The normally minified Next production build now imports the exact R12 DXF and the upstream `export_sample.dwg` fixture successfully; both produce persisted SVG image nodes without console errors.
- The second user-provided file, `/Users/liangxu/Desktop/花语江南6-204方案.dwg`, is an AutoCAD 2007/2008/2009 drawing with 4,376 model-space entities, 362 blocks, and 15,729 block entities. Parsing takes roughly 9-13 seconds, but the third-party SVG writer takes about 35 seconds and emits roughly 50 MB.
- Profiling at low arc tessellation shows recursive `INSERT` expansion contributes about 37.6 MB, while hatches contribute about 4 MB and all other model-space entity types about 1.3 MB. Increasing the timeout or reducing arc points would not address the dominant cost.
- `SvgXmlWriter._writeInsert` expands the referenced block at every occurrence and merges translation only, dropping scale and rotation. A generic fix must own reference assembly: serialize each block once, emit `<use>` instances with complete matrices, support MINSERT row/column instances, and guard nested reference cycles.
- The existing `VectorizedImageToolbar` already matches the requested compact `矢量` plus download UI. Its caller only identifies `layerRole === "vectorized-image"`, so normal uploaded SVGs and CAD-derived SVGs incorrectly receive the raster `image-node-toolbar`.
- SVG export already preserves SVG-backed source content through `downloadSvgSource`; vector detection can route the toolbar without changing the download implementation.
- Browser inspection of the second DWG reproduced the tiny-content symptom after the performance fix. Its extreme bounds come from one `MTEXT` entity around 550,000 drawing units away from the actual plans; the source marks that entity `isInvisible`, but the third-party SVG writer ignores CAD visibility. Excluding invisible/off/frozen-layer entities from both SVG output and bounds is the generic display fix.
## Leafer Renderer Implementation (2026-07-16)
- The resumed worktree contains only the Phase 32 API-spec field and its regenerated Go type; `leafer-ui` is not yet installed and no persistence or frontend render-preview plumbing exists.
- `CanvasNode` crosses both a compressed frontend snapshot and explicit server API/domain/PostgreSQL mappings. PostgreSQL uses ordered SQLC column/parameter lists, so `render_content` must be added consistently to schema, select, upsert, generated SQLC parameters, repository save/map, and mapper paths.
- Canvas covers currently derive from `node.content`; this should remain source-based for durable semantics/export unless a dedicated thumbnail consumer needs the preview texture.
- Canvas import currently creates one optimistic node per prepared file and uploads only its source. The extension point is `PreparedCanvasImportAsset`: add an optional WebP file, keep the local SVG object URL as optimistic content, and mark the node successful only when both source and preview uploads resolve.
- Existing SVG-to-transparent-WebP conversion already validates SVG, derives dimensions from `viewBox`, clears the canvas, and encodes WebP at up to 2048 px. It can be reused for render textures with a render-specific filename while leaving model-reference behavior unchanged.
- `ArtboardPreview` is a source-only DOM renderer today. Preview consumers can prefer `node.renderContent`, but crop/export/model paths must keep reading `node.content`.
- Leafer's `around: "center"` interprets `x/y` as the transform center. Passing the domain's top-left coordinates shifts the rendered image up and left by half its width/height while the React selection overlay stays at the correct domain bounds; retained visuals must pass center coordinates and convert imperative drag positions the same way.
- The apparent rectangular canvas boundary was a real initialization race: the renderer's async import completed after the stage resize state update, then applied the stale `1100 x 760` fallback captured by the project effect. The stage was actually `1685 x 1670`, so Leafer clipped everything outside the smaller internal canvas. Initial sizing must read the mounted layer's live `clientWidth/clientHeight` when the renderer becomes ready.
## Workspace Crop Drag Selection (2026-07-16)
- The clarified interaction is a two-stage tool: the workspace Crop button arms a crosshair without opening `CropOverlay`; the next click or drag chooses the initial region, then the existing editor opens.
- A click analyzes the displayed raster/WebP texture with bounded connected-color region growth. If no enclosing color boundary exists, the result becomes the maximum currently visible image region.
- A drag bypasses color analysis and maps the pointer rectangle directly into normalized source coordinates, clamped to the same visible-region maximum. This remains correct under canvas pan and zoom because mapping uses current screen bounds.
- Color analysis is capped at a 640-pixel edge and measured about 22ms for a fully connected 640 x 480 image in the local Node benchmark.
- CAD/SVG source and semantic metadata are untouched: the preview texture is used only for click analysis, while the established source-based crop path creates the final result and crop-specific CAD context.
## Reference-Anchored Generation Placement (2026-07-16)
- Canvas-backed composer references carry an ordered `canvas-node:<nodeId>` identity in `UploadedReferenceImage.id`, but `AgentContent` serialization keeps only the image URL/name and semantic context.
- Agent generation project updates currently enter through `useGenerationStream.applyProject` without any turn-specific placement hint, so generated nodes retain backend/default coordinates unrelated to the visible capsules.
- The placement rule must bind to the submitted turn, not the current composer state after reset: capture the last ordered canvas-backed reference at submit time and consume that anchor while reconciling newly introduced generated image nodes.
- A hidden `settings` directive now carries only the resolved node ID with the submitted turn; optimistic chat continues to render the original visible contents, so users do not see placement metadata.
- Resolution walks submitted capsules from right to left, preferring the stable `canvas-node:` identity and falling back to source URL matching. An unmatched external capsule falls back to the preceding canvas-backed capsule; no match emits no directive and preserves default placement.
- Backend placement applies to initial placeholders, non-incremental results, and every incremental result. Multiple outputs form a left-to-right row beside the anchor while the referenced image remains unchanged.
# Pen/Brush correction (2026-07-17)
- The required toolbar order is `Shape -> PenTool -> PenLine (Brush) -> Text`; the Pen button is additive, not a replacement.
- Preserve the existing `pen` tool as continuous Brush behavior and use the separate `path` tool for new vertex-editable Pen paths.
- Only explicitly marked new Pen SVGs and Shape-tool nodes should enter `leafer-x-path-editor`; unmarked legacy Brush strokes must remain non-editable.
- Current source already has separate toolbar buttons and `P`/`B` shortcuts, but `editablePathForNode` still accepts every `pen-stroke` SVG and therefore crosses the Brush/Pen boundary.
- `isEditablePathNode` currently recognizes only Shape nodes, so the new Pen node path must also be aligned with the marker contract.
- The partial implementation already persists new Pen output through `createEditablePenPathNode` as a Shape `kind: "path"`; this is the clean existing-domain distinction, so no SVG marker or API change is required.
- The remaining fix is to remove legacy `pen-stroke` SVGs from `editablePathForNode`/`applyEditedPathToNode` and lock the `pen -> Brush` versus `path -> Pen` wiring in the regression test.
- Frontend review confirms the additive UI follows the existing compact toolbar pattern and Lucide icon system; no visual redesign is needed for this correction.
- Dependency inspection resolves `leafer-ui`, `@leafer-in/editor`, `@leafer-in/state`, `@leafer-ui/core`, and `@leafer-ui/interface` to exactly `2.2.2`; `leafer-x-path-editor` is `1.1.3`.
- The previous Vite browser-verification server on port 5177 is no longer running and must be restarted after the production build.
- `leafer-x-path-editor` is strictly an inner editor for an existing Leafer `Path`; it does not provide blank-canvas path creation. The current Pen incorrectly stops after reusing Brush sampling and selecting the resulting Shape.
- Correct Pen workflow: blank-canvas drag creates the initial Shape-backed path and immediately opens `CanvasPathEditor`; clicking an existing editable Shape while Pen is active opens the same editor. Select-mode double-click remains supported.
- The corrected implementation passes the full 43-test suite, strict TypeScript, Next production build, Vite production build, and `git diff --check`.
- Browser sessions open the authentication dialog on the fresh Vite origin because the prior route mocks were origin/session scoped; restore focused auth/project mocks before interaction verification.
- Browser mock requirements are a stored `img-infinite-canvas.auth.session`, `/api/auth/profile`, the Lovart-compatible query-project fallback, `/api/projects/path-editor-test`, plus any initial Brand Kit/thread reads made after workspace hydration.
- The focused browser fixture can use one persisted rectangle Shape plus an empty canvas area, mock the Lovart query route as unavailable, and return the normal project document fallback; save-canvas responses must also be mocked because closing Path Editor commits immediately.
- Real-browser Pen verification passes: toolbar order is Shape, Pen, Brush, Text; drawing with Pen creates a second Shape node and immediately mounts one Path Editor overlay with two Leafer canvases while hiding the React preview.
- Real-browser Brush verification passes after closing the editor: Brush creates one `pen-stroke-node`, mounts no editor overlay, leaves Shape count unchanged, and remains the active repeated-drawing tool.
- Browser probing found one remaining wiring defect: with Pen active, clicks hit the existing Shape's SVG/DOM node but no editor overlay mounts and Pen stays active, so an earlier branch in `handleNodePointerDown` is intercepting before the Path branch.
- While verifying, the shared worktree gained a separate anchor-based `pathPen` implementation and updated regressions. Preserve it: Pen now creates anchors/control handles independently from Brush, but its commit and existing-Shape node branch still need to hand off to `CanvasPathEditor`.
- The first post-fix browser probe was stale: Vite logged a Fast Refresh invalidation because `createEditablePenPathNode` disappeared during the concurrent anchor-Pen change. A full reload is required before treating the browser result as current.
- After a clean Vite restart, active Pen clicking the existing rectangle mounts one Path Editor overlay, hides `shape-1`, and switches the active tool to Select.
- Anchor-based Pen creation also hands off correctly: two blank-canvas anchors plus Enter create a second Shape and immediately mount Path Editor for its new node ID.
- Final clean-browser Brush check passes on the anchor-Pen build: it creates exactly one `pen-stroke-node`, mounts no Path Editor overlay, and remains active for repeated freehand strokes.
- The only browser console errors are the two deliberate 404 responses used to force the mocked Lovart query-project route onto the normal project-document fallback; there are no runtime/editor errors.
- Final dependency and build checks confirm the frontend design integration remains on the existing compact toolbar system and exact Leafer 2.2.2 runtime; no version upgrade or API contract change was introduced.