ima2-gen 3.12.3 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +17 -3
  2. package/bin/commands/prompt-sub/build.js +5 -1
  3. package/config.js +14 -0
  4. package/docs/API.md +21 -0
  5. package/docs/CLI.md +7 -0
  6. package/docs/PROMPT_STUDIO.md +9 -0
  7. package/docs/migration/runtime-test-inventory.md +4 -1
  8. package/lib/configFileStore.js +44 -0
  9. package/lib/configKeys.js +4 -0
  10. package/lib/promptBuilder/client.js +106 -64
  11. package/lib/promptBuilder/constants.js +20 -2
  12. package/lib/promptBuilder/requestSchema.js +42 -7
  13. package/lib/promptBuilder/router.js +70 -0
  14. package/lib/promptBuilder/transport.js +5 -4
  15. package/package.json +2 -2
  16. package/routes/keys.js +6 -32
  17. package/routes/promptBuilder.js +68 -14
  18. package/skills/ima2/SKILL.md +40 -3
  19. package/skills/ima2-front/SKILL.md +1 -1
  20. package/skills/ima2-front/references/asset-requirements.md +16 -0
  21. package/ui/dist/.vite/manifest.json +44 -35
  22. package/ui/dist/assets/{AgentWorkspace-AJkVTaU5.js → AgentWorkspace-CBwn1WUC.js} +1 -1
  23. package/ui/dist/assets/AssetGenWorkspace-CleWSwPz.js +2 -0
  24. package/ui/dist/assets/AssetsWorkspace-DdjXb45L.js +1 -0
  25. package/ui/dist/assets/{CardNewsWorkspace-C_Eb2g7d.js → CardNewsWorkspace-BaNpBvJw.js} +1 -1
  26. package/ui/dist/assets/{GenerationRequestLogPanel-giH1e4hp.js → GenerationRequestLogPanel-Cd2ArsWr.js} +1 -1
  27. package/ui/dist/assets/HomeWorkspace-nKDhivwR.js +1 -0
  28. package/ui/dist/assets/KeyingPanel-DT8zsqpi.js +1 -0
  29. package/ui/dist/assets/{NodeCanvas-CF_NHudI.js → NodeCanvas-DcKs0w36.js} +1 -1
  30. package/ui/dist/assets/PromptBuilderPanel-e6BSRAdj.js +2 -0
  31. package/ui/dist/assets/{PromptImportDialog-B40X9UAa.js → PromptImportDialog-CLkjHz7L.js} +2 -2
  32. package/ui/dist/assets/{PromptImportDiscoverySection-BWoUDHj1.js → PromptImportDiscoverySection-DoQw4zcd.js} +1 -1
  33. package/ui/dist/assets/{PromptImportFolderSection-DBSEyJb-.js → PromptImportFolderSection-CcLiliAZ.js} +1 -1
  34. package/ui/dist/assets/{PromptLibraryPanel-DLywJBpt.js → PromptLibraryPanel-BjxzL7t5.js} +2 -2
  35. package/ui/dist/assets/SettingsWorkspace-Ctu_usKk.js +1 -0
  36. package/ui/dist/assets/SpriteRecipeWorkspace-DZjsabce.js +1 -0
  37. package/ui/dist/assets/index-0fpfY8vu.css +1 -0
  38. package/ui/dist/assets/index-DueH_AmZ.js +30 -0
  39. package/ui/dist/assets/index-ICf98ZcU.js +5 -0
  40. package/ui/dist/assets/{pptxgen.es-CSd_gCsx.js → pptxgen.es-C--aL3JM.js} +1 -1
  41. package/ui/dist/assets/promptBuilderStore-CkwnWMbR.js +1 -0
  42. package/ui/dist/assets/useAgentDialogFocus-DPB8JITN.js +1 -0
  43. package/ui/dist/index.html +2 -2
  44. package/ui/dist/assets/AssetGenWorkspace-Cguej69V.js +0 -2
  45. package/ui/dist/assets/AssetsWorkspace-DIQ9wBuD.js +0 -1
  46. package/ui/dist/assets/HomeWorkspace-DHmXGtqC.js +0 -1
  47. package/ui/dist/assets/PromptBuilderPanel-D7NJto-1.js +0 -2
  48. package/ui/dist/assets/SettingsWorkspace-BawWId1t.js +0 -1
  49. package/ui/dist/assets/SpriteRecipeWorkspace-Biy2yRAk.js +0 -1
  50. package/ui/dist/assets/VectorizePanel-DdlVvwV1.js +0 -1
  51. package/ui/dist/assets/index-D05Ong5g.js +0 -5
  52. package/ui/dist/assets/index-DGRIdnsL.css +0 -1
  53. package/ui/dist/assets/index-Sch70vBs.js +0 -30
  54. package/ui/dist/assets/useAgentDialogFocus-DcWwFfYd.js +0 -1
@@ -0,0 +1,70 @@
1
+ import { getGrokEndpoint } from "../grokImageCore.js";
2
+ import { waitForOAuthReady } from "../oauthProxy/runtime.js";
3
+ import { PROMPT_BUILDER_AUTO_ORDER, } from "./constants.js";
4
+ import { promptBuilderError } from "./errors.js";
5
+ function unavailableBackendError(backend) {
6
+ if (backend === "api") {
7
+ return promptBuilderError("OpenAI API key is required for Prompt Builder", "PROMPT_BUILDER_API_KEY_REQUIRED", 401);
8
+ }
9
+ if (backend === "grok-api") {
10
+ return promptBuilderError("xAI API key is required for Prompt Builder", "PROMPT_BUILDER_XAI_KEY_REQUIRED", 401);
11
+ }
12
+ return promptBuilderError(`${backend === "oauth" ? "OAuth" : "Grok"} backend is unavailable for Prompt Builder`, backend === "oauth"
13
+ ? "PROMPT_BUILDER_OAUTH_UNAVAILABLE"
14
+ : "PROMPT_BUILDER_GROK_UNAVAILABLE", 503);
15
+ }
16
+ export function selectPromptBuilderBackend(requestedBackend, lanes, allowed = PROMPT_BUILDER_AUTO_ORDER) {
17
+ if (requestedBackend !== "auto") {
18
+ if (lanes[requestedBackend]?.status !== "ready") {
19
+ throw unavailableBackendError(requestedBackend);
20
+ }
21
+ return { requestedBackend, backend: requestedBackend };
22
+ }
23
+ const backend = allowed.find((candidate) => lanes[candidate]?.status === "ready");
24
+ if (!backend) {
25
+ throw promptBuilderError("No Prompt Builder backend is ready", "PROMPT_BUILDER_NO_BACKEND_READY", 503);
26
+ }
27
+ const first = allowed[0];
28
+ return first === undefined || backend === first
29
+ ? { requestedBackend, backend }
30
+ : {
31
+ requestedBackend,
32
+ backend,
33
+ fallbackFrom: first,
34
+ fallbackReason: lanes[first]?.reason || lanes[first]?.status || "not-ready",
35
+ };
36
+ }
37
+ export async function resolvePromptBuilderTransport(ctx, backend, endpoint) {
38
+ try {
39
+ if (backend === "oauth") {
40
+ await waitForOAuthReady(ctx);
41
+ return {
42
+ url: `${ctx.oauthUrl}${endpoint === "responses" ? "/v1/responses" : "/v1/chat/completions"}`,
43
+ headers: { "Content-Type": "application/json" },
44
+ useOAuthFetch: true,
45
+ };
46
+ }
47
+ if (backend === "api") {
48
+ if (!ctx.apiKey)
49
+ throw unavailableBackendError("api");
50
+ return {
51
+ url: "https://api.openai.com/v1/responses",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ Accept: "text/event-stream",
55
+ Authorization: `Bearer ${ctx.apiKey}`,
56
+ },
57
+ useOAuthFetch: false,
58
+ };
59
+ }
60
+ const directApiKey = backend === "grok-api" ? ctx.xaiApiKey : undefined;
61
+ if (backend === "grok-api" && !directApiKey) {
62
+ throw unavailableBackendError("grok-api");
63
+ }
64
+ const target = getGrokEndpoint(ctx, "/v1/chat/completions", directApiKey);
65
+ return { ...target, useOAuthFetch: false };
66
+ }
67
+ catch (error) {
68
+ throw error;
69
+ }
70
+ }
@@ -1,5 +1,5 @@
1
1
  import { attachmentText, hasImageAttachments } from "./attachments.js";
2
- import { PROMPT_BUILDER_RESPONSE_MAX_OUTPUT_TOKENS } from "./constants.js";
2
+ import { PROMPT_BUILDER_RESPONSE_MAX_OUTPUT_TOKENS, } from "./constants.js";
3
3
  import { PROMPT_BUILDER_SYSTEM_PROMPT } from "./systemPrompt.js";
4
4
  import { contextText } from "./context.js";
5
5
  function toChatContent(message) {
@@ -30,7 +30,7 @@ function toResponsesContent(message) {
30
30
  return text;
31
31
  return [{ type: "input_text", text }, ...imageParts];
32
32
  }
33
- export function buildTransportPayload(model, messages, context) {
33
+ export function buildTransportPayload(backend, model, messages, context) {
34
34
  const currentContextText = contextText(context);
35
35
  const systemText = [
36
36
  PROMPT_BUILDER_SYSTEM_PROMPT,
@@ -38,7 +38,8 @@ export function buildTransportPayload(model, messages, context) {
38
38
  ]
39
39
  .filter(Boolean)
40
40
  .join("\n\n");
41
- const useResponses = hasImageAttachments(messages);
41
+ const useResponses = backend === "api"
42
+ || (backend === "oauth" && hasImageAttachments(messages));
42
43
  const endpoint = useResponses ? "responses" : "chat";
43
44
  const body = useResponses
44
45
  ? {
@@ -61,7 +62,7 @@ export function buildTransportPayload(model, messages, context) {
61
62
  })),
62
63
  ],
63
64
  stream: false,
64
- reasoning_effort: "low",
65
+ ...(backend === "oauth" ? { reasoning_effort: "low" } : {}),
65
66
  };
66
67
  return { endpoint, body };
67
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "3.12.3",
3
+ "version": "3.13.0",
4
4
  "packageManager": "npm@11.18.0",
5
5
  "description": "Local-first visual generation runtime and studio for people and coding agents, with reproducible image and video workflows across multiple providers.",
6
6
  "type": "module",
@@ -121,5 +121,5 @@
121
121
  "typescript": "^5.9.3",
122
122
  "yaml": "2.9.0"
123
123
  },
124
- "gitHead": "9cd60ac1fe483887bd25147679f385c58209ed4f"
124
+ "gitHead": "d39f9ea2205d47b4aae1df2d98648571f435ef7c"
125
125
  }
package/routes/keys.js CHANGED
@@ -1,31 +1,5 @@
1
- import { readFile, writeFile, rename } from "node:fs/promises";
2
- import { randomBytes } from "node:crypto";
3
1
  import { initVertexAuth, clearVertexAuth } from "../lib/vertexAuth.js";
4
- // Atomic + 0600 config write: temp file then rename, so a crash or concurrent
5
- // save can't corrupt config.json (which may hold API keys). Rename also forces
6
- // 0600 perms even if a looser-perm config pre-existed.
7
- async function writeConfigAtomic(cfgPath, data) {
8
- const tmp = `${cfgPath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
9
- await writeFile(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
10
- await rename(tmp, cfgPath);
11
- }
12
- let configMutationQueue = Promise.resolve();
13
- function serializeConfigMutation(mutation) {
14
- const result = configMutationQueue.then(mutation, mutation);
15
- configMutationQueue = result.then(() => undefined, () => undefined);
16
- return result;
17
- }
18
- async function updateConfigFile(cfgPath, mutate) {
19
- await serializeConfigMutation(async () => {
20
- let existing = {};
21
- try {
22
- existing = JSON.parse(await readFile(cfgPath, "utf-8"));
23
- }
24
- catch { /* new file */ }
25
- mutate(existing);
26
- await writeConfigAtomic(cfgPath, existing);
27
- });
28
- }
2
+ import { updateConfigFileAtomic } from "../lib/configFileStore.js";
29
3
  const KEY_PREFIX_MAP = {
30
4
  openai: ["sk-"],
31
5
  xai: ["xai-"],
@@ -130,7 +104,7 @@ export function mountKeyRoutes(app, ctx) {
130
104
  return res.status(400).json({ ok: false, error: "mode must be apikey|vertex", code: "INVALID_MODE" });
131
105
  }
132
106
  const cfgPath = ctx.config.storage.configFile;
133
- await updateConfigFile(cfgPath, (existing) => { existing.geminiAuthMode = mode; });
107
+ await updateConfigFileAtomic(cfgPath, (existing) => { existing.geminiAuthMode = mode; });
134
108
  ctx.geminiAuthMode = mode;
135
109
  return res.json({ ok: true, geminiAuthMode: mode });
136
110
  });
@@ -167,7 +141,7 @@ export function mountKeyRoutes(app, ctx) {
167
141
  }
168
142
  // Save to config.json
169
143
  const cfgPath = ctx.config.storage.configFile;
170
- await updateConfigFile(cfgPath, (existing) => {
144
+ await updateConfigFileAtomic(cfgPath, (existing) => {
171
145
  existing.vertexServiceAccountJson = trimmed;
172
146
  existing.geminiAuthMode = "vertex";
173
147
  });
@@ -186,7 +160,7 @@ export function mountKeyRoutes(app, ctx) {
186
160
  return res.status(400).json({ ok: false, error: "Cannot remove env-sourced key", code: "ENV_KEY_IMMUTABLE" });
187
161
  }
188
162
  const cfgPath = ctx.config.storage.configFile;
189
- await updateConfigFile(cfgPath, (existing) => { delete existing.vertexServiceAccountJson; });
163
+ await updateConfigFileAtomic(cfgPath, (existing) => { delete existing.vertexServiceAccountJson; });
190
164
  clearVertexAuth();
191
165
  ctx.vertexServiceAccountJson = undefined;
192
166
  ctx.vertexProjectId = undefined;
@@ -266,7 +240,7 @@ export function mountKeyRoutes(app, ctx) {
266
240
  }
267
241
  // Save to config.json
268
242
  const cfgPath = ctx.config.storage.configFile;
269
- await updateConfigFile(cfgPath, (existing) => {
243
+ await updateConfigFileAtomic(cfgPath, (existing) => {
270
244
  existing[CONFIG_KEY_MAP[provider]] = trimmed;
271
245
  if (provider === "gemini")
272
246
  existing.geminiAuthMode = "apikey";
@@ -321,7 +295,7 @@ export function mountKeyRoutes(app, ctx) {
321
295
  }
322
296
  // Remove from config.json
323
297
  const cfgPath = ctx.config.storage.configFile;
324
- await updateConfigFile(cfgPath, (existing) => { delete existing[CONFIG_KEY_MAP[provider]]; });
298
+ await updateConfigFileAtomic(cfgPath, (existing) => { delete existing[CONFIG_KEY_MAP[provider]]; });
325
299
  // Clear runtime
326
300
  if (provider === "openai") {
327
301
  ctx.apiKey = undefined;
@@ -1,27 +1,81 @@
1
+ import { updateConfigFileAtomic } from "../lib/configFileStore.js";
1
2
  import { errInfo } from "../lib/errInfo.js";
2
3
  import { logError } from "../lib/logger.js";
3
4
  import { requestPromptBuilderChat } from "../lib/promptBuilder/client.js";
4
- import { requireRuntimeContext } from "../lib/runtimeContext.js";
5
+ import { PROMPT_BUILDER_AUTO_ORDER, PROMPT_BUILDER_BACKENDS, PROMPT_BUILDER_MODELS, } from "../lib/promptBuilder/constants.js";
6
+ import { promptBuilderError } from "../lib/promptBuilder/errors.js";
7
+ import { normalizePromptBuilderConfig } from "../lib/promptBuilder/requestSchema.js";
8
+ import { requireRuntimeContext, } from "../lib/runtimeContext.js";
9
+ import { buildLaneSummary } from "./models.js";
10
+ function configLocks() {
11
+ return {
12
+ backend: process.env.IMA2_PROMPT_BUILDER_BACKEND !== undefined,
13
+ model: process.env.IMA2_PROMPT_BUILDER_MODEL !== undefined,
14
+ };
15
+ }
16
+ function configPayload(ctx) {
17
+ return {
18
+ ...ctx.config.promptBuilder,
19
+ options: {
20
+ backends: [...PROMPT_BUILDER_BACKENDS],
21
+ models: PROMPT_BUILDER_MODELS,
22
+ autoOrder: [...PROMPT_BUILDER_AUTO_ORDER],
23
+ },
24
+ locked: configLocks(),
25
+ };
26
+ }
27
+ function sendPromptBuilderError(res, error) {
28
+ const info = errInfo(error);
29
+ const unreadable = info.code === "CONFIG_UNREADABLE";
30
+ const code = unreadable
31
+ ? "PROMPT_BUILDER_CONFIG_UNREADABLE"
32
+ : info.code ?? "PROMPT_BUILDER_UNKNOWN";
33
+ const status = unreadable
34
+ ? 500
35
+ : typeof info.status === "number" && info.status >= 400 ? info.status : 500;
36
+ logError("prompt-builder", code, error, { status });
37
+ res.status(status).json({ error: { code, message: info.message } });
38
+ }
39
+ async function savePromptBuilderConfig(ctx, raw) {
40
+ const current = ctx.config.promptBuilder;
41
+ const next = normalizePromptBuilderConfig(raw, current);
42
+ const locked = configLocks();
43
+ if ((locked.backend && next.backend !== current.backend)
44
+ || (locked.model && next.model !== current.model)) {
45
+ throw promptBuilderError("Prompt Builder config is managed by environment variables", "PROMPT_BUILDER_CONFIG_ENV_LOCKED", 409);
46
+ }
47
+ await updateConfigFileAtomic(ctx.config.storage.configFile, (existing) => {
48
+ const saved = existing.promptBuilder;
49
+ const base = saved && typeof saved === "object" && !Array.isArray(saved)
50
+ ? saved
51
+ : {};
52
+ existing.promptBuilder = { ...base, ...next };
53
+ });
54
+ ctx.config.promptBuilder.backend = next.backend;
55
+ ctx.config.promptBuilder.model = next.model;
56
+ }
5
57
  export function registerPromptBuilderRoutes(app, ctxRaw) {
6
58
  const ctx = requireRuntimeContext(ctxRaw);
59
+ app.get("/api/prompt-builder/config", (_req, res) => {
60
+ res.json(configPayload(ctx));
61
+ });
62
+ app.put("/api/prompt-builder/config", async (req, res) => {
63
+ try {
64
+ await savePromptBuilderConfig(ctx, req.body);
65
+ res.json(configPayload(ctx));
66
+ }
67
+ catch (error) {
68
+ sendPromptBuilderError(res, error);
69
+ }
70
+ });
7
71
  app.post("/api/prompt-builder/chat", async (req, res) => {
8
72
  try {
9
- const result = await requestPromptBuilderChat(ctx, req.body);
73
+ const lanes = await buildLaneSummary(ctx);
74
+ const result = await requestPromptBuilderChat(ctx, req.body, lanes);
10
75
  res.json(result);
11
76
  }
12
77
  catch (error) {
13
- const info = errInfo(error);
14
- const code = typeof info.code === "string" ? info.code : "PROMPT_BUILDER_UNKNOWN";
15
- const status = typeof info.status === "number" && info.status >= 400
16
- ? info.status
17
- : 500;
18
- logError("prompt-builder", code, {
19
- message: info.message,
20
- status,
21
- });
22
- res.status(status).json({
23
- error: { code, message: info.message },
24
- });
78
+ sendPromptBuilderError(res, error);
25
79
  }
26
80
  });
27
81
  }
@@ -131,6 +131,12 @@ ima2 gen "1girl, blue hair, city at night" \
131
131
  --nai-auto-smea --nai-decrisper --nai-variety-plus
132
132
  ```
133
133
 
134
+ In the web app, selecting NovelAI changes the composer into two peer panes:
135
+ **Positive prompt** and **Undesired content**. They sit side by side when the
136
+ composer is wide enough and stack below a 719px container width. Other providers
137
+ keep the normal single prompt because they do not share NovelAI's dedicated
138
+ negative-prompt contract.
139
+
134
140
  For V5 native alpha, pair the request flag with an alpha-aware prompt:
135
141
 
136
142
  ```bash
@@ -172,6 +178,34 @@ Native control details come from the official
172
178
  [quality tags](https://docs.novelai.net/en/image/qualitytags/), and
173
179
  [seed](https://docs.novelai.net/en/image/seed/) pages, checked the same date.
174
180
 
181
+ ## Prompt Builder
182
+
183
+ The right-sidebar Prompt Builder refines prompts without generating an image. Open
184
+ Settings > Providers > Prompt Builder backend to choose where its text request runs,
185
+ and use Builder model to select a model from that backend's catalog. `Auto` is the
186
+ default: it tries GPT OAuth, Grok, OpenAI API, then Grok API and selects the first
187
+ ready backend. Each successful reply carries a `via <backend>` badge showing the
188
+ backend that actually answered.
189
+
190
+ Choose an explicit backend when cost, credentials, or model behavior must be stable.
191
+ An unavailable explicit backend returns a typed error and never falls back. Only Auto
192
+ performs readiness fallback, and there is no retry on a different backend after an
193
+ upstream request has been sent.
194
+
195
+ The CLI wrapper accepts the same per-request selection:
196
+
197
+ ```bash
198
+ ima2 prompt build \
199
+ --message "Turn this rough idea into a production image prompt" \
200
+ --backend <auto|oauth|api|grok|grok-api> \
201
+ --model <model>
202
+ ```
203
+
204
+ Run `ima2 config get promptBuilder.backend` and
205
+ `ima2 config get promptBuilder.model` to inspect the persisted server preference.
206
+ The Builder's model choices depend on the selected backend; do not assume the GPT
207
+ model list applies to Grok.
208
+
175
209
  ## Prompting Guidance
176
210
 
177
211
  GPT Image 2 can follow detailed visual instructions and can render visible text
@@ -420,9 +454,12 @@ generate a clean cutout FIRST; tracing quality follows input flatness.
420
454
  `mono` has no alpha channel, so a transparent cutout becomes a silhouette on a
421
455
  black field. Use it for stencils and line art, not for cutouts.
422
456
 
423
- In the app, the same operation is the "Convert to SVG" action on any image
424
- asset (generation grid tile or Assets library preview), which saves the SVG
425
- into the current project.
457
+ In the app, the same operation is **Convert to SVG** on an AssetGen tile or Assets
458
+ library preview. Canvas Mode also exposes **Trace to SVG (vector)** in Export: it
459
+ flattens the current canvas composition to PNG, saves that hidden canvas version,
460
+ then opens the same preset and fine-tuning panel. Do not confuse it with
461
+ **SVG (embedded raster)**, which keeps the base image as bitmap data and vectorizes
462
+ only Canvas annotations.
426
463
 
427
464
  ### Korean Text in Images
428
465
 
@@ -226,7 +226,7 @@ Read `references/aesthetics.md` for full guidelines. Summary:
226
226
  - **Motion**: See `references/motion.md`. One signature moment + a few
227
227
  supporting reveals > 10 scattered effects; landing-bucket floor/ceiling per
228
228
  FE-MOTION-BUCKET-01.
229
- - **Assets**: Use screenshots, product images, diagrams, charts, illustrations, generated bitmaps, or soft 3D only when they add product meaning. When a real bitmap is needed (icon, hero, illustration), generate it with `ima2` — probe `ima2 status`, attempt `ima2 serve` if down, inspect `ima2 models --kind image`, and set `ima2 defaults set image <lane>/<model>` before using bare generation — falling back to the native `imagegen` tool only when ima2 is truly unavailable; never ship a placeholder. `ima2` is preferred because it supports reference images, multi-candidate generation (`-n N`, multimode, independent CLI parallel — see `asset-requirements.md` FE-ASSET-PARALLEL-01), prompt builder, session style sheets, provider routing (GPT/Grok/Gemini — see `asset-requirements.md` FE-ASSET-PROVIDER-01), variant selection with element-ledger synthesis (`asset-requirements.md` FE-ASSET-SELECT-01), cutout asset background strategy (`asset-requirements.md` FE-ASSET-BG-01), and video (`ima2 video` — see `motion.md` FE-MOTION-VIDEO-01) for motion assets. For parallel generation, monitor with `ima2 ps --json` and cancel unwanted jobs with `ima2 cancel <id>`. Write **very explicit long prompts** (subject, composition, palette, lighting, style, aspect) per `asset-requirements.md`; prefer real/generated image or video assets over CSS gradient washes. Read any design reference or captured screenshot back into context with `view_image` before matching it. Third-party captures follow `reference-capture.md` (analysis-only, provenance manifest).
229
+ - **Assets**: Use screenshots, product images, diagrams, charts, illustrations, generated bitmaps, or soft 3D only when they add product meaning. When a real bitmap is needed (icon, hero, illustration), generate it with `ima2` — probe `ima2 status`, attempt `ima2 serve` if down, inspect `ima2 models --kind image`, and set `ima2 defaults set image <lane>/<model>` before using bare generation — falling back to the native `imagegen` tool only when ima2 is truly unavailable; never ship a placeholder. `ima2` is preferred because it supports reference images, multi-candidate generation (`-n N`, multimode, independent CLI parallel — see `asset-requirements.md` FE-ASSET-PARALLEL-01), a backend-selectable prompt builder, raster-to-vector tracing (`ima2 vectorize` and Canvas trace — see `asset-requirements.md` Raster vs real vector), session style sheets, provider routing (GPT/Grok/Gemini — see `asset-requirements.md` FE-ASSET-PROVIDER-01), variant selection with element-ledger synthesis (`asset-requirements.md` FE-ASSET-SELECT-01), cutout asset background strategy (`asset-requirements.md` FE-ASSET-BG-01), and video (`ima2 video` — see `motion.md` FE-MOTION-VIDEO-01) for motion assets. For parallel generation, monitor with `ima2 ps --json` and cancel unwanted jobs with `ima2 cancel <id>`. Write **very explicit long prompts** (subject, composition, palette, lighting, style, aspect) per `asset-requirements.md`; prefer real/generated image or video assets over CSS gradient washes. Read any design reference or captured screenshot back into context with `view_image` before matching it. Third-party captures follow `reference-capture.md` (analysis-only, provenance manifest).
230
230
  - **Visual verification**: after UI changes, exercise the flow per visual verification (screenshot -> view_image) — `browser:control-in-app-browser` on the dev server, screenshot, `view_image` — instead of claiming visual correctness from code alone.
231
231
 
232
232
  ### Cutout Asset Generation (FE-ASSET-BG-01 surface — STRICT)
@@ -45,6 +45,22 @@ usable asset.
45
45
  | 4 | CSS device frame + screenshot | App mockup heroes (Toss/카카오 style). Pure CSS phone/laptop frame wrapping a real screenshot or UI |
46
46
  | 5 | Placeholder service (last resort) | `picsum.photos/800/600`, `placehold.co`. Mark as TODO for replacement |
47
47
 
48
+ ### Raster vs real vector
49
+
50
+ An image prompted as "flat vector style" is still a raster bitmap. When the shipped
51
+ asset must contain scalable paths (logo mark, icon, simple sprite, stencil), generate a
52
+ clean flat or transparent raster first, then run:
53
+
54
+ ```bash
55
+ ima2 vectorize input.png -o output.svg --preset auto --json
56
+ ```
57
+
58
+ The same tracer is available in AssetGen, Assets, and Canvas Mode. In Canvas Export,
59
+ choose **Trace to SVG (vector)**; **SVG (embedded raster)** is self-contained but is
60
+ not a pixel trace. Inspect path count and render the SVG back to PNG before shipping.
61
+ Tracing works best for flat color, strong edges, cutouts, and logos; photographs,
62
+ gradients, and small text are not acceptable vectorization targets.
63
+
48
64
  ### Korean Service Patterns
49
65
 
50
66
  Korean product pages almost always use concrete visual evidence in the first viewport:
@@ -3,13 +3,13 @@
3
3
  "file": "assets/AssetGenWorkspace-B7tTK0Sx.css",
4
4
  "src": "_AssetGenWorkspace-B7tTK0Sx.css"
5
5
  },
6
- "_AssetGenWorkspace-Cguej69V.js": {
7
- "file": "assets/AssetGenWorkspace-Cguej69V.js",
6
+ "_AssetGenWorkspace-CleWSwPz.js": {
7
+ "file": "assets/AssetGenWorkspace-CleWSwPz.js",
8
8
  "name": "AssetGenWorkspace",
9
9
  "isDynamicEntry": true,
10
10
  "imports": [
11
11
  "index.html",
12
- "_VectorizePanel-DdlVvwV1.js"
12
+ "_KeyingPanel-DT8zsqpi.js"
13
13
  ],
14
14
  "dynamicImports": [
15
15
  "src/components/assetgen/SpriteRecipeWorkspace.tsx"
@@ -18,12 +18,12 @@
18
18
  "assets/AssetGenWorkspace-B7tTK0Sx.css"
19
19
  ]
20
20
  },
21
- "_VectorizePanel-DdlVvwV1.js": {
22
- "file": "assets/VectorizePanel-DdlVvwV1.js",
23
- "name": "VectorizePanel",
21
+ "_KeyingPanel-DT8zsqpi.js": {
22
+ "file": "assets/KeyingPanel-DT8zsqpi.js",
23
+ "name": "KeyingPanel",
24
24
  "imports": [
25
25
  "index.html",
26
- "_useAgentDialogFocus-DcWwFfYd.js"
26
+ "_useAgentDialogFocus-DPB8JITN.js"
27
27
  ]
28
28
  },
29
29
  "__vite-browser-external": {
@@ -32,15 +32,22 @@
32
32
  "src": "__vite-browser-external",
33
33
  "isDynamicEntry": true
34
34
  },
35
- "_useAgentDialogFocus-DcWwFfYd.js": {
36
- "file": "assets/useAgentDialogFocus-DcWwFfYd.js",
35
+ "_promptBuilderStore-CkwnWMbR.js": {
36
+ "file": "assets/promptBuilderStore-CkwnWMbR.js",
37
+ "name": "promptBuilderStore",
38
+ "imports": [
39
+ "index.html"
40
+ ]
41
+ },
42
+ "_useAgentDialogFocus-DPB8JITN.js": {
43
+ "file": "assets/useAgentDialogFocus-DPB8JITN.js",
37
44
  "name": "useAgentDialogFocus",
38
45
  "imports": [
39
46
  "index.html"
40
47
  ]
41
48
  },
42
49
  "index.html": {
43
- "file": "assets/index-Sch70vBs.js",
50
+ "file": "assets/index-DueH_AmZ.js",
44
51
  "name": "index",
45
52
  "src": "index.html",
46
53
  "isEntry": true,
@@ -55,16 +62,16 @@
55
62
  "src/components/card-news/CardNewsWorkspace.tsx",
56
63
  "src/components/agent/AgentWorkspace.tsx",
57
64
  "src/components/assets/AssetsWorkspace.tsx",
58
- "_AssetGenWorkspace-Cguej69V.js",
65
+ "_AssetGenWorkspace-CleWSwPz.js",
59
66
  "src/components/home/HomeWorkspace.tsx",
60
67
  "src/components/PromptLibraryPanel.tsx"
61
68
  ],
62
69
  "css": [
63
- "assets/index-DGRIdnsL.css"
70
+ "assets/index-0fpfY8vu.css"
64
71
  ]
65
72
  },
66
73
  "node_modules/pptxgenjs/dist/pptxgen.es.js": {
67
- "file": "assets/pptxgen.es-CSd_gCsx.js",
74
+ "file": "assets/pptxgen.es-C--aL3JM.js",
68
75
  "name": "pptxgen.es",
69
76
  "src": "node_modules/pptxgenjs/dist/pptxgen.es.js",
70
77
  "isDynamicEntry": true,
@@ -78,7 +85,7 @@
78
85
  ]
79
86
  },
80
87
  "src/components/GenerationRequestLogPanel.tsx": {
81
- "file": "assets/GenerationRequestLogPanel-giH1e4hp.js",
88
+ "file": "assets/GenerationRequestLogPanel-Cd2ArsWr.js",
82
89
  "name": "GenerationRequestLogPanel",
83
90
  "src": "src/components/GenerationRequestLogPanel.tsx",
84
91
  "isDynamicEntry": true,
@@ -87,7 +94,7 @@
87
94
  ]
88
95
  },
89
96
  "src/components/NodeCanvas.tsx": {
90
- "file": "assets/NodeCanvas-CF_NHudI.js",
97
+ "file": "assets/NodeCanvas-DcKs0w36.js",
91
98
  "name": "NodeCanvas",
92
99
  "src": "src/components/NodeCanvas.tsx",
93
100
  "isDynamicEntry": true,
@@ -99,7 +106,7 @@
99
106
  ]
100
107
  },
101
108
  "src/components/PromptImportDialog.tsx": {
102
- "file": "assets/PromptImportDialog-B40X9UAa.js",
109
+ "file": "assets/PromptImportDialog-CLkjHz7L.js",
103
110
  "name": "PromptImportDialog",
104
111
  "src": "src/components/PromptImportDialog.tsx",
105
112
  "isDynamicEntry": true,
@@ -112,7 +119,7 @@
112
119
  ]
113
120
  },
114
121
  "src/components/PromptImportDiscoverySection.tsx": {
115
- "file": "assets/PromptImportDiscoverySection-BWoUDHj1.js",
122
+ "file": "assets/PromptImportDiscoverySection-DoQw4zcd.js",
116
123
  "name": "PromptImportDiscoverySection",
117
124
  "src": "src/components/PromptImportDiscoverySection.tsx",
118
125
  "isDynamicEntry": true,
@@ -121,7 +128,7 @@
121
128
  ]
122
129
  },
123
130
  "src/components/PromptImportFolderSection.tsx": {
124
- "file": "assets/PromptImportFolderSection-DBSEyJb-.js",
131
+ "file": "assets/PromptImportFolderSection-CcLiliAZ.js",
125
132
  "name": "PromptImportFolderSection",
126
133
  "src": "src/components/PromptImportFolderSection.tsx",
127
134
  "isDynamicEntry": true,
@@ -130,7 +137,7 @@
130
137
  ]
131
138
  },
132
139
  "src/components/PromptLibraryPanel.tsx": {
133
- "file": "assets/PromptLibraryPanel-DLywJBpt.js",
140
+ "file": "assets/PromptLibraryPanel-BjxzL7t5.js",
134
141
  "name": "PromptLibraryPanel",
135
142
  "src": "src/components/PromptLibraryPanel.tsx",
136
143
  "isDynamicEntry": true,
@@ -142,52 +149,53 @@
142
149
  ]
143
150
  },
144
151
  "src/components/SettingsWorkspace.tsx": {
145
- "file": "assets/SettingsWorkspace-BawWId1t.js",
152
+ "file": "assets/SettingsWorkspace-Ctu_usKk.js",
146
153
  "name": "SettingsWorkspace",
147
154
  "src": "src/components/SettingsWorkspace.tsx",
148
155
  "isDynamicEntry": true,
149
156
  "imports": [
150
- "index.html"
157
+ "index.html",
158
+ "_promptBuilderStore-CkwnWMbR.js"
151
159
  ]
152
160
  },
153
161
  "src/components/agent/AgentWorkspace.tsx": {
154
- "file": "assets/AgentWorkspace-AJkVTaU5.js",
162
+ "file": "assets/AgentWorkspace-CBwn1WUC.js",
155
163
  "name": "AgentWorkspace",
156
164
  "src": "src/components/agent/AgentWorkspace.tsx",
157
165
  "isDynamicEntry": true,
158
166
  "imports": [
159
167
  "index.html",
160
- "_useAgentDialogFocus-DcWwFfYd.js"
168
+ "_useAgentDialogFocus-DPB8JITN.js"
161
169
  ]
162
170
  },
163
171
  "src/components/assetgen/SpriteRecipeWorkspace.tsx": {
164
- "file": "assets/SpriteRecipeWorkspace-Biy2yRAk.js",
172
+ "file": "assets/SpriteRecipeWorkspace-DZjsabce.js",
165
173
  "name": "SpriteRecipeWorkspace",
166
174
  "src": "src/components/assetgen/SpriteRecipeWorkspace.tsx",
167
175
  "isDynamicEntry": true,
168
176
  "imports": [
169
177
  "index.html",
170
- "_AssetGenWorkspace-Cguej69V.js",
171
- "_VectorizePanel-DdlVvwV1.js",
172
- "_useAgentDialogFocus-DcWwFfYd.js"
178
+ "_AssetGenWorkspace-CleWSwPz.js",
179
+ "_KeyingPanel-DT8zsqpi.js",
180
+ "_useAgentDialogFocus-DPB8JITN.js"
173
181
  ]
174
182
  },
175
183
  "src/components/assets/AssetsWorkspace.tsx": {
176
- "file": "assets/AssetsWorkspace-DIQ9wBuD.js",
184
+ "file": "assets/AssetsWorkspace-DdjXb45L.js",
177
185
  "name": "AssetsWorkspace",
178
186
  "src": "src/components/assets/AssetsWorkspace.tsx",
179
187
  "isDynamicEntry": true,
180
188
  "imports": [
181
189
  "index.html",
182
- "_VectorizePanel-DdlVvwV1.js",
183
- "_useAgentDialogFocus-DcWwFfYd.js"
190
+ "_KeyingPanel-DT8zsqpi.js",
191
+ "_useAgentDialogFocus-DPB8JITN.js"
184
192
  ],
185
193
  "css": [
186
194
  "assets/AssetsWorkspace-BdptASFf.css"
187
195
  ]
188
196
  },
189
197
  "src/components/canvas-mode/index.ts": {
190
- "file": "assets/index-D05Ong5g.js",
198
+ "file": "assets/index-ICf98ZcU.js",
191
199
  "name": "index",
192
200
  "src": "src/components/canvas-mode/index.ts",
193
201
  "isDynamicEntry": true,
@@ -199,7 +207,7 @@
199
207
  ]
200
208
  },
201
209
  "src/components/card-news/CardNewsWorkspace.tsx": {
202
- "file": "assets/CardNewsWorkspace-C_Eb2g7d.js",
210
+ "file": "assets/CardNewsWorkspace-BaNpBvJw.js",
203
211
  "name": "CardNewsWorkspace",
204
212
  "src": "src/components/card-news/CardNewsWorkspace.tsx",
205
213
  "isDynamicEntry": true,
@@ -208,7 +216,7 @@
208
216
  ]
209
217
  },
210
218
  "src/components/home/HomeWorkspace.tsx": {
211
- "file": "assets/HomeWorkspace-DHmXGtqC.js",
219
+ "file": "assets/HomeWorkspace-nKDhivwR.js",
212
220
  "name": "HomeWorkspace",
213
221
  "src": "src/components/home/HomeWorkspace.tsx",
214
222
  "isDynamicEntry": true,
@@ -217,12 +225,13 @@
217
225
  ]
218
226
  },
219
227
  "src/components/prompt-builder/PromptBuilderPanel.tsx": {
220
- "file": "assets/PromptBuilderPanel-D7NJto-1.js",
228
+ "file": "assets/PromptBuilderPanel-e6BSRAdj.js",
221
229
  "name": "PromptBuilderPanel",
222
230
  "src": "src/components/prompt-builder/PromptBuilderPanel.tsx",
223
231
  "isDynamicEntry": true,
224
232
  "imports": [
225
- "index.html"
233
+ "index.html",
234
+ "_promptBuilderStore-CkwnWMbR.js"
226
235
  ]
227
236
  }
228
237
  }